本文摘自《实战Java高并发程序设计》,了解详细内容推荐购买原版书籍。
第三章 JDK并发包
为了更好的支持并发程序,JDK内部提供了大量实用的API和框架。本章主要介绍这些JDK内部的功能,主要分为三大部分:
一、 首先介绍有关同步控制的工具,之前synchronized关键字就是一种同步控制手段。
二、 详细介绍JDK对线程池的支持,使用线程池,将能很大程度提高线程调度的性能。
三、 介绍JDK一些并发容器,这些容器专为并行访问设计,是高效、安全、稳定的工具。
3.1 多线程的团队协作:同步控制
同步控制是并发程序必不可少的重要手段。之前介绍的synchronized关键字就是一种最简单的。他决定一个线程是否可以访问临界区资源。
本节介绍synchronized、Object.wait()和Object.notify()方法的替代品(或者说增强版)——重入锁。
3.1.1 synchronized的功能扩展:重入锁
重入锁可完全代替synchronized关键字。使用java.util.concurrent.locks.ReentrantLock类实现。下面为使用案例:
开发人员必须手动指定何时加锁,何时释放锁。所以重入锁灵活性远远好于synchronized。注意:退出临界区时,记得释放锁。否则其他线程没机会访问了。
为什么叫重入锁?因为这种锁是可以反复进入的。当然这里反复进入仅仅局限于一个线程。上述代码7-12行可以写成下面形式:
这种情况下,一个线程连续获得两把锁是允许的!不过不允许这样做,同一个线程在第二次获得锁时,会和自己产生死锁。但是!获得多少次锁要记得释放多少次锁。
重入锁还可以提供中断处理的能力。
- 中断响应
对于synchronized来说,如果一个线程在等待锁,会有两种情况:要么获得锁继续执行,要么继续等待。而使用重入锁,提供另外一种可能,就是线程可以中断。也就是说在等待锁的过程中,程序可以根据需要取消对锁的请求。
下面代码产生了一个死锁,但得益于中断,可以很好解决。:
线程t1和t2启动后,t1线占用lock1,再占用lock2;t2先占用lock2,在请求lock1。因此,很容易形成t1和t2之间相互等待。这里对锁的请求,统一使用lockInterruptibly()方法。这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断。
代码47行,主线程main处于休眠,此时,两个线程处于死锁状态,49行,由于t2线程被中断,所以t2会放弃对lock1的申请,同时释放已获得lock2.导致t1可以将继续顺利得到lock2而继续执行下去。
执行上述代码,输出:
可以看到,中断后,两个线程双双退出。但真正完成工作的只有t1。而t2放弃其任务直接退出,释放资源。
- 锁申请等待时限
除等待外部通知之外,避免死锁还有另外一种方法,就是限时等待。通常无法判断为什么一个线程迟迟无法拿到锁,或许是因为死锁,获取因为产生了饥饿。如果给一个等待时间,让线程自动放弃,那么来说对系统是有意义的。可以使用tryLock()方法进行一次限时的等待。
限时等待锁的使用:
由于占用锁的线程会持有6秒,所以另外一个线程无法再5秒等待时间获得锁,请求失败。