前言

环境

  • openjdk 14

    正文

    存在意义

    既然已经有了内部锁,为什么还需要 Lock 呢?这得从内部锁的缺点讲起。

  • 只支持非公平锁,可能会造成线程饥饿

  • 不可中断获取锁的过程,可能会造成死锁
  • 只有一个“条件”

这是至今仍存在的问题,粒度大,只适合单一场景的问题已经随着内部锁的优化逐渐解决。Lock 解决了以上问题,Lock总体如下:

  • 支持公平锁与非公平锁
  • 支持尝试获取锁
  • 支持取消获取共享资源
  • 支持多个“条件”
  • 支持共享锁和互斥锁
  • 支持重入

    基础模块

    依照管程模型,由 java 实现。具体由 AbstractQueuedSynchronizer 类来实现
    管程模型图.png

可以从以下三个部分来看实现:互斥、线程等待队列、条件变量及条件队列

共享资源状态的实现方式

分析

先思考一下,如何实现互斥?一个线程可以持有多个共享资源,而一个共享资源只可以被一个线程持有。线程与共享资源的关系是一对多。所以我觉得可以这样实现,用一个类来封装共享资源和线程ID,在线程获取共享资源时,先查看该资源是否已经有关联的线程,如果有,则进入等待队列,如果没有,则尝试获取,获取后,将线程 ID 填入。这个资源并不一定是可以指代的实体,比如说代码块,因此需要再将思路优化下,保证互斥就需要在进入共享资源前先进行判断是否有线程持有,因此,可以在共享资源前添加一个方法,让线程执行到共享资源前,先进入这个方法进行判断,可以使用一个变量来判断当前有线程持有该资源,并且判断是哪一个线程(线程切换后,重新来到这里需要判断下是否是自己持有该资源)。Lock 就是这样,用 state 来判断是否有线程持有,用 exclusiveOwnerThread 来记录持有线程
image.png
当没有线程持有共享资源时,state 就是 0,如果有线程持有,那么 state > 0,同时设置线程持有者
image.png

总结

  • Lock 使用 AQS 中的 state 来表示资源是否被线程持有
  • Lock 使用 AQS 中的 exclusiveOwnerThread 来记录持有该资源的线程

    等待队列的实现方式

    就如上面管程模型描述的那样,当共享资源被其他线程持有,线程进入等待队列中,等待持有共享资源的线程释放共享资源。
    先看下它的节点
    image.png
    prev 和 next ,看起来还是个双向队列呀,waiter就是队列存储的数据咯。status,从注释看,不知道是做什么的;想从方法着手,看看 status 在方法中起到的作用,结果一层一层,跳到最后发现是由 JVM 实现的,无奈,去上网查了查相关的博客,发现 openjdk 14的实现和 openjdk 8的实现还不一样,具体从哪一版本开始变更的,这也不重要,来看一下区别,推断下 status 到底是做什么的

    OpenJDK 8 的实现方式

    严重怀疑,我是不是下了假源码……openjdk 8 的注释好详细啊。回归正题,openjdk 8 将条件,独占,共享等队列的节点用同一个节点进行表示
    先看下源码(这里我把源码中的注释删掉了,太多了,感兴趣的可以去看一下) ```java static final class Node {

    1. /** Marker to indicate a node is waiting in shared mode */
    2. static final Node SHARED = new Node();
    3. /** Marker to indicate a node is waiting in exclusive mode */
    4. static final Node EXCLUSIVE = null;
    5. /** waitStatus value to indicate thread has cancelled */
    6. static final int CANCELLED = 1; //取消
    7. /** waitStatus value to indicate successor's thread needs unparking */
    8. static final int SIGNAL = -1;//释放
    9. /** waitStatus value to indicate thread is waiting on condition */
    10. static final int CONDITION = -2;//在条件队列中等待
    11. /**
    12. * waitStatus value to indicate the next acquireShared should
    13. * unconditionally propagate
    14. */
    15. static final int PROPAGATE = -3;
  1. volatile int waitStatus;
  2. volatile Node prev;
  3. volatile Node next;
  4. volatile Thread thread;
  5. Node nextWaiter;
  6. final boolean isShared() {
  7. return nextWaiter == SHARED;
  8. }
  9. final Node predecessor() throws NullPointerException {
  10. Node p = prev;
  11. if (p == null)
  12. throw new NullPointerException();
  13. else
  14. return p;
  15. }
  16. Node() { // Used to establish initial head or SHARED marker
  17. }
  18. Node(Thread thread, Node mode) { // Used by addWaiter
  19. this.nextWaiter = mode;
  20. this.thread = thread;
  21. }
  22. Node(Thread thread, int waitStatus) { // Used by Condition
  23. this.waitStatus = waitStatus;
  24. this.thread = thread;
  25. }
  26. }
  1. <a name="PIvM2"></a>
  2. #### OpenJDK 14 的实现方式
  3. openJDK 14 中的 abstract static class,就是队列的基础结构,其余的 条件,独占,共享等节点,都以此为基础进行扩展<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1308518/1600086044812-160ccd12-0665-4f05-9ef9-0e4849f21bf4.png#align=left&display=inline&height=167&margin=%5Bobject%20Object%5D&name=image.png&originHeight=333&originWidth=1132&size=56919&status=done&style=none&width=566)
  4. ```java
  5. /** CLH Nodes */
  6. abstract static class Node {
  7. volatile Node prev; // initially attached via casTail
  8. volatile Node next; // visibly nonnull when signallable
  9. Thread waiter; // visibly nonnull when enqueued
  10. volatile int status; // written by owner, atomic bit ops by others
  11. // methods for atomic operations
  12. final boolean casPrev(Node c, Node v) { // for cleanQueue
  13. return U.weakCompareAndSetReference(this, PREV, c, v);
  14. }
  15. final boolean casNext(Node c, Node v) { // for cleanQueue
  16. return U.weakCompareAndSetReference(this, NEXT, c, v);
  17. }
  18. final int getAndUnsetStatus(int v) { // for signalling
  19. return U.getAndBitwiseAndInt(this, STATUS, ~v);
  20. }
  21. final void setPrevRelaxed(Node p) { // for off-queue assignment
  22. U.putReference(this, PREV, p);
  23. }
  24. final void setStatusRelaxed(int s) { // for off-queue assignment
  25. U.putInt(this, STATUS, s);
  26. }
  27. final void clearStatus() { // for reducing unneeded signals
  28. U.putIntOpaque(this, STATUS, 0);
  29. }
  30. private static final long STATUS
  31. = U.objectFieldOffset(Node.class, "status");
  32. private static final long NEXT
  33. = U.objectFieldOffset(Node.class, "next");
  34. private static final long PREV
  35. = U.objectFieldOffset(Node.class, "prev");
  36. }

初始化

入队

出队

“等待”

公平锁与非公平锁

其实就是当一个不在等待队列和条件队列中的线程尝试获取共享资源,如果 state 为 0,即该资源暂时没有线程获取时,是否要判断下等待队列及条件队列中是否有线程在等待。公平锁是,如果有线程在等待,则加入等待队列末尾;如果没有线程等待,则获取该资源。而非公平锁是不判断是否有等待线程,直接获取。代码如下(等待队列及条件队列是按管程模型来讲的,在实现中,这是同一个队列,只是节点属性不同)

公平锁源码

image.png

非公平锁源码

可以看到,如果 CAS 获取成功,则直接设为持有线程了,并不判断是否有等待线程
image.png

互斥锁

介绍及使用

实现方式

锁定

释放

共享锁

介绍及使用

可以这我的这篇文章 ReadWriteLock

实现方式

锁定

释放

重入

介绍及使用

我觉得引入重入锁,一是为了减少 “锁的重量”,比如当同一个线程多次访问已持有的被锁保护的共享资源,如果是按“持有锁-操作共享资源-释放锁”这一流程,要想重复使用,得先把共享资源释放,然后再竞争,对于需要重复使用的场景,很明显严重拖累了速度。二是为了避免死锁,当锁定的方法需要调用另一个同一个锁锁定的方法时,如果不支持重入,那么就会陷入死锁。

实现方式

锁定

进入lock 后,调用 sync.lock 方法,sync 是 AQS 子类的实例
image.png

解锁

验证

验证代码

代码如下:

  1. package cn.zjm404.stu.thread.lock.explicition;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. /**
  4. * @author ZJM
  5. */
  6. public class ReentrantLockTest {
  7. private final ReentrantLock lock = new ReentrantLock();
  8. public void method1(){
  9. lock.lock();
  10. try{
  11. //...执行任务
  12. System.out.println("hello world");
  13. }finally {
  14. lock.unlock();
  15. }
  16. }
  17. public void method2(){
  18. lock.lock();
  19. try{
  20. method1();
  21. }finally {
  22. lock.unlock();
  23. }
  24. }
  25. }
  1. package cn.zjm404.stu.thread.lock.explicition;
  2. public class Client {
  3. public static void main(String[] args) {
  4. ReentrantLockTest rt = new ReentrantLockTest();
  5. rt.method2();
  6. }
  7. }

调试

第一次获取锁,先进入 lock
image.png

结尾

回顾

  • Lock 存在意义是什么?与 Synchronized 相比,增加了什么功能?
  • AQS 是什么?
  • Lock 如何实现互斥锁?
  • Lock 如何实现共享锁?
  • Lock 如何实现重入?
  • Lock 如何实现公平锁及非公平锁?

    参考