Lock And AQS
锁是用来控制多线程访问共享资源的一种方式,一般情况下,锁可以防止多个线同时访问共享资源,但有些锁可以允许多个线程并发地访问共享资源,如读写锁中的读锁。
Lock 接口
一般情况下,我们常用 synchronized 关键字来实现锁的功能,前面提到 synchronize 关键字是利用对象监视器来实现同步,synchronize获取锁和释放锁很固定,就是先获取再释放,虽然简便了同步化管理,但是不够灵活方便。比如我们要实现这种需求:获取锁 A 后,再去获得锁 B,获取锁 B 后释放 A,再去获得锁 C,获得锁 C 后释放 B,再去获得锁 D……在这种场景下,使用 synchronize 关键字就不太容易实现了,jdk1.5后新增了 Lock 接口,位于 concurrent 包下。
Lock 提供了与 synchronized 关键字同样的功能,只是在使用时需要显示地获取锁、释放锁,而 synchronize 关键字不需要,但是 Lock 会更灵活,它提升了锁的获取、释放的操作性,提供了锁可中断获取锁、超时获取锁的等 synchronize 关键字不具备的功能。
注:synchronized 关键字随着不断地优化,性能越来越高,jdk8的 ConcurrentHashMap 抛弃了 ReentrantLock 而使用 synchronized
Lock 简单使用
1 | public class LockTest { |
注意:释放锁要在 finally 代码块里,保证锁最终能被释放,不要将获取锁的过程放在 try 代码块中,因为如果获取锁时发生了异常,异常抛出的同时,也会导致锁无故释放,tyr 代码块里加锁失败,会走 finally 再去释放锁会抛出”IllegalMonitorStateException”异常,这不是我们想要的。而放到try 代码块外,在获取锁失败后线程退出并排队重新去获得锁。
测试:
MyLock
1 | public class MyLock extends ReentrantLock { |
LockTest1
加锁操作在 try 外
1 | public class LockTest1 { |
LockTest1运行结果抛出:
1 | java.lang.ArithmeticException: / by zero |
LockTest2
加锁操作在try里
1 | public class LockTest2 { |
LockTest2抛出异常:
1 | java.lang.IllegalMonitorStateException |
我们发现 ArithmeticException 异常被吞了,原因就如上面所说,放到try 里,自定义加锁抛出异常,会自动解锁,继而走 finally 再次释放锁,但此时锁已经释放了,抛出IllegalMonitorStateException 异常,由于单返回值的原因,ArithmeticException 异常被吞了。
ReentrantLock 的 unlock()方法源码注释
就算在 finally 代码块里进行判断:
1 | //如果当前线程持有锁才去释放锁 |
ArithmeticException 异常还是会被吞掉,我们得不到异常提示。
所以将lock.lock()代码放到 try 前是最佳实践!!!
Lock 接口具有而 syncronized 不具有的特性:
尝试非阻塞地获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁。
能被中断地获取锁:获取到锁的线程能被中断,抛出中断异常并释放锁。
超时获取锁:在指定的截止时间之前获取锁,如果在指定的时间内仍未获取锁则返回,避免死锁。
Lock 接口 API
void lock()
获取锁,调用该方法后当前线程会获取锁,并返回。
void lockInterruptibly()
可中断地获取锁,即在锁的获取中可以中断当前线程,抛出中断异常并释放锁。
boolean tryLock()
非阻塞地尝试获取锁,调用后立即返回,获得锁返回true,反之返回 false
boolean tryLock(long time, TimeUnit unit)
超时获取锁,当前线程在以下三种情况下返回
当前线程在超时时间内获取了锁返回;
当前线程被中断返回;
当前线程到指定时间后仍未获得锁返回;
void unlock()
释放锁
Condition newCondition()
获取等待通知组件,该组件与当前线程绑定,当前线程只有获取了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
AbstractQueuedSynchronizer
简称 AQS,在查看 Lock 接口典型的的子类 ReentrantLock 和 ReentrantReadWriteLock 源码后发现这些类的实现都是通过聚合 AQS 的子类来完成线程访问控制的。
AQS:队列同步器,是用来构建锁的基础框架,使用一个 int 类型的成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程排队的工作(摘自《Java并发编程的艺术》)
AQS是一个抽象类,所以需要子类继承它来使用,子类推荐被定义为自定义同步组件的静态内部类(如ReentrantLock 的 Sync),同步器支持独占式和共享式地获取锁。
AQS 采用了“模板方法”设计模式,将一些底层操作封装成模板方法,简化了锁的实现。
AQS 属性
1 | /** |
Node 节点
Node用于包装未获得同步状态的 Thread、构成了 FIFO 队列,
1 | static final class Node { |
可以看出 Node 节点构成的等待队列是一个双向链表
同步器可以被重写的方法
1 | /** |
AQS模板方法封装了底层的一些操作,这些模板方法到分析其子类(ReentrantLock 和 ReentrantReadWriteLock)时再说吧。
- Title: Lock And AQS
- Author: 薛定谔的汪
- Created at : 2018-08-07 18:01:54
- Updated at : 2023-11-17 19:37:37
- Link: https://www.zhengyk.cn/2018/08/07/java/lock-aqs/
- License: This work is licensed under CC BY-NC-SA 4.0.