Java SDK并发包通过Lock和Condition两个接口来实现管程(管程——并发编程的万能钥匙),其中Lock用于解决互斥问题,Condition用于解决同步问题。
再造管程的理由
既然Java里已经存在管程的实现synchronized并且做了许多优化,为什么还需要在并发包里开发Lock和Condition。原因是synchronized申请资源的时候,如果申请不到,线程直接进入阻塞状态,而线程进入阻塞状态,啥也干不了,也释放不了线程已经占有的资源。
我们希望的是,对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它已经占有的资源,这样不可抢占这个条件就被破坏掉了。
设计一把互斥锁去解决这个问题:
- 能够响应中断,synchronized的问题是,持有锁A后,如果尝试获取锁B失败,那么线程就进入阻塞状态,一旦发生死锁就没有任何机会来唤醒阻塞的线程,但是如果阻塞状态的线程能够响应中断信号,也就是说我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁A,这样就破坏了不可抢占条件了。
- 支持超时,如果线程在一段时间内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁,这样也能破坏不可抢占条件。
- 非阻塞地获取锁,如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁,这样也能破坏不可抢占条件。
// 支持中断的API
void lockInterruptibly() throws InterruptedException;
// 支持超时的API
boolean tryLock(long time,TimeUnit unit) throws InterruptedException;
// 支持非阻塞获取锁的API
boolean tryLock();
使用synchronized
发生死锁
private static Object lockA = new Object();
private static Object lockB = new Object();
private static void task3(){
synchronized (lockA){
System.out.println(Thread.currentThread().getName() + "正在执行任务3A...");
synchronized (lockB){
System.out.println(Thread.currentThread().getName() + "正在执行任务3B...");
}
}
}
private static void task4(){
synchronized (lockB){
System.out.println(Thread.currentThread().getName() + "正在执行任务4B...");
synchronized (lockA){
System.out.println(Thread.currentThread().getName() + "正在执行任务4A...");
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(DealLock::task3,"Thread1");
Thread t2 = new Thread(DealLock::task4,"Thread2");
t1.start();
t2.start();
}
使用Lock完成相同任务
private static ReentrantLock lockC = new ReentrantLock();
private static ReentrantLock lockD = new ReentrantLock();
private static void task1() {
lockC.tryLock();
try {
System.out.println(Thread.currentThread().getName() + "正在执行任务1A...");
lockD.tryLock();
System.out.println(Thread.currentThread().getName() + "正在执行任务1B...");
}finally {
lockD.unlock();
lockC.unlock();
}
}
private static void task2() {
lockD.tryLock();
try {
System.out.println(Thread.currentThread().getName() + "正在执行任务2A...");
lockC.tryLock();
System.out.println(Thread.currentThread().getName() + "正在执行任务2B...");
}finally {
lockC.unlock();
lockD.unlock();
}
}
Thread t3 = new Thread(DealLock::task1,"Thread1");
Thread t4 = new Thread(DealLock::task2,"Thread2");
t3.start();
t4.start();
当获取不到锁的时候抛出错误,避免死锁发生。
公平锁与非公平锁
在使用ReentrantLock的时候会发现这个类有两个构造函数,一个是无参构造函数,一个是传入fair参数的构造函数。fair参数代表的是锁的公平策略,如果传入true就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。
//无参构造函数:默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//根据公平策略参数创建锁
public ReentrantLock(boolean fair){
sync = fair ? new FairSync()
: new NonfairSync();
}
锁都对应一个管程,管程中介绍过入口等待队列,如果一个线程没有获得锁,就会进入等待队列,当有线程释放锁的时候,就需要从等待队列中唤醒一个等待的线程。如果是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,很公平;如果是非公平锁,则不提供这个公平保证,有可能等待时间段的线程反而先被唤醒。
非公平锁演示
private static ReentrantLock notFairLock = new ReentrantLock();
private static void task(){
notFairLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"线程正在执行任务");
System.out.println("当前等待线程个数:" + notFairLock.getQueueLength());
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
notFairLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
// 启动50个线程排队去访问共享资源
for (int i = 0; i < 50; i++) {
new Thread(FairLockDemo::task,String.valueOf(i)).start();
}
}
结果分析:可以看到线程3抢到了线程2前面运行,由此可知在线程1刚好释放了锁时,被线程3抢占,入口等待队列中的线程2没有抢占到。
第一次运行结果
0线程正在执行任务
当前等待线程个数:0
1线程正在执行任务
当前等待线程个数:48
3线程正在执行任务
当前等待线程个数:47
2线程正在执行任务
当前等待线程个数:46
...
48线程正在执行任务
当前等待线程个数:1
49线程正在执行任务
当前等待线程个数:0
Process finished with exit code 0
第二次运行结果
19线程正在执行任务
当前等待线程个数:30
20线程正在执行任务
当前等待线程个数:29
21线程正在执行任务
当前等待线程个数:28
13线程正在执行任务
当前等待线程个数:27
23线程正在执行任务
当前等待线程个数:26
用锁的最佳实践
- 永远只在更新对象的成员变量时加锁;
- 永远只在访问可变的成员变量时加锁;
- 永远不再调用其他对象的方法时加锁;
Condition条件变量
如何利用两个条件变量快速实现阻塞队列
public class BlockedQueue<T>{
final Lock lock =
new ReentrantLock();
// 条件变量:队列不满
final Condition notFull =
lock.newCondition();
// 条件变量:队列不空
final Condition notEmpty =
lock.newCondition();
// 入队
void enq(T x) {
lock.lock();
try {
while (队列已满){
// 等待队列不满
notFull.await();
}
// 省略入队操作...
//入队后,通知可出队
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出队
void deq(){
lock.lock();
try {
while (队列已空){
// 等待队列不空
notEmpty.await();
}
// 省略出队操作...
//出队后,通知可入队
notFull.signal();
}finally {
lock.unlock();
}
}
}
注意:在Lock&Condition实现的管程里只能使用await、signal()、signalAll,而wait、notify、botifyAll只有在synchronized实现的管程里才能使用。