您的位置:首页 > 编程语言 > Java开发

[译文]一些Java并发技巧

2009-11-09 09:58 169 查看
原文:Some Java Concurrency Tips
作者:Carol McDonald
出处:
http://weblogs.java.net/blog/caroljmcdonald/archive/2009/09/17/some-java-concurrency-tips

这是来自Joshua Bloch、Brian Goetz和其他人的一个关于一些并发技巧的汇总。

首先选择不可变的对象/数据

不可变对象(immutable object)在构建之后就不会改变了,不可变对象更简单、安全,不需要锁,并且是线程安全的。为了使得不可变对象无需提供setter/mutator方法,把域修饰成private final的,并可防止其子类别化。如果不可选择不变性,那么限制可变的状态,越少的可变状态意味着越少的协调配合。在任何实际可行的情况下把域声明成final的,final域比可变域要简单得多。
在线程共享可变数据的时候,每个读线程或者写线程都必须要协调对数据的访问,未能同步共享的可变数据可能会导致原子性的失败、竞争条件、不一致的状态,以及其他形式的非确定性。这些不确定的问题都是最难以调试的。
使并发交互只在明确定义的地方发生,减少共享数据,考虑复制而不是共享。

Web应用的线程风险

一个Servlet的get、post、service方法能够同时被多个客户端调用,多线程Servlet实例和静态变量是共享的,因此如果是可变的话,那么访问必须加以协调。Servlet是典型的生命期长且被线程高频度加载的对象,如果过分同步的话性能会受影响,或者尽量共享不可变(final)数据,或者尝试完全不共享,请求参数和局部变量会更安全。

保持锁定的时间尽可能的短

在同步区内完成尽可能少的工作,把不需要加锁的代码放到同步代码块之外,特别是如果这些代码非常耗时的话!



Lock接口比使用同步代码块提供了更详尽的锁定操作,其有一个好处是如果锁是不可用的话,则不会导致阻塞。你应该只有在需要的时候才获取锁来读取或者写入共享的数据,并在一个finally子句内部解锁,以确保锁被释放。下面是一个使用ReentrantReadWriteLock的例子:



一种减少保持锁定的时间的做法是锁分拆或者锁分离,该方法是为各个状态变量使用不同的锁而不是使用单个锁,这减小了锁的粒度,提供了更好的可伸缩性,但必须要按照规则顺序来取用锁,否则会有死锁的危险。

首先选择executor和task而不是thread

使用Java并发工具Executor框架(Java Concurrency Utilities Executor Framework)而不是直接使用thread来工作。Executor服务把任务的提交从执行策略中分离出来,从可运行任务的角度来考虑,由executor服务来执行这些任务。



executor可以被直接创建或者是通过使用Executors类的工厂方法来创建:





现在给出一个使用Executor、Executors和ExecutorService类的例子:



这个例子是一个web service类,该类使用一个固定的线程池来处理多路同时传入的连接(connection),固定线程池使用返回一个ExecutorService对象的Executors类的newFixedThreadPool方法来初始化,收到的连接通过调用ExecutorService类型的pool对象的execute方法来处理,给该方法传入一个Runnable对象,Runnable对象的run方法处理连接,当run方法执行完成时,线程会被自动返还给线程池。如果有连接进来但所有的线程都已正被使用的话,那么主循环将会阻塞直到有一个线程被释放。

首先选择并发实用工具而不是wait和notify操作

每次只要你打算使用wait和notify方法时,查看一下java.util.concurrent中是否有你所需要的类,并发集合总是会提供诸如List、Queue和Map一类的标准集合接口的高性能并发实现。



BlockingQueue是带有扩展的阻塞方法的并发对象,这些方法会一直等待(或者阻塞),直到检索的元素变为可用的,或者是存储空间变为可用的时才继续执行。



生产者消费者模式

阻塞队列对生产者消费者模式(Producer Consumer Pattern)来说很有用,在该模式中,生产者线程把工作项目加入队列而消费者线程则把工作项目从队列中取出并进行处理。以下是一个被多个线程使用的记录器的消费者模式(Consumer Pattern)例子:



下面是使用logger的生产者(Producer)的例子,一个新的ArrayBlockingQueue被实例化以传递给logger的构造方法,在run方法中,信息被放入队列中以进行记录,如果队列满了的话,则放入操作会被阻塞直到logger已把队列中的消息取走才继续。



同步器

同步器是一些用来帮助线程间的协调访问的对象,最常用的同步器是倒计数锁存器(CountDownLatch)和信号量(Semaphore),同步器的使用可以消除大多数wait或者notify方法的使用。



下面是一个使用信号量来控制资源池访问的例子,多个线程可以请求使用一个资源,并在完成使用后把它交还。
在构造方法中,我们创建了一个新的信号量,信号量的大小与我们正在创建的资源池的大小相同。
在getResource()方法中,信号量的aquire方法被调用,尝试取得使用资源的许可,如果有资源可用的话,则该方法会有返回,然后会从池中返回一个资源。如果所有的资源都正在使用当中的话,则aquire方法的调用会阻塞,直到另一个线程调用该信号量的release方法时才继续执行。当某个线程完成资源的使用时,该资源会被返还给池,然后release方法会被调用。aquire和release方法都可以看作是原子操作。



多线程的延迟初始化需要一些技巧

当多个线程共享一个延迟初始化的域时,对该域的访问必须是同步的,否则可能会导致一些非确定性的错误出现。



首先选择正常的初始化

不要使用延迟初始化,除非对象或者域的初始化代价昂贵且不经常使用。通常情况下,正常的初始化是最好的;) 下面是一个线程安全的单例(singleton)预先初始化的例子,私有的final实例域和私有的构造方法使得该单例是不可变的。



如果基于静态域的性能问题而需要使用延迟加载的话,则使用按需初始化持有者模式(initialize-on-demand holder pattern),该模式利用了类直到被使用时才会初始化这一担保



参考资料和更多信息:

Effective Java,第二版,Joshua Bloch著
Java Concurrency in Practice,Brian Goetz著
Robust and Scalable Concurrent Programming: Lessons from the Trenches
Concurrency: Past and Present
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: