一. 基础知识
1.1 CLH(非饥饿公平自旋锁)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class CLH {
public static void main(String[] args) {
CLHLock LOCK = new CLHLock();
}
static class QNode {
public volatile boolean lock;
}
static interface Lock {
public void lock();
public void unlock();
}
static class CLHLock implements Lock{
//最后一个持有锁的线程node
AtomicReference<QNode> tail;
//上一个线程保存的锁节点
ThreadLocal<QNode> preThreadLock;
//当前节点保存的锁节点
ThreadLocal<QNode> currentLock;
public CLHLock(){
this.tail = new AtomicReference<>();
this.currentLock = ThreadLocal.withInitial(QNode::new);
this.preThreadLock = new ThreadLocal<>();
}
@Override
public void lock() {
QNode node = this.currentLock.get();
QNode pre = tail.getAndSet(node);
this.preThreadLock.set(pre);
node.lock = true;
while(pre.lock){
//自旋
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void unlock() {
this.currentLock.get().lock = false;
//防止当前线程释放后,马上又重新竞争到锁,从而产生的死锁。
this.preThreadLock.set(this.currentLock.get());
}
}
}
CLH结构模型
数据模型 CLH中主要保存两个数据,上一个节点的数据,和当前节点的数据,每个节点代表一个线程,于是就把线程串联起来变成了一个有序队列。
- 每个节点的锁状态主要靠每个节点数据的中的一个布尔属性控制,为了使每个这个属性每个线程实时可见,使用了volatile进行修饰。
- 用一个AtomicReference来包裹住最后一个线程节点的数据,使得后面添加进来的线程能看见上一个线程节点的状态。
线程模型 每个线程直接调用lock(),将各个线程串联成一个链表,只能感知到上一个线程的状态,当上一个线程完成了,unlock()锁释放后, 下一个线程就能直接获取锁,然后进行操作。
1.2 有了Synchronized为什么还需要AQS?1
性能缺陷
1.6以前,synchronized在性能上的缺陷还很严重,需要AQS轻量苏作为补充
功能缺陷
synchronized的功能缺陷就是当进入了死锁以后,没办法进行容错操作,死锁就一直卡主。
AQS可以做到死锁的补救措施, 防止synchronized产生死锁后没办法恢复。AQS从三个方面对死锁的容错补救做了改进:
- 响应中断,当一个线程进入了锁获取的阻塞状态后, 可以通过响应中断的方式进行唤醒而不是一直阻塞,synchronized便是不能响应中断的。
- 支持超时,当一个线程获取锁的时候设置一个超时时间, 超过这个时间就自动返回, 而不会一直阻塞,synchronized便不支持超时操作。
- 非阻塞api,提供非阻塞的api让获取锁的操作在失败的时候立即返回不进行阻塞操作。
二. AQS源码解析
AQS是基于CLH(非饥饿公平自旋锁)变体而来。
2.1 源码结构
2.2 数据结构
类 | 字段 | 说明 | |
---|---|---|---|
AbstractQueuedSynchronizer | volatile int | state | 通过这个state实现所有的同步锁状态 |
AbstractQueuedSynchronizer | Node | head | 头节点 |
AbstractQueuedSynchronizer | Node | tail | 尾部节点 |
AbstractQueuedSynchronizer$Node | volatile int | waitState | 节点等待的状态 |
AbstractQueuedSynchronizer$Node | Node | prev | 上一个节点 |
AbstractQueuedSynchronizer$Node | Node | next | 下一个节点 |
AbstractQueuedSynchronizer$Node | Thread | thread | 当前节点表示的线程 |
AbstractQueuedSynchronizer$Node | Node | nextWaiter | 指代下一个处于Condition状态的节点 |
waitState | 说明 |
---|---|
1(CANCELLED) | 节点已取消获取锁 |
-1(SIGNAL) | 线程已经准备好,等待资源释放,此时没有阻塞 |
-2(CONDITION) | 节点在线程等待队列中,等待唤醒 |
-3(PROPAGATE) | 指代下一个个节点获取的是共享锁 |
2.3 主要方法
方法 | 说明 |
---|---|
acquire(int arg) | 获取独占锁,不响应中断 |
acquireInterruptibly(int arg) | 获取独占锁,响应中断处理,抛出异常 |
acquireShared(int arg) | 获取共享锁,不响应终端 |
acquireSharedInterruptibly(int arg) | 获取共享锁,响应中断处理,抛出异常 |
acquireQueued(final Node node, int arg) | 独占不响应中断模式中,尝试 |
2.4 Condition
为什么会有这个接口的出现?
并发中的关键开发点有两个:
- 并发资源锁定
- 线程间的通信
AQS的lock指代的语义就是对一段内存资源的锁定进行操作,锁定对象是内存中的数据。对应就是并发资源锁定。
AQS的Condition指代的语义就是线程间的通信。
在synchronized中也有对应的语义:
synchronized指代的语义就是对并发内存的操作锁定,配合Object.wait()/notify()进行线程间的通信。
所以condition指代的就是着这个锁上的一个线程通信队列,用于在这个锁上的线程之间的协调
AQS与synchronized对比
对比项 | synchronized | AQS |
---|---|---|
wait()/await()语义 | 执行线程在这个对象上进行等待 | 执行线程在这个锁的这一个condition队列上进行等待 |
notify()/singal()语义 | 随机唤醒在这个对象上等待的一个线程 | 随机唤醒在这个锁的这一个condition队列上等待的一个线程 |
notifyAll()/singalAll()语义 | 唤醒所有在这个对象上等待的线程 | 唤醒所有在这个锁的这一个condition队列上等待的所有线程 |
等待队列 | 只有一个队列,就是以这个对象为队列维度 | 支持多个队列,Lock.newCondition()指代的就是一个队列,可以在这个队列上将线程进行wait和singal |
使用方法 | synchronized(this){ object.wait() / object.notify() } | Condition condition = lock.newCondition(); Lock.lock(); condition.await()/condition.singalAll() lock.unlock() |
前置条件 | 获取到这个对象锁 | 获取到这个condition所关联的锁 |