Java语言深入:编写高效的线程安全类

2024-03-19 15:38

本文主要是介绍Java语言深入:编写高效的线程安全类,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Java语言深入:编写高效的线程安全类

来源:ccidnet.com

 

原文地址:http://www.sudu.cn/info/html/edu/20080403/259009.html

 

 

Java 编程语言为编写多线程应用程式提供强大的语言支持。不过,编写有用的、没有错误的多线程程式仍然比较困难。本文试图概述几种方法,程式员可用这几种方法来创建高效的线程安全类。

并发性
只有当要解决的问题需要一定程度的并发性时,程式员才会从多线程应用程式中受益。例如,如果打印队列应用程式仅支持一台打印机和一台客户机,则不应该将他编写为多线程的。一般说来,包含并发性的编码问题通常都包含一些能并发执行的操作,同时也包含一些不可并发执行的操作。例如,为多个客户机和一个打印机提供服务的打印队列能支持对打印的并发请求,但向打印机的输出必须是串行形式的。多线程实现还能改善交互式应用程式的响应时间。

Synchronized 关键字
虽然多线程应用程式中的大多数操作都能并行进行,但也有某些操作(如更新全局标志或处理共享文件)不能并行进行。在这些情况下,必须获得一个锁来防止其他线程在执行此操作的线程完成之前访问同一个方法。在 Java 程式中,这个锁是通过 synchronized 关键字提供的。清单 1 说明了他的用法。

清单 1. 使用 synchronized 关键字来获取锁


public class MaxScore {
    int max;
    public MaxScore() {
        max = 0;
    }

    public synchronized void currentScore(int s) {
        if(s> max) {
            max = s;
        }
    }

    public int max() {
        return max;
    }
}

 



这里,两个线程不能同时调用 currentScore() 方法;当一个线程工作时,另一个线程必须阻塞。不过,能有任意数量的线程同时通过 max() 方法访问最大值,因为 max() 不是同步方法,因此他和锁定无关。

试考虑在 MaxScore 类中添加另一个方法的影响,该方法的实现如清单 2 所示。


清单 2. 添加另一个方法


   public synchronized void reset() {
        max = 0;
    }

 



这个方法(当被访问时)不仅将阻塞 reset() 方法的其他调用,而且也将阻塞 MaxScore 类的同一个实例中的 currentScore() 方法,因为这两个方法都访问同一个锁。如果两个方法必须不彼此阻塞,则程式员必须在更低的级别使用同步。清单 3 是另一种情况,其中两个同步的方法可能需要彼此独立。

清单 3. 两个独立的同步方法


import java.util.*;

public class Jury {
    Vector members;
    Vector alternates;

    public Jury() {
        members = new Vector(12, 1);
        alternates = new Vector(12, 1);
    }

    public synchronized void addMember(String name) {
        members.add(name);
    }

    public synchronized void addAlt(String name) {
        alternates.add(name);
    }

    public synchronized Vector all() {
        Vector retval = new Vector(members);
        retval.addAll(alternates);
        return retval;
    }
}

 



此处,两个不同的线程能将 members 和 alternates 添加到 Jury 对象中。请记住,synchronized 关键字既可用于方法,更一般地,也可用于所有代码块。清单 4 中的两段代码是等效的。

清单 4. 等效的代码


synchronized void f() {              void f() {      
    // 执行某些操作                                              synchronized(this) {
}                                                    // 执行某些操作

                                            }
                                     }  

 



所以,为了确保 addMember() 和 addAlt() 方法不彼此阻塞,可按清单 5 所示重写 Jury 类。

清单 5. 重写后的 Jury 类


import java.util.*;

public class Jury {
    Vector members;
    Vector alternates;

    public Jury() {
        members = new Vector(12, 1);
        alternates = new Vector(12, 1);
    }

    public void addMember(String name) {
        synchronized(members) {
            members.add(name);
        }
    }

    public void addAlt(String name) {
        synchronized(alternates) {
            alternates.add(name);
        }
    }

    public Vector all() {
        Vector retval;
        synchronized(members) {
            retval = new Vector(members);
        }

        synchronized(alternates) {
            retval.addAll(alternates);
        }

        return retval;
    }
}

 



请注意,我们还必须修改 all() 方法,因为对 Jury 对象同步已没有意义。在改写后的版本中,addMember()、addAlt() 和 all() 方法只访问和 members 和 alternates 对象相关的锁,因此锁定 Jury 对象毫无用处。另请注意,all() 方法本来能写为清单 6 所示的形式。

清单 6. 将 members 和 alternates 用作同步的对象


   public Vector all() {
        synchronized(members) {
            synchronized(alternates) {
                Vector retval;
                retval = new Vector(members);
                retval.addAll(alternates);
            }
        }
        return retval;
    }

 



不过,因为我们早在需要之前就获得 members 和 alternates 的锁,所以这效率不高。清单 5 中的改写形式是个较好的示例,因为他只在最短的时间内持有锁,并且每次只获得一个锁。这样就完全避免了当以后增加代码时可能产生的潜在死锁问题。

同步方法的分解
正如在前面看到的那样,同步方法获取对象的一个锁。如果该方法由不同的线程频繁调用,则此方法将成为瓶颈,因为他会对并行性造成限制,从而会对效率造成限制。这样,作为一个一般的原则,应该尽可能地少用同步方法。尽管有这个原则,但有时一个方法可能需要完成需要锁定一个对象几项任务,同时还要完成相当耗时的其他任务。在这些情况下,可使用一个动态的“锁定-释放-锁定-释放”方法。例如,清单 7 和清单 8 显示了可按这种方式变换的代码。

清单 7. 最初的低效率代码


public synchonized void doWork() {
         unsafe1();
    write_file();
    unsafe2();
}

 



清单 8. 重写后效率较高的代码


public void doWork() {
    synchonized(this) {
                 unsafe1();
    }
    write_file();
    synchonized(this) {
        unsafe2();
    }
}

 



清单 7 和清单 8 假定第一个和第三个方法需要对象被锁定,而更耗时的 write_file() 方法不必对象被锁定。如你所见,重写此方法以后,对此对象的锁在第一个方法完成以后被释放,然后在第三个方法需要时重新获得。这样,当 write_file() 方法执行时,等待此对象的锁的所有其他方法仍然能运行。将同步方法分解为这种混合代码能明显改善性能。不过,你需要注意不要在这种代码中引入逻辑错误。

嵌套类
内部类在 Java 程式中实现了一个令人关注的概念,他允许将整个类嵌套在另一个类中。嵌套类作为包含他的类的一个成员变量。如果定期被调用的的一个特定方法需要一个类,就能构造一个嵌套类,此嵌套类的唯一任务就是定期调用所需的方法。这消除了对程式的其他部分的相依性,并使代码进一步模块化。清单 9,一个图像时钟的基础,使用了内部类。

清单 9. 图像时钟示例

public class Clock {
    protected class Refresher extends Thread {
        int refreshTime;
        public Refresher(int x) {
            super("Refresher");
            refreshTime = x;
        }

        public void run() {
            while(true) {
                try {
                    sleep(refreshTime);
                }
                catch(Exception e) {}
                repaint();
            }
        }
    }

    public Clock() {
        Refresher r = new Refresher(1000);
        r.start();
    }

    private void repaint() {
        // 获取时间的系统调用
        // 重绘时钟指针
    }
}

清单 9 中的代码示例不靠所有其他代码来调用 repaint() 方法。这样,将一个时钟并入一个较大的用户界面就相当简单。

事件驱动处理
当应用程式需要对事件或条件(内部的和外部的)作出反映时,有两种方法或用来设计系统。在第一种方法(称为轮询)中,系统定期确定这一状态并据此作出反映。这种方法(虽然简单)也效率不高,因为你始终无法预知何时需要调用他。

第二种方法(称为事件驱动处理)效率较高,但实现起来也较为复杂。在事件驱动处理的情况下,需要一种发信机制来控制某一特定线程何时应该运行。在 Java 程式中,你能使用 wait()、notify() 和 notifyAll() 方法向线程发送信号。这些方法允许线程在一个对象上阻塞,直到所需的条件得到满足为止,然后再次开始运行。这种设计减少了 CPU 占用,因为线程在阻塞时不消耗执行时间,并且可在 notify() 方法被调用时即时唤醒。和轮询相比,事件驱动方法能提供更短的响应时间。

创建高效的线程安全类的步骤
编写线程安全类的最简单的方法是用 synchronized 声明每个方法。虽然这种方案能消除数据损坏,但他同时也会消除你预期从多线程获得的所有收益。这样,你就需要分析并确保在 synchronized 块内部仅占用最少的执行时间。你必须格外关注访问缓慢资源 ? 文件、目录、网络套接字和数据库 ? 的方法,这些方法可能降低你的程式的效率。尽量将对这类资源的访问放在一个独立的线程中,最佳在所有 synchronized 代码之外。

一个线程安全类的示例被设计为要处理的文件的中心储存库。他和使用 getWork() 和 finishWork() 和 WorkTable 类对接的一组线程一起工作。本例旨在让你体验一下全功能的线程安全类,该类使用了 helper 线程和混合同步。请注意继续添加要处理的新文件的Refresher helper 线程的用法。本例没有调整到最佳性能,非常明显有许多地方能改写以改善性能,比如将 Refresher 线程改为使用 wait()/notify() 方法事件驱动的,改写 populateTable() 方法以减少列出磁盘上的文件(这是高成本的操作)所产生的影响。

小结
通过使用可用的全部语言支持,Java 程式中的多线程编程相当简单。不过,使线程安全类具有较高的效率仍然比较困难。为了改善性能,你必须事先考虑并谨慎使用锁定功能。

这篇关于Java语言深入:编写高效的线程安全类的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/826470

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2