Java 并发包中锁原理剖析(一) LockSupport 类、抽象同步队列 AQS

一、LockSupport 工具类

     JDK 中的 rt.jar 包中的 LockSupport 类 是个很重要的工具类 ,它的主要作用是挂起 和 唤醒 线程,该工具类是创建 锁 和 其他同步类的基础。LockSupport 是使用 Unsafe 类实现的。
     LockSupport 类与每个使用它的线程都会关联一个许可证。默认情况下,调用 LockSupport 类的方法的线程是不持有这个许可证的。源码中关于许可证的注释:

(Unlike with Semaphores though, permits do not accumulate. There is at most one.)

译:不过与 信号量 不同,许可证 不会累积。最多只能有一个

    

1、void park() 方法

源码:

 public static void park() {
        UNSAFE.park(false, 0L);
    }

(Unsafe 类里都是 native 方法,点不进去了。)
     如果调用 park() 方法的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.park() 时会马上返回,否则,调用线程会被禁止参与线程的调度,也就是被阻塞挂起。

例👀: 在 main 函数中调用 park 方法,当前线程会被挂起,因为默认情况下调用线程是不持有许可证的。
运行结果:
在这里插入图片描述

     在其他线程调用 unpark(Thread thread)方法并且将当前线程作为参数时,调用 park 方法而被阻塞的线程会返回。另外,如果其他线程调用了阻塞线程的 interrupt() 方法,设置了中断标志 或者 线程被虚假唤醒,则 阻塞线程也返回。所以在调用 park() 方法时 最好也使用循环条件判断方式。
     ✨需要注意的是,因 调用 park() 方法 而被阻塞的线程,被其他线程中断而返回时,并不会抛出 InterruptedException 异常。✨
    

2、void unpark(Thread thread) 方法

源码:

public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

     当一个线程调用 unpark 时,如果参数 thread 线程没有持有 thread 与 LockSupport 类关联的许可证,则让 thread 线程持有,如果 thread 之前因为调用 park() 方法而被挂起,则调用 unpark() 方法后,该线程被唤醒。如果 thread 之前没有调用 park ,则调用 unpark 方法后,再调用 park() 方法,会立刻返回。
例1 👀:修改之前的代码:

public class ParkTest {
    public static void main(String[] args) {

        // 使当前线程获取许可证
        LockSupport.unpark(Thread.currentThread());
        System.out.println("begin park!");
        LockSupport.park();
        System.out.println("end park!");
    }
}

运行结果:
在这里插入图片描述
例2 👀

package LockPack;

import java.util.concurrent.locks.LockSupport;

public class ParkTest2 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("child thread begin park!");

                // 调用park方法,挂起自己
                LockSupport.park();

                System.out.println("child thread unpark!");
            }
        });

        // 启动子线程
        thread.start();

        // 主线程休眠 1s
        Thread.sleep(1000);

        System.out.println("main thread begin unpark!");

        // 调用 unpark 方法让 child 线程持有许可证,然后 park 方法返回
        LockSupport.unpark(thread);
    }
}
 

运行结果:
在这里插入图片描述
    以上代码创建了一个子线程 thread,子线程启动后,调用 park 方法,由于默认情况下子线程没有持有许可证,所以它会把自己挂起。接下来 CPU 给到主线程,主线程休眠 1s,再下来调用 unpark 方法让子线程持有许可证,这样的话,子线程调用的 park 方法就返回了。
    🙌注意:park 方法返回时不会告诉我们是因为何种原因返回,所以调用者需要根据之前调用 park 方法的原因,再次检查条件是否满足,如果不满足则还需要再次调用 park 方法。
例3 👀:根据调用前后中断状态的对比,判断是否因为中断而返回。

import java.util.concurrent.locks.LockSupport;

public class ParkTest3 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("child thread begin park!");

                // 调用 park 方法,挂起自己,只有被中断时才会退出循环
                while (!Thread.currentThread().isInterrupted()){
                    LockSupport.park();
                }

                System.out.println("child thread unpark!");
            }
        });

        // 启动子线程
        thread.start();

        // 主线程休眠 1s
        Thread.sleep(1000);

        System.out.println("main thread begin unpark!");

        // 中断子线程
        thread.interrupt();
    }
}

运行结果:
在这里插入图片描述

3、void parkNanos(long nanos) 方法

源码:

 public static void parkNanos(long nanos) {
        if (nanos > 0)
            UNSAFE.park(false, nanos);
    }

    和 park 方法类似,如果调用 park 方法的线程已经拿到了与 LockSupport 关联的许可证,则 调用 LockSupport.parkNanos(long nanos) 方法后会立马返回。该方法的不同之处在于 如果没有拿到许可证,则调用线程会被挂起 nanos 时间后修改为自动返回。
     ✨另外 park 方法还支持带有 blocker 参数的方法 void park(Object blocker)方法,当线程在没有持有许可证的情况下调用 park 方法而被阻塞挂起时,这个 blocker 对象会被记录到该线程内部。✨

park(Object blocker) 方法源码:

   public static void park(Object blocker) {
   		// 获取调用线程
        Thread t = Thread.currentThread();

		// 设置该线程的 blocker 变量
        setBlocker(t, blocker);

		// 挂起线程
        UNSAFE.park(false, 0L);

		// 线程被激活后清除 blocker 变量,因为一般是在线程阻塞时才分析原因
        setBlocker(t, null);
    }

     Thread 类里有个变量 parkBlocker,用来存放 park 方法传递的 blocker 对象 :

volatile Object parkBlocker;

     使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用 getBlocker(Thread) 的方法来获取 blocker 对象的,所以 JDK 推荐我们使用带有 blocker 参数的 park 方法,并且把 blocker 方法设置为 this,这样当在打印线程堆栈排查问题时就能知道是哪个类被阻塞了。
例如以下代码:

package LockPack;

import java.util.concurrent.locks.LockSupport;

public class ParkTest4 {
    public void testPark(){
        LockSupport.park();
    }
    public static void main(String[] args) {
        ParkTest4 parkTest4=new ParkTest4();
        parkTest4.testPark();
    }
}
jstack pid 命令的使用

     在 dos 模式下,先进入 jdk 安装目录,输入 jps 获取当前进程信息,可以看到,这个类的进程 id 是 17060,然后在输入 jstack 17060,就可以看到进程下每个线程的状态。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
修改代码为:

 LockSupport.park(this);

重复上述过程,观察线程被阻塞原因,可以看到:

在这里插入图片描述
     ✨ 使用带 blocker 参数的 park 方法,线程堆栈可以提供更多有关线程阻塞对象的信息。✨
    

4、void parkUntil(Object blocker,long deadline) 方法

源码:

  public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }

     参数 deadline 的单位为 ns,该时间是从 1970 年到现在某一个时间点的毫秒值。这个方法和 parkNanos(Object blocker,long nanos) 方法的区别是,后者是从当前算 等待 nanos 时间,而前者是指定一个时间点,需要计算那个时间点转换为 从 1970 年到这个时间点的总毫秒数。
    

二、抽象同步队列 AQS 概述

1、AQS——锁的底层支持

     AbstractQueuedSynchronizer 抽象同步队列,简称 AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用 AQS 实现的。

(1) AQS 类图

在这里插入图片描述
     AQS 是一个 FIFO 的 双向队列 ,其内部通过节点 head 和 tail 记录队首 和 队尾元素,队列元素的类型是 Node,其中 Node 中的 thread 变量用来存放进入 AQS 队列里面的线程:Node 节点内部的 SHARED 用来标记 该线程是 获取共享资源时被阻塞挂起后放入 AQS 队列的,EXCLUSIVED 用来标记 线程是获取独占资源时 被挂起后放入 AQS 队列的;waitStatus 记录当前线程等待状态,可以为 CANCELLLED(线程被取消了)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里等待)、PROPAGATE(释放共享资源时需要通知其他节点);prev 记录当前节点的前驱节点,next 记录当前节点的后继节点。
     在 AQS 中维持了一个 单一的 状态信息 state

/**
     * The synchronization state.
     */
    private volatile int state;

     可以通过 getState、setState、compareAndSetState 方法修改 state 的值。
     对于读写锁 ReentReadWriteLock 来说,state 的高 16 位表示 读状态,即获取 锁 的次数,低 16 位表示锁的线程的可重入次数。
     对于 semaphore 来说,state 表示当前可用信号的个数。
     对于 CountDownlatch 来说, state 表示计数器当前的值。

(2)条件变量 ConditionObject

     AQS 有个内部类 ConditionObject,用来结合锁 实现线程同步。ConditionObject 可以直接访问 AQS 对象内部的变量,比如 state 状态值和 AQS 队列。ConditionObject 是条件变量,每个条件变量对应一个条件队列(单向链表队列) ,(🙌 注意区分 AQS 队列 和 条件队列) 用来存放调用条件变量的 await 方法后 被阻塞的线程。这个条件队列的头、尾元素分别是 firstWaiter 和 lastWaiter。

     ✨对于 AQS 来说,线程同步的关键是对状态值 state 进行操作。✨
     根据 state 是否属于一个线程,操作 state 的方式分为 独占方式 和 共享方式。
     使用独占方式获取的资源是与具体线程绑定的,就是说 如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作 state 就会发现当前该资源不是自己持有的,就会在获取失败后被阻塞。比如,独占锁 ReentrantLock 的实现,当一个线程获取了 ReentrantLock 的锁后,在 AQS 内部会首先使用 CAS 操作把 state 的值从 0 变为1 ,然后设置当前锁的持有者为当前线程,当该线程再一次获取锁时发现它就是锁的持有者,则会把状态值从 1 变为 2,也就是设置可重入数,而当另一个线程获取锁时,发现自己并不是该锁的持有者就会被放入 AQS 阻塞队列后挂起。
     对应共享方式的资源 与 具体线程是不相关的,当多个线程去请求资源时通过 CAS 方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再去获取时如果当前资源还能满足它的需要,则当前线程只需要使用 CAS 方式进行获取许可。比如 Semaphore 信号量,当一个线程通过 acquire() 方法获取信号量时,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋 CAS 获取信号量。

👉独占方式👈
    在独占方式下,获取和释放资源 的流程如下:

  • 当一个线程调用 acquire(int arg) 方法 获取独占资源时,会首先使用 tryAcquire 方法尝试获取资源,具体是 设置状态变量 state 的值 ,成功 则直接返回,失败则将当前线程封装为 Node.EXCLUSIVE 的 Node 节点后 插入到 AQS 阻塞队列的尾部,并调用 LockSupport.park(this) 方法挂起自己。

源码:

 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//(1)
            selfInterrupt();

		 // 成功则直接返回
    }

(1):

 static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }
  • 当一个线程调用 release(int arg) 方法时,会尝试使用 tryRelease 释放资源,具体是设置状态变量 state 的值,然后调用 LockSupport.unpark(thread) 方法激活 AQS 队列里 被阻塞的一个线程。被激活的线程则使用 tryAcquire 尝试,看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入 AQS 队列并被挂起。

源码:

 public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

👉共享方式👈
    在共享方式下,获取和释放资源的流程如下:

  • 当一个线程调用 acquire(int arg) 方法获取共享资源时,会首先使用 tryAcquireShared 方法尝试获取资源,具体是设置状态变量 state 的值,成功 则直接返回,失败则将当前线程封装为 Node.SHARED 的 Node 节点后 插入到 AQS 阻塞队列的尾部,并调用 LockSupport.park(this) 方法挂起自己。

源码:

 public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
  • 当一个线程调用 releaseShared(int arg) 方法时,会尝试使用 tryReleaseShared 释放资源,具体是设置状态变量 state 的值,然后调用 LockSupport.unpark(thread) 方法激活 AQS 队列里 被阻塞的一个线程。被激活的线程则使用 tryAcquire 尝试,看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入 AQS 队列并被挂起。

源码:

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

    需要注意的是,AQS 类并没有提供可用的 tryAcquire 、tryAcquireShared 和 tryRelease、tryReleaseShared 方法,正如 AQS 是锁 阻塞 和 同步器的基础框架一样,这些方法需要由具体子类来实现,子类在实现这些方法时要根据具体场景使用 CAS 算法尝试修改 state 状态值 ,成功则返回 true,否则返回 false。子类还需要定义调用这些方法时 state 状态值的增减代表什么含义。
👻比如:

  • 继承自 AQS 实现的独占锁 ReentrantLock ,定义 当 status 为 0 时,表示 锁空闲,为 1 时表示锁已经被占用。在重写 tryAcquire 时,在内部需要使用 CAS 算法查看当前 state 是否为 0,如果为 0 则使用 CAS 设置为 1,并设置当前锁的持有者为当前线程,而后返回 true,如果 CAS 失败则返回 false。
  • 继承自 AQS 实现的独占锁在实现 tryRelease 时,在内部需要使用 CAS 算法把当前 state 值从 1 修改为 0,并设置当前锁的持有者为 null,然后 返回 true,如果 CAS 失败,则返回 false。
  • 继承自 AQS 实现的读写锁 ReentrantReadWriteLock 里的读锁在重写 tryAcquireShared 时,首先看 写锁 是否被其他线程持有,如果是 则直接返回 false,否则使用 CAS 递增 state 的高 16 位(在 ReentrantReadWriteLock 中,state 的高 16 位为获取读锁的次数。)
  • 继承自 AQS 实现的 读写锁 ReentrantReadWriteLock 里的读锁在重写 tryReleaseShared 时,在内部需要使用 CAS 算法 把当前 state 的值的高 16 位减1 ,然后返回 true,如果 CAS 失败 则返回 false。
        注意到还有 :
public final void acquireInterruptibly(int arg)
public final void acquireSharedInterruptibly(int arg)

    不带 Interruptibly 关键字 的意思是 不对中断进行响应,也就是 线程在调用不带 Interruptibly 关键字的方法 获取资源时 或者 获取资源失败被挂起时,其他线程中断了该线程,该线程不会因为被中断而抛出异常,它还是继续获取资源 或者 被挂起。而带 Interruptibly 关键字的方法,是要抛出 InterruptedException 异常而返回的。

    来看看如何维护 AQS 提供的队列:
👉入队 enq(final Node node) 👈
    当一个线程获取锁失败后,该线程会被转换为 Node 节点,然后就会使用 enq(final Node node) 方法将该节点插入到 AQS 的阻塞队列。
源码:

  private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;

				//(1)
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

(1):

 private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);

    第一次循环时,队列 头 尾 都指向 null,然后节点 t 也会指向 head,接下来通过 CAS 算法设置一个哨兵节点为头节点,如果 CAS 成功,则让尾部节点也指向哨兵节点,如图所示:

在这里插入图片描述

    这样只是插入了一个哨兵节点,还需要插入 node 节点,所以第二次循环中,t 先指向尾节点,接下来设置前驱节点为尾节点,再下来,t 引用存放的是 tail 的值,通过 CAS 操作把 node 的值存在 tail 引用中,即 tail 指向 node;CAS 成功后,再设置哨兵节点的后驱节点为 node,这样就完成 双向链表的插入 啦:

在这里插入图片描述
    

2、AQS——条件变量的支持

    正如 notify 和 wait,是配合synchronized 内置实现线程间同步 ,条件变量的 signal 和 await 方法也是用来配合 使用 AQS 实现的锁,实现线程间同步的。
    它们的不同在于 synchronized 同时只能与一个共享变量的 notify 或 wait 方法实现同步,而 AQS 的一个锁可以对应多个条件变量
    调用共享变量的 notify 和 wait 方法前必须先获取该共享变量的内置锁,同理,在调用条件变量的 signal 和 await 方法前也必须先获取条件变量对应的锁。否则会抛出 java.lang.IllegalMonitorStateException 异常。

例 👀

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionObjectTest {

    public static void main(String[] args) {
        ReentrantLock lock=new ReentrantLock();

		// // new 一个在 AQS 内部声明的 ConditionObject 对象
        Condition condition=lock.newCondition();
        
        lock.lock();
        try{
            System.out.println("begin wait!");

            // 阻塞挂起当前线程,
            // 当其他线程调用条件变量的 signal 方法时
            // 被阻塞的线程才会从 await 处返回
            condition.await();
            System.out.println("end wait!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            lock.unlock();
        }
        lock.lock();
        try{
            System.out.println("begin signal!");
            condition.signal();
            System.out.println("end signal!");
        } catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

运行结果:

在这里插入图片描述
    ✨ 这里的 Lock 对象等价于 synchronized 加上共享变量,调用 lock.lock() 方法就相当于进入了 synchronized 块,调用 lock.unlock() 方法就相当于退出 synchronized 块。调用条件变量的 await() 方法就相当于调用共享变量的 wait() 方法,调用条件变量的 signal() 方法就相当于调用共享变量的 notify() 方法。调用条件变量的 signalAll() 方法就相当于调用共享变量的 notifyAll() 方法。✨

(1)await() 方法

源码:

      public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();

			// 创建新的 node 节点,并尾插
            Node node = addConditionWaiter();

			// 释放当前线程的锁
            int savedState = fullyRelease(node);
            
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {

				// 阻塞当前线程
                LockSupport.park(this);
                
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

    当线程调用变量的 await() 方法时,在内部会构造一个类型为 Node.CONDITION 的 node 节点,然后将该节点插入到条件队列的末,之后当前线程会释放获取的锁,也就是 会操作锁对应的 state 变量的值。并被阻塞挂起。(确实像极了 Object 的 waIt)这时,如果有其他线程调用 lock.lock() 尝试获取锁,就会有一个线程获取到锁,其他线程会被转换为 Node 节点插入到 lock 锁对应的 AQS 阻塞队列里,并做自旋 CAS 尝试获取锁。
    

(2)signal() 方法

源码:

/**
         * Moves the longest-waiting thread, if one exists, from the
         * wait queue for this condition to the wait queue for the
         * owning lock.
         *
         * @throws IllegalMonitorStateException if {@link #isHeldExclusively}
         *         returns {@code false}
         */
         
          public final void signal() {
              if (!isHeldExclusively())
                  throw new IllegalMonitorStateException();
              Node first = firstWaiter;
              if (first != null)

				// 将条件队列头元素移动到 AQS 队列
                 doSignal(first);
        }

    注释译为:将等待时间最长的线程(如果存在)从该条件的等待队列 移至 拥有锁的等待队列。 注释里说的 ”等待队列“ 其实就是上面说的 AQS 维护的队列。


源码中的注释:

 /**
     * Wait queue node class.
     *
     * <p>The wait queue is a variant of a "CLH" (Craig, Landin, and
     * Hagersten) lock queue. CLH locks are normally used for
     * spinlocks.  We instead use them for blocking synchronizers, but
     * use the same basic tactic of holding some of the control
     * information about a thread in the predecessor of its node.  A
     * "status" field in each node keeps track of whether a thread
     * should block.  A node is signalled when its predecessor
     * releases.  Each node of the queue otherwise serves as a
     * specific-notification-style monitor holding a single waiting
     * thread. The status field does NOT control whether threads are
     * granted locks etc though.  A thread may try to acquire if it is
     * first in the queue. But being first does not guarantee success;
     * it only gives the right to contend.  So the currently released
     * contender thread may need to rewait.
     *
     * <p>To enqueue into a CLH lock, you atomically splice it in as new
     * tail. To dequeue, you just set the head field.
     * <pre>
     *      +------+  prev +-----+       +-----+
     * head |      | <---- |     | <---- |     |  tail
     *      +------+       +-----+       +-----+
     * </pre>
     *
     * <p>Insertion into a CLH queue requires only a single atomic
     * operation on "tail", so there is a simple atomic point of
     * demarcation from unqueued to queued. Similarly, dequeuing
     * involves only updating the "head". However, it takes a bit
     * more work for nodes to determine who their successors are,
     * in part to deal with possible cancellation due to timeouts
     * and interrupts.
     *
     * <p>The "prev" links (not used in original CLH locks), are mainly
     * needed to handle cancellation. If a node is cancelled, its
     * successor is (normally) relinked to a non-cancelled
     * predecessor. For explanation of similar mechanics in the case
     * of spin locks, see the papers by Scott and Scherer at
     * http://www.cs.rochester.edu/u/scott/synchronization/
     *
     * <p>We also use "next" links to implement blocking mechanics.
     * The thread id for each node is kept in its own node, so a
     * predecessor signals the next node to wake up by traversing
     * next link to determine which thread it is.  Determination of
     * successor must avoid races with newly queued nodes to set
     * the "next" fields of their predecessors.  This is solved
     * when necessary by checking backwards from the atomically
     * updated "tail" when a node's successor appears to be null.
     * (Or, said differently, the next-links are an optimization
     * so that we don't usually need a backward scan.)
     *
     * <p>Cancellation introduces some conservatism to the basic
     * algorithms.  Since we must poll for cancellation of other
     * nodes, we can miss noticing whether a cancelled node is
     * ahead or behind us. This is dealt with by always unparking
     * successors upon cancellation, allowing them to stabilize on
     * a new predecessor, unless we can identify an uncancelled
     * predecessor who will carry this responsibility.
     *
     * <p>CLH queues need a dummy header node to get started. But
     * we don't create them on construction, because it would be wasted
     * effort if there is never contention. Instead, the node
     * is constructed and head and tail pointers are set upon first
     * contention.
     *
     * <p>Threads waiting on Conditions use the same nodes, but
     * use an additional link. Conditions only need to link nodes
     * in simple (non-concurrent) linked queues because they are
     * only accessed when exclusively held.  Upon await, a node is
     * inserted into a condition queue.  Upon signal, the node is
     * transferred to the main queue.  A special value of status
     * field is used to mark which queue a node is on.
     *
     * <p>Thanks go to Dave Dice, Mark Moir, Victor Luchangco, Bill
     * Scherer and Michael Scott, along with members of JSR-166
     * expert group, for helpful ideas, discussions, and critiques
     * on the design of this class.
     */
    static final class Node {

     Node 类是等待队列节点类 ,等待队列是“ CLH”(Craig,Landin 和* Hagersten)锁队列的变体。 CLH 锁通常用于 自旋锁 ,我们还用它们代替 阻塞同步器 ,但是使用相同的基本策略,即在其节点的前身中包含有关线程的某些控制信息。每个节点中的 “status 状态” 字段跟踪线程,表示线程是否应阻塞。 当节点 node 的前任predecessor 释放时,会被唤醒。否则,队列的每个节点都充当*特定通知样式的监视器,每个节点都持有一个等待线程。虽然状态字段不控制线程是否被授予锁等。如果节点在队列头部 first,线程可能会尝试获取资源 acquire。但是头节点并不能保证成功它仅授予竞争权。当前释放了资源的竞争者线程可能需要重新等待。
     <p>要加入CLH锁,您可以自动将其作为新的尾部拼接。要出队,您只需设置头字段。
     <p>插入到 CLH 队列中,只需要对 尾节点 tail 执行一个原子性操作,因此存在一个简单的
分界的原子点,即从未排队到排队。同样,出队仅涉及更新头节点 head 。但是,节点需要花费更多的精力来确定其后继者是谁,部分原因是由于超时和中断而可能导致的取消。
    <p>主要需要 前驱 prev(在原始CLH锁中不使用)来处理取消。如果取消某个节点,则其
后继节点(通常)会重新链接到一个未取消的
前任节点。
    <p>我们也使用 后继 next 以实现阻塞机制。 每个节点的线程 ID 保留在其自己的节点中,因此前任 predecessor 通过遍历 下一个链接 next 确定它是哪个线程,从而唤醒下一个节点的。确定后继者必须避免与新排队的节点竞争以设置其前任的“下一个”字段。 必要时,当一个节点的后继 successor 看起来可能会是 null 时,自动更新 尾部 tail 从而 向后检查,来解决此问题。 (或者换句话说,next-links是一种优化,因此我们通常不需要向后扫描。)
    <p> 由于我们必须让取消状态的节点出队 poll,因此我们可能会遗漏一个 在前面 或者 后面 被取消的节点。要解决此问题,必须始终 unpark 取消节点的后继者 successor,让它们拥有稳定的前任。
     <p> CLH 队列需要一个虚拟标头节点才能开始。但是我们不会在构造方法中创建它们,因为如果从不发生冲突,那将会浪费时间精力。而是,在第一次出现冲突时 构造节点,设置头和尾指针。
     <p>等待条件的线程使用相同的节点,但是使用附加的链接。条件只需要在简单(非并行)链接队列中链接节点,因为仅当它们被独占时才可以访问它们。等待时,将节点插入条件队列。收到信号后,该节点被转移到主队列。 status 字段的特殊值用于标记节点所在的队列。


    
    当另外一个线程调用条件变量的 signal 方法时,在内部会把条件队列里队头的一个线程节点从条件队列里移除,并放入 AQS 的阻塞队列里,然后激活这个线程。
    

    需要注意的是,AQS 只提供了 ConditionObject 的实现,并没有提供 newCondition 方法,该方法用来 new 一个 ConditionObject 对象,需要 AQS 的子类来提供 newCondition 方法
    
🎭 总结
    一个锁对应一个 AQS 阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。
在这里插入图片描述

3、基于 AQS 实现自定义同步器

    来基于 AQS 实现一个不可重入的独占锁,自定义 AQS 需要重写一系列函数,还需要定义原子变量 state 的含义。我们定义:

  • state 为 0 表示锁没有被线程持有
  • state 为 1 表示锁已经被某一个线程持有。
        由于是不可重入锁,所以不需要记录持有锁的线程获取锁的次数,而且,自定义的锁支持条件变量。
(1)代码实现
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 基于 AQS 实现一个不可重入的独占锁,
 * 自定义 AQS 需要重写一系列函数,还需要定义原子变量 state 的含义。我们定义:
 *  state  为 0 表示锁没有被线程持有
 *  state  为 1 表示锁已经被某一个线程持有。
 * 由于是不可重入锁,所以不需要记录持有锁的线程获取锁的次数,
 * 而且,自定义的锁支持条件变量。
 */

public class NonReentrantLock implements Lock,Serializable {

    // 内部帮助类
    private static class Sync extends AbstractQueuedSynchronizer{
        @Override
        // 锁是否已经被持有
        protected boolean isHeldExclusively(){
            return getState()==1;
        }


        // 如果 state 为0,则尝试获取锁
        @Override
        public boolean tryAcquire(int acquires){

            assert  acquires == 1;

            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 尝试释放锁
        @Override
        protected boolean tryRelease(int releases) {

            assert releases == 1;
            if (getState() == 0)
                throw new IllegalMonitorStateException();

                setExclusiveOwnerThread(null);

                // 设置状态值 为 0
                setState(0);
                return true;
        }
        
        // 提供条件变量接口
        Condition newCondition(){
            return new ConditionObject();
        }
    }

    // 创建 Sync 对象
    private final Sync sync=new Sync();

    @Override
    public void lock() {
        // 尝试获取资源,也就是设置 state 的值
        sync.acquire(1);
    }
    
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked(){
        return sync.isHeldExclusively();
    }
    
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1,unit.toNanos(timeout));
    }
    
}
(2)使用自定义锁实现生产-消费模型

👇 生产 - 消费模型:
    通过容器解决生产者和消费者的强耦合问题,生产者 和 消费者 之间不直接通讯,而通过 阻塞队列来进行通信。
代码:

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Condition;

public class ProducerAndConsumer {
    final static NonReentrantLock lock=new NonReentrantLock();
    final static Condition notFull=lock.newCondition();
    final static Condition notEmpty=lock.newCondition();

    final static Queue<String> queue=new LinkedBlockingQueue<>();
    final static int queueSize=10;

    public static void main(String[] args) {
        // 生产者
        Thread producer=new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取独占锁
                lock.lock();
                try{
                    // 如果队列满了,则等待
                    // while 循环避免虚假唤醒
                    while (queue.size() == queueSize){
                        notEmpty.await();
                    }

                    // 添加元素到队列
                    queue.add("ele");

                    // 唤醒消费线程
                    notFull.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    // 释放锁
                    lock.unlock();
                }
            }
        });

        // 消费者
        Thread consumer=new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取独占锁
                lock.lock();
                try{
                    // 队列空,则等待
                    while (0 == queue.size()){
                        notFull.await();

                        // 消费一个元素
                        String ele=queue.poll();
                        // 唤醒生产线程
                        notEmpty.signalAll();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    // 释放锁
                    lock.unlock();
                }
            }
        });

        // 启动线程
        producer.start();
        consumer.start();

    }
}

    在 main 方法里,首先创建了 producer 生产者线程,在线程内部先调用 lock.lock(); 获取独占锁,然后判断当前队列是否已满,如果满了就调用 notEmpty.await(); 阻塞挂起当前线程,如果队列不满,则直接向队列里添加元素,然后调用 notFull.signalAll() 唤醒所有因为无消费元素而被阻塞的线程,最后释放获取的锁。
    然后,创建了 consumer 消费线程,在线程内部先调用 lock.lock(); 获取独占锁 ,然后判断当前队列里面是不是有元素,如果队列为空,则调用 notFull.await() 阻塞挂起当前线程,如果队列不为空,就直接从队列里面获取并移除元素,然后唤醒因为队列满而被阻塞的线程,最后释放获取的锁。
(说白了就是:有两个条件变量,两个队列,一个队列 notFull 管”空“,空的时候,消费者线程封装成节点,到这个队列排队,而生产者正常生产完,就不空了,所以要从这个队列里出队一个,进行唤醒,进而消费;另一个队列 notEmpty 管”满“,满的时候,生产者线程封装成节点,到这个队列排队,而消费者正常消费完,就不满了,所以要从这个队列里出队一个,进行唤醒,进而生产。)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值