ReentrantLock与AQS
参考:
《对线面试官》
公平锁和非公平锁
公平锁:在竞争环境下,先到的线程一定比后到的线程更快获取到锁
非公平锁:先到的线程未必能够先获取锁
怎么实现
可以使用先进先出队列
公平锁:竞争线程先入队,持有锁的线程释放锁后,唤醒队列的下一个线程去获取锁 (先排队)
非公平锁:竞争线程先尝试获取锁,获取到就直接执行同步代码块,获取不到就放入队列进行等待 (先尝试获取锁)
区别:是否先尝试获取锁
为什么不自旋而用队列
自旋需要耗费资源,多个线程自旋而且大多数是竞争失败,做无用功
AQS
AbstractQueuedSynchronizer,一个双向队列 ,存Node节点,双向链表实现
内部实现是一个先进先出队列+state状态变量 (加锁成功state为1,重入+1,解锁为0)
会把需要等待的线程一Node的形式放到队列上
支持共享锁和排它锁
设计出来是为其他组件提供基础而不是直接使用的,方法不定义为抽象的是避免继承后必须重写,重写哪个方法应该交给具体的实现类,ReentrantLock是独占锁,就不需要实现共享锁部分的方法
head:指向对头
tail:指向队尾
state:锁状态,0锁自由,1锁被占用
Node:
- thread
- 链表
Thread存储要排队的线程信息
wateStatue:用于记录节点状态
- int SIGNAL = -1//后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行;
- int CONDITION = -2//当前节点进入等待队列中
- int PROPAGATE = -3//表示下一次共享式同步状态获取将会无条件传播下去
- int INITIAL = 0;//初始状态
队列对头Node的thread永远是null
=》第二个才是排队的,第一个叫正在处理中
=》持有锁的线程不在队列中,需要虚拟一个头部(Node对象,thread=null)
持有锁的线程不在队列当中,不参与排队
ws =-1表示线程在睡眠
解锁后的头部,是手动把thread设置为null的,旧的头部没有引用指向,方便GC
支持两种模式;独占(锁只会被⼀个线程独占)和共享(多个线程可同时执⾏)
RreetrentLock
一部分是在java级别解决 (交替执行时,通过改变锁的状态来实现,AQS中的state,此时不会初始化队列)
还有一部分会使用到os的api(竞争执行时), 放到队列中调用park()方法会用到os函数
默认是 排他锁/独占锁
支持公平锁与非公平锁,默认是非公平锁 非公平锁效率更高
/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */ public ReentrantLock() { sync = new NonfairSync(); }
通过控制boolean参数可以调整
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
就两个私有属性,主要属性Sync,可以看到是一个AQS队列
加锁:
非公平锁
代码解析
/** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { // 先通过cas方式尝试获取锁资源 调用unsave工具包 预期AQS的state值0,尝试更新为1 (默认是0) if (compareAndSetState(0, 1)) // 获取锁资源成功 设置排他独占 将当前线程设置到AQS的exclusiveOwnerThread(AOS中),代表当前线程拿着资源 setExclusiveOwnerThread(Thread.currentThread()); else // 获取不到,调用尝试获取锁 acquire(1); } // 公平锁和非公平锁都会调用acquire public final void acquire(int arg) { // tryAcquire分为两种实现,一种是公平锁,一种是非公平锁 // 公平锁操作:如果state为0,再看是否需要排队。如果是重入锁,直接获取锁 // 非公平锁:如果state为0,直接尝试CAS修改。如果是锁重入,直接获取锁 // 没有拿到锁,就要排队了 addWaiter将当前线程封装为Node对象放到AQS排队 if (!tryAcquire(arg) && // 查看当前线程是否是排在队伍前面的,如果是就尝试获取锁资源,如果两次自旋后还是没有拿到,需要将当前线程挂起 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } // 非公平实现 final boolean nonfairTryAcquire(int acquires) { // 拿到当前线程 final Thread current = Thread.currentThread(); // 拿到AQS的state int c = getState(); // state 锁自由 if (c == 0) { // 基于CAS尝试修改state从0~1,如果成功 表示拿到锁 if (compareAndSetState(0, acquires)) { // 将exclusiveOwnerThread属性设置为当前线程 setExclusiveOwnerThread(current); // 返回true 表示已经成功拿到锁资源 return true; } } // state不为0 锁被占用 // 判断占用锁的线程是否是当前线程 else if (current == getExclusiveOwnerThread()) { // 锁重入 对state+1 int nextc = c + acquires; // 判断锁重入是否已经达到最大值(加成负数了 超过了锁重入的最大限制) if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); // AQS state+1 setState(nextc); // 返回true 表示已经成功拿到锁资源 return true; } // 尝试拿到锁失败 就要去排队了 state非0,也不是被当前线程占用,无法重入 return false; }
以默认的非公平锁为例
- 判断锁的状态
- 如果有人持有,入队(enq()),根据线程实例化一个Node(待入队的Node,就叫做t2吧),再实例化一个thread为null的空Node并将其设置为AQS的头和尾完成初始化队列(队列有人了),t2入队(更改前后指针维护链表)
- 判断上一个节点是不是空Node,即自己是不是第一个排队的,是的话自旋tryAcquire尝试获取锁(万一确实轮到自己了呢,且除了判断锁是否是自由状态,还会判断是否是第一个排队的人,不是则返回false,是才会让拿到锁)
- 没有拿到(前面的还没搞完),shouldParkAfterFailed()设置头Node的ws=-1(初始为0,一次改为-1,再来就失败退出。多自旋一次是为了尽量不park),再次自旋尝试获取锁,
- 还没拿到(头Node的ws已经=-1),shouldParkAfterFailed()方法中确认需要park,park当前线程(阻塞)
- 没有拿到(前面的还没搞完),shouldParkAfterFailed()设置头Node的ws=-1(初始为0,一次改为-1,再来就失败退出。多自旋一次是为了尽量不park),再次自旋尝试获取锁,
- 判断上一个节点是不是空Node,即自己是不是第一个排队的,是的话自旋tryAcquire尝试获取锁(万一确实轮到自己了呢,且除了判断锁是否是自由状态,还会判断是否是第一个排队的人,不是则返回false,是才会让拿到锁)
- 如果没有人持有锁,判断自己是否需要排队
- 队列是否初始化
- 没有被初始化
- 被初始化了
- 队列当中的元素>1
- 队列当中的元素等于1
- 队列是否初始化
- 假设t3来了,维护队列,排在t2后面,shouldParkAfterFailed()发现t2的ws=0,将其改为-1,循环发现t2的ws=-1,t3睡眠,t3的ws=0
为什么修改的是前一个节点的ws?
因为代码中,线程自己睡眠了,需要别人来判断它是否是睡眠了,没法自己再判断自己是否是睡眠了
重入:使用lock加锁时,会判断当前线程和持有锁的线程是否相同,如果相同,会把持有锁的计数器+1,表示重入
公平锁
代码解读
// 公平锁---------------------------------------- final void lock() { // 直接调用acquire 不会 先尝试直接CAS获取锁 acquire(1); } public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { // 获取到当前线程 final Thread current = Thread.currentThread(); // 获取state int c = getState(); // 如果锁自由 if (c == 0) { // 先查看有无线程排队 抱着不插队的思想 if (!hasQueuedPredecessors() && // 没有人排队,尝试CAS获取锁 compareAndSetState(0, acquires)) { // 设置exclusiveOnwer为当前线程 setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
区别
公平锁和非公平锁的tryAcquire方法的唯一区别就是,当判断state为0(锁自由)后
- 公平锁会先查看是否有线程正在排队,如果有则进行排队,如果没有,执行CAS尝试获取锁资源
- 非公平锁不管有没有线程排队,直接以CAS的方式尝试获取锁资源,拿不到就去排队
排队
private Node addWaiter(Node mode) { // 将当前线程打的包成Node节点 Node node = new Node(Thread.currentThread(), mode); // 拿到AQS的尾指针p // Try the fast path of enq; backup to full enq on failure Node pred = tail; // aqs同步队列中已经有了节点 if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 同步队列为空的时候 enq(node); return node; } /** * Inserts node into queue, initializing if necessary. See picture above. * @param node the node to insert 当前进入的线程打包的节点 * @return node's predecessor */ private Node enq(final Node node) { // 不断通过自旋的方式将线程节点加入到同步队列当中 for (;;) { Node t = tail; // 尾指针指向为空(队列为空) if (t == null) { // Must initialize // cas方式设置为头结点 new个空Node if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; // cas方式设置为尾节点 if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
获取队列
作者: deity-night
出处: https://www.cnblogs.com/deity-night/
关于作者:码农
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接 如有问题, 可邮件(***@163.com)咨询.