明天你会感谢今天奋力拼搏的你。
ヾ(o◕∀◕)ノヾ
首先对锁的各种类型做个介绍:
如下所示,locks包里包含了锁的核心类和接口

其中包括三个同步器:
一个条件接口Condition:条件接口,提供了线程之间的协调机制,优化了等待通知的机制,有效避免惊群效应带来的损耗。
一个LockSupport工具类:线程阻塞的工具类,实现了park/unpark机制。
一些锁的接口和实现:
lock()最常用,lockInterruptibly()方法一般更昂贵。
有的impl可能没有实现lockInterruptibly(),只有真的需要效应中断时,才使用,使用之前看看impl对该方法的描述。
lock()方法不会对interrupt()方法响应,不会像Wait一样进行报错,要用lockInterruptibly方法。
AbstractQueuedSynchronizer抽象队列同步器简称AQS,定义了FIFO同步队列和同步状态的获取和释放方法,是构件其他同步组件的基础组件(如:ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch等)。其中类图如下:
AQS的核心逻辑根据它的名字:“抽象队列同步器”,就已经总结的简单明了。
抽象:
其采用的模板方法设计模式,作为其他同步组件的基础组件。
队列:
定义了一个FIFO双向链表的同步队列,来完成线程获取锁的排队工作,线程获取锁失败后,会被添加至队尾。
还定义了一个条件队列ConditionObject供子类使用,用来精确的控制唤醒。参考ReentrantLock类的newCondition方法。
同步器:
通过维护了一个volatile修饰的state字段作为同步状态,当线程调用lock方法时,如果state=0,说明该资源没有被占有,可以获得锁;如果state>=1,说明该资源已被占用,需要加入同步队列等待。
为什么采用双向链表结构?
AQS里面最重要的就是两个操作和一个状态:获取操作(acquire)、释放操作(release)、同步状态(state)。两个操作通过各种条件限制,如下:
AQS采用的是模板方法的设计模式,子类通过继承同步器并实现它的抽象方法来管理同步状态,可重写的方法tryAcquire(int arg)、tryRelease(int arg)、tryAcquireShared(int arg)、tryReleaseShared(int arg)、isHeldExclusively()。
内部获取操作如acquire方法会调用子类定义的tryAcquire来尝试获取锁,失败则通过CAS自旋加入同步队列,并调用park进行等待。
内部释放操作如release方法会调用子类定义的tryRelease来尝试释放锁,成功则调用unpark唤醒head节点的后继节点,失败返回false。
队列:AQS通过一个内置的FIFO双向队列来完成线程排队的工作。内部有head和tail字段用来记录队首和队尾元素。
/**
* Head of the wait queue, lazily initialized.
*/
private transient volatile Node head;
/**
* Tail of the wait queue. After initialization, modified only via casTail.
*/
private transient volatile Node tail;
其Node类中声明了waiter字段用来存放AQS队列中的线程引用。Node中的字段修改都是通过CAS操作来完成,保证其原子性。
持有锁的线程:存储在父类AbstractOwnableSynchronizer的exclusiveOwnerThread字段中,通过get/set方法赋值。保证其线程安全的方式是外层调用时通过判断state的cas操作来实现,以ReentrantLock类下的tryLock方法为例:
final boolean tryLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) {
if (++c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
}
return false;
}
状态:state=0,说明该资源没有被占有,可以获得锁;如果state!=0,说明该资源已被占用,如果是可重入锁,重入的次数其实就是对state+1。
ReentrantLock是一种同时拥有独占式、可重入、可中断、公平/非公平特性的同步器!
先来看看它的关系图谱:
其内部拥有三个内部类,分别为Sync、FairSync(公平锁)、NonfariSync(非公平锁),其中FairSync、NonfariSync继承父类Sync。Sync又继承了AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。
通过构造方法可以生成公平锁还是非公平锁,默认为非公平锁。

FairSync和NonfariSync的主要区别是公平锁要申请锁的先后顺序,也就是入队的先出队获得锁。源码中如何判断就在如下方法中,可以自行往下跟踪了解。

怎么实现的可重入?
先从owner说起:要想重入当然要判断是否是当前线程,当前线程才能再次进入。ReentrantLock中有个getOwner如下,其实调用的就是AQS中的exclusiveOwnerThread字段。

再说count:既然可重入多次那么当然要记录重入了多少次,程序中需要注意lock了多少次,后续要unlock多少次。如果没有释放相同次数就不会释放锁(即其他线程抢不到锁)。如果释放了超过的数量就会报IllegalMonitorStateException。这个重入次数其实就记录在AQS的state字段内。

概念:
适用场景:
锁降级:
为保证多个字段的一致性时要加读锁。利用读写锁互斥的关系,让读写顺利完成,而不是读到一半后面的字段被改了。
JDK8中引入的StampedLock提供了一种替代方案,允许读锁在一定条件下升级为写锁。
StampedLock 是JUC并发包里面 JDK8 版本新增的一个锁,是读写锁的一种具体实现,它提供了更灵活的并发控制。与ReentrantReadWriteLock 不同的是其不提供可重入性,不是基于某个Lock或者ReadWriteLock接口,也没有引入AQS,而是基于CLH锁思想实现,并且StampedLock不支持条件变量 Condition。
如下是三个锁的并发度比较:
| 锁 | 并发度 |
| ReentrantLock | 读读互斥,读写互斥,写写互斥 |
| ReentrantReadWriteLock | 读读不互斥、读写互斥、写写互斥 |
| StampedLock | 读读不互斥、读写不互斥、写写互斥 |
StampedLock 的读写不互斥是指 乐观读和写,而不是悲观读和写。
读锁示例:
long stamp = lock.readLock();
try {
// 读操作
} finally {
lock.unlockRead(stamp);
}
写锁示例:
long stamp = lock.writeLock();
try {
// 写操作
} finally {
lock.unlockWrite(stamp);
}
乐观读锁示例:
long stamp = lock.tryOptimisticRead();
// 读操作
if (!lock.validate(stamp)) {
// 乐观读失败,升级为读锁
stamp = lock.readLock();
try {
// 重新读取数据
} finally {
lock.unlockRead(stamp);
}
}
注意:如示例代码所示,lock.tryOptimisticRead();获取悲观锁方法并不真正获取锁,而是假设在读操作期间数据不会被修改。使用戳记来验证数据是否在读操作期间被修改,必要时需要升级为悲观读锁来保证数据一致性。
转换锁(锁的升降级)
与ReentrantReadWriteLock不同,StampedLock是支持锁升级的。
Synchronized优点:
Synchronized缺点:
无法实现一些锁的高级功能如:公平锁、中断锁、超时锁、读写锁、共享锁等
Lock优点:
Lock缺点:
需手动释放锁unlock,新手使用不当可能造成死锁
全部评论