可扩展性和线程安全

## 用ExecutorService管理线程

每个ExecutorService都代表一个线程池,其作用是将线程的创建与执行了过程分离开来,而不是将线程的生命周期管理和任务的执行过程绑在一起。我们可以按需配置线程池的类型,单线程的、带缓存的、基于优先级的、按预定时间调度/按周期调度的和固定大小的,待调度任务的等待队列的大小亦可通过代码进行配置。通过这组API,我们可以很容易地调度执行任意数量的任务。如果只是想简单地丢一个任务进去执行,我们只需将任务的执行过程封装到一个Runnable接口中就可以了。而对于那些需要返回结果的任务,我们可以将其封装到Callable接口里面。

ExecutorService还提供了一些检测服务是否已经被中止或关闭的方法。但我们最好不要依赖于这些方法,而是应该好好设计如何完成任务而不是操控线程/服务的消亡,即专注于任务的完成(应用程序逻辑)而非线程的终止(基础设施的活动)。

使线程协作

使用线程池

注意一种情况:

线程池内的线程在等待某些任务的响应,而这些任务却在ExecutorService的队列中等待执行机会。如果我们没有设置超时的话,这将演变成一种潜在的“线程池诱发型死锁”( Pool Induced Deadlock )。为了避免死锁,当我们等待其他任务/线程响应的时候不能占住当前的主调线程。

我们不但需要创建一个用于保存计算任务返回结果的不变类,还需要花心思去设计如何不断地分派任务以及协调处理任务的返回结果。增加了程序的复杂度。

使用CountDownLatch辅助实现线程的协作

我们把线程闩的值设为1,于是CountDownLatch变成了一个标识所有任务全部结束的开关。我们还可以将线程闩的值设大一些,以便让多个线程可以同时处于等待其释放的状态。如果我们希望多个线程在继续执行其他任务之前都能抵达同一个协作位置,就可以采用这种方法来实现。需要注意的是,CountDownLatch是不可复用的。所以一旦某个线程闩的实例在同步动作中被用过了之后,就必须废弃掉。如果我们的程序需要一个可复用的同步点,则应该用CyclicBarrier来替代CountDownLatch。

使用队列进行数据交换

在线程间互发多组数据,则采用BlockingQueue。

Java 7 Fork-Join API

Fork-join使用了work-stealing策略,即线程在完成自己的任务之后,发现其他线程还有活没干完,就主动帮其他人一起干。该策略的使用不但提升了API的性能,而且还有助于提高线程利用率。Fork-join API非常适合于解决那些可以递归分解至小到足以顺序运行的问题。通过使用由ForkJoinPool管理的线程,多个较小的分解任务可以被同时执行。

可扩展集合类

Java5的java.util.concurrent包中则定义了很多支持并发访问的数据结构。

Lock和Synchronized

synchronized

我们可以使用synchronized关键字来显示地获取对象的monitor/锁,并通过“先获取monitor并在代码块结尾处释放掉”的方式来帮助线程穿越内存栅栏。但是synchronized关键字的能力过于简单并且在使用上也有相当多的限制。更加不幸的是,由于我们没办法设置synchronized关键字在获取锁的时候只等待有限时间,所以synchronized可能会导致线程为了加锁而无限期地处于阻塞状态。

使用synchronized关键字等同于使用了互斥锁,即其他线程都无法获得对象monitor的访问权。这种策略对于读多写少的应用而言是很不利的,因为即使多个读者看似可以并发运行,但他们实际上还是串行的,并将最终导致并发性能的下降。

Lock接口

Lock接口的实现同样保证了对于其方法的调用将会跨越内存栅栏。我们可以用Lock接口的lock()和unlock()函数来获取和释放锁。通过tryLock()函数,加锁的请求可以不被强制阻塞。

转载请注明:转载自srzyhead的博客(https://srzyhead.github.io)

本文链接地址: java虚拟机并发编程 (4-可扩展性和线程安全)