首页 并发锁 - AQS详解
文章
取消

并发锁 - AQS详解

一. 基础知识

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中主要保存两个数据,上一个节点的数据,和当前节点的数据,每个节点代表一个线程,于是就把线程串联起来变成了一个有序队列。

  1. 每个节点的锁状态主要靠每个节点数据的中的一个布尔属性控制,为了使每个这个属性每个线程实时可见,使用了volatile进行修饰。
  2. 用一个AtomicReference来包裹住最后一个线程节点的数据,使得后面添加进来的线程能看见上一个线程节点的状态。

线程模型 每个线程直接调用lock(),将各个线程串联成一个链表,只能感知到上一个线程的状态,当上一个线程完成了,unlock()锁释放后, 下一个线程就能直接获取锁,然后进行操作。

1.2 有了Synchronized为什么还需要AQS?1

  1. 性能缺陷

    1.6以前,synchronized在性能上的缺陷还很严重,需要AQS轻量苏作为补充

  2. 功能缺陷

    synchronized的功能缺陷就是当进入了死锁以后,没办法进行容错操作,死锁就一直卡主。

    AQS可以做到死锁的补救措施, 防止synchronized产生死锁后没办法恢复。AQS从三个方面对死锁的容错补救做了改进:

    1. 响应中断,当一个线程进入了锁获取的阻塞状态后, 可以通过响应中断的方式进行唤醒而不是一直阻塞,synchronized便是不能响应中断的。
    2. 支持超时,当一个线程获取锁的时候设置一个超时时间, 超过这个时间就自动返回, 而不会一直阻塞,synchronized便不支持超时操作。
    3. 非阻塞api,提供非阻塞的api让获取锁的操作在失败的时候立即返回不进行阻塞操作。

二. AQS源码解析

AQS是基于CLH(非饥饿公平自旋锁)变体而来。

2.1 源码结构

2.2 数据结构

 字段说明
AbstractQueuedSynchronizervolatile intstate通过这个state实现所有的同步锁状态
AbstractQueuedSynchronizerNodehead头节点
AbstractQueuedSynchronizerNodetail尾部节点
AbstractQueuedSynchronizer$Nodevolatile intwaitState节点等待的状态
AbstractQueuedSynchronizer$NodeNodeprev上一个节点
AbstractQueuedSynchronizer$NodeNodenext下一个节点
AbstractQueuedSynchronizer$NodeThreadthread当前节点表示的线程
AbstractQueuedSynchronizer$NodeNodenextWaiter指代下一个处于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

为什么会有这个接口的出现?

并发中的关键开发点有两个:

  1. 并发资源锁定
  2. 线程间的通信

AQS的lock指代的语义就是对一段内存资源的锁定进行操作,锁定对象是内存中的数据。对应就是并发资源锁定。

AQS的Condition指代的语义就是线程间的通信。

在synchronized中也有对应的语义:

synchronized指代的语义就是对并发内存的操作锁定,配合Object.wait()/notify()进行线程间的通信。

所以condition指代的就是着这个锁上的一个线程通信队列,用于在这个锁上的线程之间的协调

AQS与synchronized对比

对比项synchronizedAQS
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所关联的锁

参考

Java并发之Condition详解

本文由作者按照 CC BY 4.0 进行授权

并发工具 - Unsafe

InnoDB