明天你会感谢今天奋力拼搏的你。
ヾ(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字段内。
概念:
适用场景:
锁降级:
问题:读锁用在什么时候?
为保证多个字段的一致性时要加读锁。如:读写都要去操作多个字段,为保证操作读的时候写已经完成了,即防止写到一半字段的时候去读,导致这多个字段的一致性被破坏。
全部评论