Java并发之AQS源码分析ReentranLock、ReentrantReadWriteLock、Condition

基本概念说明

  1. AQS:全称AbstractQueuedSynchronizer,是jdk提供的一个并发框架,位于java.util.concurrent包下,该框架已实现独享锁、共享锁的大部分逻辑,开发者只需要少量编码即可实现相应的独享锁和共享锁,同时可基于该框架实现公平锁和非公平锁。jdk已经提供了读写锁和共享锁的具体实现,分别是同样位于Java.util.concurrent包下的ReentrantLock和ReentrantReadWriteLock。
  2. 独享锁:在任意时刻只允许一个线程获取该锁,在获取锁的线程还未释放锁的情况下,其它尝试获取锁的线程将被阻塞直到锁被释放后再次重新尝试获取锁。
  3. 共享锁:允许一定数量的线程获取锁,超过该数量后的线程尝试获取锁时将被阻塞直到有持有锁的的线程释放锁。
  4. 公平锁和非公平锁:二者属于独享锁或共享锁的一种获取锁的策略,公平锁即表示根据尝试获取锁的顺序先后依次获取锁,非公平锁则存在后来的线程可能比先排队的线程先获取锁。
  5. ReentrantLock:jdk独享锁的具体实现,默认构造器创建的是非公平的独享锁,可以通过重载构造器传入一个布尔值true创建一个公平独享锁。
  6. ReentrantReadWriteLock:jdk共享锁的一种实现,称为读写锁,可允许多个线程获取读锁(读锁不更改数据,线程安全)、仅允许一个线程获取写锁(线程不安全),当已经有线程获取读锁时则尝试获取写锁的线程将被阻塞直到所有读锁线程释放锁,同理当有线程获取写锁时尝试获取读锁的其它线程将被阻塞直到写锁释放,获取写锁的线程可以再次获取读锁。

锁的基本原理思考

假设没有synchronized和AQS框架及其实现,如何实现一个简单的线程安全的锁?我们知道锁的本质实际上是一套让线程有序访问临界资源的代码。因此该代码应该具备以下功能:

  1. 多个线程执行同一操作只能有一个成功,成功的线程意味着获取到锁。
  2. 对《1》操作失败的线程将被阻塞。
  3. 应该将所有被阻塞的线程保存起来等待获取锁成功的线程释放锁时通知这些被阻塞的线程继续执行。
  4. 被阻塞的线程被通知唤醒后需要重新执行《1》操作。

对上述4点的功能的具体实现如下:

  1. 定义一个整形初始值为0的变量: int state = 0;多个线程同时通过CAS操作尝试将state修改为1,cas的语义保证多个线程有且只有一个线程能成功。cas属于cpu的指令,当多个线程通知执行cas修改同一内存时,有且只有一个能修改成功,其它将修改失败。
  2. 对《1》执行cas失败(返回false)的线程如何进入阻塞状态?jdk提供了一个LockSupport.park()方法可以让当前线程进入阻塞状态。
  3. 定义Queue队列用于保存被阻塞的队列,这里选择Queue仅是为了能够得到一个顺序队列同时能快速获取队首的元素。
  4. 获取锁的线程在释放锁时通过执行LockSupport.unpark(Thread)来通知唤醒被阻塞的线程回复运行。

测试环境

  • 联想S2笔记本
  • i5处理器、8g内存,固态硬盘

实现方案1

package com.test.sync;

import java.util.Queue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;

public class MyLock {
    private volatile AtomicInteger state;
    private Queue<Thread> waiters;
    private Thread lockOwnerThread;

    public MyLock(){
        //简便起见使用AtomicInteger的CAS操作
        this.state = new AtomicInteger(0);
        //实际上LinkedBlockingQueue内部是基于ReentrantLock实现的,但为了简化起见直接使用该队列
        waiters = new LinkedBlockingQueue<>();
    }

    public void lock(){
        Thread currentThread = Thread.currentThread();
        //并发执行cas,只有一个能成功并不进入if代码块标识获取锁成功,失败的线程将进入if代码块
        if(!state.compareAndSet(0,1)){
            //被park()的线程可以被interrupt()操作唤醒,该变量保存interrupted标识,阻塞期间的不响应中断,获取锁后根据该标志重新设置interrupt
            boolean interrupted = false;
            this.waiters.add(currentThread);//入队操作,保存cas失败的线程,这里waiters本身线程安全保证并发
            //自旋操作,直到CAS操作成功(即获取到锁位置,否则循环阻塞)
            for(;;){
                //再次CAS操作防止获取锁的线程在上述入队操作之前已经释放锁,如果直接阻塞将导致死锁(如果再waiters.add之前另一线程就已经释放锁则这里不进行获取锁操作可能导致死锁)
                if(currentThread != waiters.peek() || !state.compareAndSet(0,1)) {
                    LockSupport.park(this);
                    //阻塞期间暂存中断标识
                    if(currentThread.isInterrupted()){
                        interrupted = true;
                    }
                }else{
                    this.waiters.remove();
                    break;
                }
            }
            if(interrupted){
                //最佳阻塞期间的中断请求
                currentThread.interrupt();
            }
        }
        lockOwnerThread = currentThread;
    }

    public void unlock(){
        Thread currentThread = Thread.currentThread();
        if(currentThread != lockOwnerThread){
            throw new IllegalMonitorStateException("当前线程未持有锁,不允许执行释放锁操作!");
        }
        //释放锁
        state.set(0);
        //通知线程
        LockSupport.unpark(waiters.peek());
    }
}

通过上述代码及实现了简单的锁结构:实际上是伪锁,因为在代码中使用到了LinkedBlockingQueue的waiters,而queue内部通过ReentrantLock来保证线程安全的;其次使用了AtomicInteger来简化CAS操作,如果将waiters改为其他非线程安全的队列在通过cas操作来保证线程安全同时使用Unsafe类的CAS操作来修改state就接近ReenTrantLock了;

在效率测试中对比RentrantLock发现当循环上升到千万级别时效率差距很大,就其原因主要在于对waiters的操作上增加了两次的锁操作功能,尝试优化上述代码去除AtomicInteger和LinkedBlockQueue。代码说明:

  • jdk提供的最原始的CAS操作位于Unsafe类中,但由于Unsafe获取实例的方法不允许用户访问因此使用反射方法获取Unsafe类的静态实例对象。
  • CAS属于cpu的指令,需要3个参数分别是:目标内存地址、期望值、修改值;其逻辑为:如果目标内存地址的值和期望值相等则将目标内存地址的值更改为修改值并返回true,否则返回false。
  • 关于jdk中如何获取对象成员变量的内存地址可参考java对象模型以及Unsafe类的具体方法。

实现方案2

注意实现方案2中的unlock方法注释部分在测试中会抛出NullPointerException,,大家可以分析为什么会抛出该异常。

package com.test.sync;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

public class MyLock implements Lock {

    static class Node {//双向链表结构用于保存阻塞线程
        private volatile Node pre;
        private volatile Node next;
        private volatile Thread thread;

        public Node(Thread thread) {
            this.thread = thread;
        }
    }

    private volatile int state;//锁信息,0表示未被获取,1表示已被线程获取
    private volatile Node head;//最先被阻塞的线程所在Node,实际上表示占有锁的线程的Node,主要用于保证真正被阻塞的Node的pre属性不为空
    private volatile Node tail;//最后被阻塞的线程所在Node,该属性用于通过循环cas操作将阻塞线程保存在队尾
    private volatile Thread lockHoldThread;//占有锁的线程

    //顾名思义非安全类,jdk对获取其实例的静态方法getUnsafe添加了@CallerSensitive注解,不能被用户直接调用,因此通过反射机制获取其theUnsafe实例
    private static Unsafe unsafe;
    private static final long stateOffset;//state成员变量的内存偏移量,详情参考java对象模型
    private static final long tailOffset;//tail成员变量的的内存偏移量,同上

    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
            stateOffset = unsafe.objectFieldOffset(MyLock.class.getDeclaredField("state"));
            tailOffset = unsafe.objectFieldOffset(MyLock.class.getDeclaredField("tail"));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
    }

    //state的cas操作
    private boolean compareAndSwapState(int expected, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expected, update);
    }

    //tail的cas操作
    private boolean compareAndSwapTail(Node expected, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expected, update);
    }

    @Override
    public void lock() {
        Thread ct = Thread.currentThread();
        if (!compareAndSwapState(0, 1)) {
            boolean interrupted = false;
            //基于cas的自旋操作将当前线程添加到阻塞队列尾部
            Node ctNode = new Node(ct);
            for (; ; ) {
                Node t = tail;
                if (compareAndSwapTail(t, ctNode)) {
                    if (t == null) {
                        t = head = new Node(null);
                    }
                    ctNode.pre = t;
                    t.next = ctNode;
                    break;
                }
            }
            //基于cas操作自旋获取锁,成功获取锁后退出循环,否则将循环阻塞
            for (; ; ) {
                if (ctNode.pre == head && compareAndSwapState(0, 1)) {
                    head.next = null;
                    ctNode.pre = null;
                    ctNode.thread = null;
                    head = ctNode;
                    break;
                } else {
                    LockSupport.park(this);
                    if (ct.isInterrupted()) {
                        interrupted = true;
                    }
                }
            }
            if (interrupted) {
                ct.interrupt();
            }
        }
        lockHoldThread = ct;
    }

    @Override
    public void unlock() {
        if (Thread.currentThread() != lockHoldThread) {
            throw new IllegalMonitorStateException("当前线程未持有锁,不允许执行释放锁操作!");
        }
        state = 0;
        Node n = null;
        if (head != null && (n = head.next) != null) {
            LockSupport.unpark(n.thread);//测试中此处将抛出NullPointerException异常
        }
        /**
        if (head != null && head.next != null) {
            LockSupport.unpark(head.next.thread);//使用该代码替换上面代码将抛出NullPointerException异常,分析原因
        }
        */
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean tryLock() {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        throw new UnsupportedOperationException();
    }

    @Override
    public Condition newCondition() {
        throw new UnsupportedOperationException();
    }
}

此次已经实现了一个线程安全的独享锁了,但在对比ReenTrantLock测试效率结果发现比上一版本差距还要夸张!且随着并发线程数和循环计算次数的增加差距越来越大!!具体测试代码这里没有粘贴,相差了20倍以上!且ReentrantLock对CPU的占有率在80%左右,而MyLock则高达100%。

上述代码已经接近ReentrantLock的逻辑了,但实际效率不在一个量级,而且ReentrantLock不管是增加并发线程数还是增加单线程循环计算次数均能保持较好的效率。大家可以尝试优化一下上述代码得到一个与ReentrantLock效率相近的结果

ReentrantLock warmUp!耗时:0毫秒,4个线程同时执行10000次+1操作,结果为:40000
MyLock warmUp!耗时:47毫秒,4个线程同时执行10000次+1操作,结果为:40000
ReentrantLock累加计算耗时:4590毫秒,100个线程同时执行1000000次+1操作,结果为:100000000
MyLock累加计算耗时:99758毫秒,100个线程同时执行1000000次+1操作,结果为:100000000

独占锁:ReentrantLock源码分析

类依赖和类成员变量说明

上述实现已经接近于AQS的逻辑,来分析一下AQS源码
先看一下AQS的成员变量:与方案2中的成员变量相差不大,下面仅列出不一样的部分:

  • spinForTimeoutThreshld:超时等待时间的最小阈值,单位ns
  • nextOffset:Node对象的next变量的内存偏移量
  • ConditionObject:Condition的默认实现,用于线程阻塞后加入特定阻塞队列中,持锁线程在释放锁时可指定阻塞队列进行唤醒,适用于类似生产者消费者模式
    AQS成员变量

加锁过程,入口方法:lock

    public void lock() {
    	//sync分非公平和公平实现
        sync.lock();
    }
    //非公平锁实现
    final void lock() {
    	//直接尝试cas操作获取锁,如果成功则将独占线程更新为自身线程
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
        	//获取锁失败进入正常获取锁流程
            acquire(1);
    }
    //公平锁实现
    final void lock() {
        //相对于非公平锁,没有直接通过cas操作获取锁,进入正常获取锁流程
        acquire(1);
    }

acquire是AQS获取独享锁的顶层入口

	/* 
	* arg参数表示尝试获取锁的数量,对于独占锁,arg=1
	*/
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

看一下tryAcquire方法的具体逻辑,该方法表示是否成功获取锁

    /*
    方法体中直接抛出异常,表明该方法应该在子类中实现,这么做的主要原因在于公平锁和非公平锁实现有所差异
     */
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

独占锁(ReentrantLock)中的非公平锁的实现逻辑

		protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
    	}
    	
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            	//如果当前锁没有被其它线程获取,则尝试通过cas操作占有该锁
                if (compareAndSetState(0, acquires)) {
                	//将独占锁的线程设置未当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果当前线程已经占有该锁则计算锁重入,做了一个int数值溢出的判断
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

再看一下独占锁(ReentrantLock)中的公平锁的实现逻辑

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
        	//相对于公平锁增加了是否存在排队获取锁的判断,若存在排队情况,则不获取锁,顺便看一下hasQueuedPredecessors方法判断逻辑
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                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;
    }
    public final boolean hasQueuedPredecessors() {
    	//因此定义局部变量来保存瞬时值。这在前面方案2中抛出NullPointerException已经得到验证
        Node t = tail; 
        Node h = head;
        Node s;
        //如果存在排队且非当前线程处于排队的队首
        return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
    }

当上面尝试获取锁不成功后将执行addWaiter方法将当前线程加入等待获取锁队列中

    private Node addWaiter(Node mode) {
    	//将当前线程封装未Node对象
        Node node = new Node(Thread.currentThread(), mode);
        // 如果当前存在排队情况则尝试通过cas操作将当前队列加入到队尾并与之前的队尾Node相互连接
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果不存在排队情况或上述cas操作失败,则执行enq方法后并最终返回当前node
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
    	/**
    	* 典型的cas自旋操作,逻辑具体如下:
    	* 1.如果当前不存在排队情况,则先通过cas操作将队首初始化(队首Node不含具体的排队线程信息,主要用于保持每个排队的线程对应的Node都有一个前置Node,这里可以将队首Node看成获取锁的Node)
    	* 通过第一步保证tail和head不为空后再通过循环cas操作将当前节点添加到排队队尾并与前置节点相互链接
    	*/
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

成功将当前线程添加到队尾后将执行acquireQueued方法,逻辑如下:

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
        	//暂存阻塞期间的中断信息,阻塞期间不响应中断,中断请求会让阻塞的线程恢复执行
            boolean interrupted = false;
            //自旋,退出循环的条件时成功获取到锁,期间不响应中断
            for (;;) {
                final Node p = node.predecessor();
                //只有前置节点为head的节点可以尝试获取锁操作
                if (p == head && tryAcquire(arg)) {
                	//当前节点已成功获取锁,因此将当前节点设置为head,setHead方法会删除当前节点对thread的引用
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //判断当前节点是否可以进入阻塞状态,每个节点在真正进入阻塞前需要找到一个正常的前置节点后才可以放心进入阻塞状态后等待前置节点唤醒自己。
                //否则如果前置节点已经被删除则当前节点将永远不能被唤醒
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

//该方法主要是找到一个正常的前置节点修改其状态为SIGNAL,意为有后继节点等待唤醒
shouldParkAfterFailedAcquire代码如下:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            //如果前置节点已经处于SIGNAL状态则当前节点可以放心进入阻塞状态了
            return true;
        if (ws > 0) {
            //如果前置节点已取消(CANCELD),则需循环向前直到找到一个正常的节点为止
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果前置节点的状态不是SIGNAL,则将其设置为SIGNAL状态,从这里可以看出每个节点在进入阻塞前至少自旋2次。
            //第一次找到正常的节点并将其设置SIGNAL状态返回false,第二次返回true并调用真正的阻塞方法
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt方法逻辑,将自身线程阻塞,如果线程被唤醒(可能是中断请求导致唤醒,这种情况下近暂存中断标志后再次进入阻塞),返回线程的中断情况

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

自旋期间由于为止因素导致线程异常退出for循环(failed=true),则执行cancelAcquire逻辑,该逻辑不可能被执行,保守代码

    private void cancelAcquire(Node node) {
        // 节点为空则不做后续处理,实际上node不可能为空
        if (node == null)
            return;
		//置空node对象thread
        node.thread = null;

        // 如果当前节点的前置节点已被取消(waitStatus=CANCEL),则循环向前删除掉所有已被取消掉的节点
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // 很显然该节点要么是当前节点,要么是其他被CANCELED的节点,都会被从队列中去除
        Node predNext = pred.next;

        // 设置当前节点为CANCELED后,其他线程在寻找节点时将跳过该节点
        node.waitStatus = Node.CANCELLED;

        // 如果当前节点已经时队尾,则尝试将当前节点的前置节点设置为队尾并将队尾的next属性设置为空(cas操作是因为有可能其他线程已经生成了新的队尾)
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // 
            int ws;
            /*将正常的前置节点和当前节点的next节点连接起来,这个需要满足一定的条件,否则将执行唤醒操作,具体如下:
            * 1、当前置节点为head时显然是要执行unparkSuccessor
            * 2、前置节点SIGNAL检查,主要可能是前置节点也刚好在此时执行的cancelAcquire操作(waitStatus=CANCELED或thread=null),则必须唤醒后置节点重新找到一个正常的前置节点后重新进入阻塞
            */
            
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

自此独占锁的加锁过程就已经分析完成,再看一下独占锁的释放锁过程

释放锁过程,入口方法:unlock

    public void unlock() {
        sync.release(1);
    }
    
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
        	//如果锁已完全释放(重入锁每重入一次锁+1,需要多次释放),则尝试通知唤醒排队阻塞线程,
            Node h = head;
            if (h != null && h.waitStatus != 0)
            	//仅当阻塞Node的waitStatus!=0时才执行唤醒操作,这里真正唤醒的是h.next,而每个node阻塞前都会将前置节点设置设置为SIGNAL(-1),因此总能满足
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    

tryRelease方法源码如下:

    protected boolean tryRelease(int arg) {
    	//方法直接抛出异常,具体实现在子类中实现,主要目的是独享锁和共享锁的锁释放逻辑不相同
        throw new UnsupportedOperationException();
    }

看一下独享锁的释放逻辑:

        protected final boolean tryRelease(int releases) {
        	//计算释放后的锁数量,重入锁需要完全释放才返回true
            int c = getState() - releases;
            //释放锁线程校验,必须持有锁的线程才能释放锁
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //更新锁信息,=0表示可以被其他线程获取
            setState(c);
            return free;
        }

当成功释放锁后将执行唤醒阻塞线程操作

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
        	//对于共享锁而言,可能同时存在多个线程同时释放锁,因此采用cas操作只有一个线程将node(head)节点状态更新为0
            compareAndSetWaitStatus(node, ws, 0);
		//找到一个真正需要被唤醒的线程,这是因为部分线程节点可能已经被取消,同时从后向前遍历是因为该链表节点的遍历非线程安全,可能在遍历过程中出现新增节点的情况
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
        	//唤醒阻塞线程
            LockSupport.unpark(s.thread);
    }

至此独占锁的加锁和释放锁逻辑分析完成。注意上述加锁和释放锁的过程中存在一种情况:当一个线程在最终执行阻塞调用LockSupport.park(this)之前是如果持有锁的线程已完成释放锁,此时park操作将不会真正阻塞,这是因为park操作本身有一个许可保证,即执行unpark(thread)操作会让thread的许可+1(最大为1),当许可为1时park操作只会将许可-1而不会阻塞线程,当然连续执行2次park则第二次将阻塞线程。这是因为第二次时许可已经为0

中断式获取锁,入口方法:lockInterruptibly

ReentrantLock支持中断式获取锁,即如果线程获取不到锁可通过中断方式让线程退出获取锁流程转而去执行中断异常处理。

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
        	//获取锁期间如果由中断请求,直接抛出中断异常退出获取锁流程
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

tryAcqire与lock方法一致,区别在于doAcquireInterruptibly,逻辑与lock大体相同,区别在于在阻塞期间对中断的处理,如果由于中断唤醒阻塞,将直接抛出中断异常而不是保留中断标志

    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //直接抛出中断异常
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

非阻塞式获取锁,入口方法:tryLock

该方法尝试获取锁,如果获取不到锁直接返回,原理是park指定时间后自动醒来判断是否超时,如果超时则返回获取锁失败

    public boolean tryLock() {
    	//与lock的非公平方式尝试获取锁逻辑相同,获取不到直接返回
        return sync.nonfairTryAcquire(1);
    }

超时退出获取锁,入口方法:tryLock(long timeout, TimeUnit unit)

	public boolean tryLock(long timeout, TimeUnit unit)
	        throws InterruptedException {
	    //调用AQS的tryAcquireNanos
	    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
	}
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //现场时直接获取锁,失败后尝试等待nanosTimeout时长
        return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
    }

doAcquireNanos与acquireQueue逻辑基本相同

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        //如果已超时,则直接返回获取锁失败
        if (nanosTimeout <= 0L)
            return false;
        //计算获取锁的截止时间
        final long deadline = System.nanoTime() + nanosTimeout;
        //将当前节点添加到阻塞队列
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                nanosTimeout = deadline - System.nanoTime();
                //如果当前线程超时自醒后已超过截止时间则返回失败
                if (nanosTimeout <= 0L)
                    return false;
                //如果剩余时间大于自旋时间1000ns,则继续阻塞剩余时间
                if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout >  spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
            	//响应中断或其它为止异常删除当前节点
                cancelAcquire(node);
        }
    }

共享锁:ReentrantReadWriteLock读写锁源码分析

类依赖和成员变量说明

ReentrantReadWriteLock读写锁满足以下特点

  • 如果处于读锁模式(有线程成功获取到读锁),则获取写锁将被阻塞且后续的读锁和写锁均会被阻塞,如果读锁模式下没有线程获取写锁而被阻塞,则读锁均可获取成功
  • 如果处于写锁模式(有线程成功获取到写锁),则无论获取读锁或写锁均会被阻塞(获取到写锁的线程可再次获取读锁或写锁)

ReentrantReadWriteLock类内部依赖关系图如下:
ReentrantReadWriteLock依赖关系
从上图中可以看出ReentrantReadWriteLock类实现了ReadWriteLock接口且内部定义了5个内部类,具体如下:

  • ReadLock:读锁的具体实现
  • WriteLock:写锁的具体实现
  • Sync:继承自AQS,公平锁和非公平锁的基类,在Sync内部又定义了两个内部类
    • HoldCounter:实际上仅仅是一个维护线程和读锁数量的数据结构
    • ThreadLocalHoldCounter:ThreadLocal的子类,维护所有线程的读锁数量信息
  • FairSync:继承自Sync,公平锁的具体实现
  • NonfairSync:基层自Sync,非公平锁的具体实现

先看一下ReentrantReadWriteLock成员变量如下图:
ReentrantReadWriteLock成员变量

  • sync:公平锁或非公平锁的实例,通过构造器入参确定
  • readerLock:内部类readLock的实例
  • writerLock:内部类writerLock的实例
  • TID_OFFSET:Thread的tid变量的内存偏移量,用于通过Unsafe方式获取tid
  • UNSAFE:提供cas、park、unpark以及基于内存地址访问数据的底层工具类

在了解一下Sync的成员变量
Sync成员变量

  • EXCLUSIVE_MASK、MAX_COUNT、SHARED_UNIT、SHARED_SHIFT:ReentrantReadWriteLock通过AQS的state(int)来存储总读锁数量和总写锁数量信息,具体规则为:int类型为4字节,其中高位二字节用于存储读锁总数量,低位二字节用于存储写锁总数量,采用这种存储方式可以很好的利用cas操作来同时约束获取读锁和写锁的原子性。
    • EXCLUSIVE_MASK:EXCLUSIVE_MASK & state即可得到独占锁数量
    • MAX_COUNT:二字节的能表示的最大值:65535,同时也是读锁和写锁的上限值
    • SHARED_UNIT:共享锁每增加一把锁:state+SHARED_UNIT;
    • SHARED_SHIFT:state通过右移SHARED_SHIFT次即得到读锁数量
  • firstReader:首次获取读锁(state:0->1)的线程
  • firstReaderHoldCount:首次获取读锁的线程的读锁总数
  • cachedHoldCounter:HoldCounter实例,缓存当前线程的读锁信息
  • readHolds:ThreadLocalHoldCounter实例,保存所有线程的锁信息
    实际上firstReader、firstReaderHoldCount、cachedHoldCounter、是缓存,通过该面两直接获取避免从ThreadLocal中获取已提高效率

获取读锁,入口方法:ReadWriteLock.readLock().lock()

	public void lock() {
		//调用AQS的acquireShared
	    sync.acquireShared(1);
	}
	
    public final void acquireShared(int arg) {
    	//
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

tryAcquireShared在ReentrantReadWriteLock中实现,具体如下:

	protected final int tryAcquireShared(int unused) {
	    
	    Thread current = Thread.currentThread();
	    int c = getState();
	    //如果已有线程获取写锁且不是当前线程则直接返回-1(获取读锁失败)
	    if (exclusiveCount(c) != 0 &&
	        getExclusiveOwnerThread() != current)
	        return -1;
	    //获取读锁总数
	    int r = sharedCount(c);
	    //如果获取读锁不应该被阻塞且通过cas成功将读锁总数+1则认为当前线程获取读锁成功,这里readerShouldBlock区分公平锁和非公平锁模式,下面接着分析二者区别
	    if (!readerShouldBlock() &&
	        r < MAX_COUNT &&
	        compareAndSetState(c, c + SHARED_UNIT)) {
	        if (r == 0) {
	        	//如果线程获取到的是第一把读锁,将当前线程和读锁数量(1)设置成firstReader、firstReaderHoldCount 
	            firstReader = current;
	            firstReaderHoldCount = 1;
	        } else if (firstReader == current) {
	        	//如果是获取到第一把读锁的线程重入锁,则当前线程持有读锁数量+1
	            firstReaderHoldCount++;
	        } else {
	        	//如果不是获取到的第一把读锁的线程,则尝试将读锁和当前线程保存到cachedHoldCounter和readHolds,如果是重入,则将对应的读锁数量+1
	            HoldCounter rh = cachedHoldCounter;
	            if (rh == null || rh.tid != getThreadId(current))
	                cachedHoldCounter = rh = readHolds.get();
	            else if (rh.count == 0)
	                readHolds.set(rh);
	            rh.count++;
	        }
	        //返回获取读锁成功
	        return 1;
	    }
	    //如果上述逻辑没有成功获取到读锁也没有返回-1,说明存在多个线程竞争导致cas操作失败,继续执行完整版本的获取锁流程
	    return fullTryAcquireShared(current);
	}
	
	//判断读锁应当被阻塞的非公平实现
	final boolean readerShouldBlock() {
	    //如果存在排队且队首的Node是写锁Node,则当前读锁应当被阻塞
	    return apparentlyFirstQueuedIsExclusive();
	}
    final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }

	//判断读锁应当被阻塞的公平实现
	final boolean readerShouldBlock() {
		//只要存在怕对情况就认为当前获取读锁应当被阻塞
	    return hasQueuedPredecessors();
	}//readerShouldBlock的公平模式和非公平模式的主要区别在于如果是写锁模式下阻塞了多个读锁,在写锁释放瞬间非公平模式可能会插队,而公平模式不可能插队

完整版本获取读锁:fullTryAcquireShared

        final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                //写锁模式阻塞读写锁的获取
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                } else if (readerShouldBlock()) {
                    // 如果获取读锁应当被阻塞,则只有重入锁的情况下才能获取读锁,通过判断线程持有锁的数量>0来判断是否重入
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                    	//先从cachedHoldCounter对应线程位当前线程则使用cachedHoldCounter,否则从readHolds中获取
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                	//清楚无用缓存
                                    readHolds.remove();
                            }
                        }
                        //如果不是重入锁,则返回获取失败
                        if (rh.count == 0)
                            return -1;
                    }
                }
                //锁溢出检测(65535)
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //读锁+1的cas尝试
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                	//成功获取锁根据是否是获取到第一把读锁的线程初始化相应变量并设置新的读锁数量,与上面逻辑相同
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    //返回获取读锁成功
                    return 1;
                }
            }
        }

如果上述tryAcquireShared返回-1(获取读锁失败),则继续执行doAcquireShared

    private void doAcquireShared(int arg) {
    	//逻辑与AQS的acquireQueued相似,就有区别的地方进行注释
    	//将当前线程封装成Node(SHARE模式)添加到队尾
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋获取锁
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                    	//如果成功获取读锁,则将当前线程的Node设置位Head并根据条件通知后续节点,如果后续节点是读锁节点,则肯定能获取到锁,下一步详说setHeadAndPropagate
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        //获取读锁成功,响应中断
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

当被阻塞的读锁节点成功获取锁后将执行setHeadAndPropagate来通知后继节点

    private void setHeadAndPropagate(Node node, int propagate) {
    	//在将当前节点设置位head前先保存旧head的引用
        Node h = head; // Record old head for check below
        //将当前节点设置为head
        setHead(node);
        /*propagate>0说明有可用读锁供获取,因此阻断后续判断直接执行doReleaseShared
        *如果propagate=0说明没有可用的读锁供其它线程获取,按理说不应该去唤醒后继节点尝试获取读锁了。
        *h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0先后判断了旧head和新head的waitStatus是否<0(实际上head在addWaiter之后肯定不可能为null了)
        *这里需要注意到的是head的状态=-1是由后继节点标记的,在共享锁的释放过程中还可能出现=-3和=0的情况,其中=0是一种短暂的中间状态,在doReleaseShared和unparkSuccessor都会通过cas将状态置为0的情况
        *状态为0说明有线程正在释放锁且已经调用unparkSuccessor通知阻塞线程了,如果=-3则说明有线程正在释放锁且没有调用unparkSuccessor通知后继阻塞线程,这就有可能出现立即会有空闲读锁了
        *实际上源码中已经注释了这样判断可能会引起不必要的线程唤醒后重新阻塞,但作者认为通过<0判断即便propagate=0也有可能有线程正在释放锁
        */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //下一个节点是SHARE节点的话尝试唤醒该节点
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

doReleaseShared方法会在读锁释放过程中和当前位置处被调用,因此该方法可能存在多个线程并行的情况

    private void doReleaseShared() {
    	//退出循环的条件是循环内部执行过程中没有其它其它线程修改head
        for (;;) {
            Node h = head;
            //存在阻塞节点的判断
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                	//释放锁时会将head节点的状态cas设置为0,如果此处并发则只有一个线程会调用到unparkSuccessor
                	//需要注意的时head节点被设置为0后将很快被唤醒的线程将自身节点设置为新head,而旧head脱离队列被回收
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                //如果由于上述并发导致有线程cas失败,则失败的线程会将head设置为PROPAGATE(-1),这也是前面为什么在propagate=0的情况下任然认为可能马上有读锁被释放的原因
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

unparkSuccessor:唤醒head后继节点的最终方法

    private void unparkSuccessor(Node node) {
        //该方法在多个地方被调用,因此再次做了cad将节点状态设置为0的操作
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
		//寻找一个正常的后继节点(去除CANCELED状态),这里做s==null判断是应为可能存在cancelAcquired操作刚好将node节点和next节点断开还没来得及设置新的next的情况
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            //从队尾向前遍历,找到一个离node最近的正常节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //唤醒节点
        if (s != null)
            LockSupport.unpark(s.thread);
    }

自此读锁的获取流程已经结束

释放读锁,入口方法:ReadWriteLock.readLock().unlock()

	public void unlock() {
		//AQS的releaseShared
	    sync.releaseShared(1);
	}
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

tryReleaseShared:判断本次读锁释放后当前线程的所有读锁已释放完成(锁重入时当前线程会持有多个读锁)

        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            //释放读锁,如果时获取到第一把读锁的线程通过firstReader和firstReaderHoldCount来操作释放读锁,否则通过cachedHoldCounter和readHolds来释放读锁
            if (firstReader == current) {
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    //当前锁还有-1就已经<=0说明存在错误(没有先通过lock来获取锁直接调用unlock)
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                //线程持有锁-1
                --rh.count;
            }
            //自旋操作,释放锁可能出现并发情况
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    //当前释放锁后持有锁的数量=0则说明已经释放完成
                    return nextc == 0;
            }
        }

如果上述线程完全释放锁,则继续执行doReleaseShared,而doReleaseShared已经在前面的setHeadAndPropagate中被调用过,不在此处继续分析。进说明一下doReleaseShared可能并发的线程时多个尝试释放锁的线程以及被释放锁线程唤醒并成功获取到锁的线程。

获取写锁,入口方法:ReadWriteLock.writeLock().lock()

	public void lock() {
		//调用AQS的acquire
	    sync.acquire(1);
	}
	//与前面的ReentrantLock逻辑相同,其中tryAcquire由ReentrantReadWriteLock重写
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

ReentrantReadWriteLock.Sync.tryAcquire获取写锁,重源码中可以发现

	protected final boolean tryAcquire(int acquires) {
	    Thread current = Thread.currentThread();
	    //获取读锁、写锁信息
	    int c = getState();
	    //获取写锁总数
	    int w = exclusiveCount(c);
	    if (c != 0) {
	        // 如果存在读锁并且不是当前线程持有锁,获取写锁失败
	        if (w == 0 || current != getExclusiveOwnerThread())
	            return false;
	        //写锁总数溢出检查
	        if (w + exclusiveCount(acquires) > MAX_COUNT)
	            throw new Error("Maximum lock count exceeded");
	        // 经过前面两次判断说明当前属于锁重入,锁总数+1并返回成功
	        setState(c + acquires);
	        return true;
	    }
	   	//如果写锁应该被阻塞或者cas尝试将写锁总数+1失败,返回获取写锁失败,writerShouldBlock区分公平模式和非公平模式,下面讨论二者区别
	    if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
	        return false;
	    //执行到这里说明写锁上述cas操作成功,将当前线程设置为独占锁线程并返回成功
	    setExclusiveOwnerThread(current);
	    return true;
	}
	
	//非公平模式的writerShouldBlock总是返回false
	final boolean writerShouldBlock() {
	    return false;
	}

	//公平模式的writerShouldBlock如果存在排队情况则返回true
	final boolean writerShouldBlock() {
	    return hasQueuedPredecessors();
	}
	//与readerShouldBlock相比writerShouldBlock的非公平模式在任何情况下都不应该阻塞,这是由于读锁共享而写锁独占的原因。读锁需要保证写锁有更大的机会能够获取到锁

写锁的逻辑与ReenTrantLock的逻辑大体相似。

释放写锁,入口方法:ReadWriteLock.writeLock().unlock()

写锁的释放过程与ReenTrantLock的unlock过程基本一直,区别在于重写了其中的tryRelease,具体如下:

	protected final boolean tryRelease(int releases) {
		//检查是否当前线程独占锁
	    if (!isHeldExclusively())
	        throw new IllegalMonitorStateException();
	    int nextc = getState() - releases;
	    //判断本次锁释放后写锁是否释放完成
	    boolean free = exclusiveCount(nextc) == 0;
	    if (free)
	        setExclusiveOwnerThread(null);
	    setState(nextc);
	    return free;
	}

ReenTrantLock.tryRelease与ReenTrantReadWriteLock.tryRelease基本相同,区别在于后者仅判断写锁部分(state低2字节部分)是否为0。
自此读锁和写锁的加锁和释放锁过程分析完成。

条件锁:Condition(ReentrantLock实现)源码分析

假设现在有这样一个需求:
	1.有26个线程,它们的工作时分别打印大写字母A~Z,每个线程打印固定的字母
	2.要求一个线程首先打印A,然后让另一个线程打印B···,一直交替有序到最后一个线程打印Z
	3.重复2的操作,要求每打印完一轮输入一个tab空格,每打印完3轮输出一个换行,总共打印10行
	4.打印结果如下所示:
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	

如何去实现这样一个需求?从需求中很容易发现这是一个多线程协调有序工作的问题,一个线程完成部分工作后唤醒指定的线程工作并让自身阻塞等待被唤醒后再次工作。有两种方案可以实现该需求

  • 使用synchronized配合wait和notifyAll,26个线程竞争临界资源,临界资源是一个字符(A~Z),初始化为A,,如果首先竞争成功的不是打印A的线程则调用notifyAll后在调用wait释放锁,直到打印A的线程成功获取锁并打印A,此时将临界资源修改为B后调用notifyAll和wait释放锁,重复上述操作即可实现,实现代码如下:
package com.test.sync;

import lombok.SneakyThrows;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class WaitNotifyTest {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(26);
        for (int i = 0; i < 26; i++) {
            es.submit(new PrintTask2((char)('A'+i)));
        }
        es.shutdown();
        es.awaitTermination(10, TimeUnit.SECONDS);
    }
}

class PrintTask2 implements Runnable{
    private static Object lock = new Object();
    private static char shouldPrintChar = 'A';
    private char printChar;

    public PrintTask2(char printChar) {
        this.printChar = printChar;
    }

    @SneakyThrows
    @Override
    public void run() {
        synchronized (lock){
            for(int i = 1;i<=30;){
                if(printChar == shouldPrintChar){
                    System.out.print(printChar);
                    if(printChar == 'Z'){
                        shouldPrintChar = 'A';
                        System.out.print('\t');
                        if(i % 3 == 0){
                            System.out.println();
                        }
                    }else{
                        shouldPrintChar = (char)(shouldPrintChar + 1);
                    }
                    i++;
                    lock.notifyAll();
                }
                if(i<=30){
                    lock.wait();
                }
            }
        }
    }
}

测试结果符合要求,但很明显上述程序有个问答的问题,每当一个线程完成打印任务后不能指定唤醒另一个线程来接着打印工作,而是通过唤醒所有线程竞争锁资源,竞争到锁后判断是否是轮到自己打印了,如果不是则继续通知所有线程并释放锁阻塞自己。整个过程存在很多不必要的锁竞争唤醒阻塞。如果想实现唤醒指定线程工作则可以使用Condition来实现。

ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
  • Condition让线程阻塞在特定的队列上,当需要唤醒特定的线程来工作时,只需要调用特定的Condition的signal或signalAll后自身释放锁即可。这里的Condition是一个队列,可以让多个线程阻塞在此队列上,因此signal或signalAll分别为唤醒此队列上的一个和全部线程,本例中一个Condition只有一个线程,代码如下:
package test.other;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Practise {
    public static void main(String[] args) {
        PrintTask printTask = new PrintTask(30);
        for (int i = 'A'; i < 'Z'; i++) {
            printTask.newPrinter((char)i);
        }
        printTask.start();
    }
}

class PrintTask{
    private final ExecutorService executorService = Executors.newCachedThreadPool();
    private final Lock lock = new ReentrantLock();
    private final List<Printer> printerList = new ArrayList<>();
    private final int cycleCount;
    private volatile int printedCount;
    private Printer master;
    private volatile Printer inPerm;
    private boolean started;

    public PrintTask() {
        this.cycleCount = 1;
    }

    public PrintTask(int cycleCount) {
        this.cycleCount = cycleCount;
    }

    public synchronized void newPrinter(char printChar){
        if(started)
            throw new Error("打印任务已启动,不能再添加");
        Printer printer = new Printer(printChar);
        if(master == null){
            master = printer;
            inPerm = printer;
            printer.next = printer;
        }else{
            Printer lastPrinter = printerList.get(printerList.size() - 1);
            lastPrinter.next = printer;
            printer.next = master;
        }
        printerList.add(printer);
    }

    public synchronized void start(){
        started = true;
        printerList.forEach(executorService::submit);
        executorService.shutdown();
    }

    class Printer implements Runnable{
        private final char printChar;
        private final Condition cond = lock.newCondition();
        private Printer next;

        public Printer(char printChar) {
            this.printChar = printChar;
        }

        @Override
        public void run() {
            try{
                lock.lock();
                while(!finished()){
                    checkPrintOpportunity();
                    doPrint();
                    notifyNextPrinter();
                }
            }finally {
                lock.unlock();
            }
        }

        private void notifyNextPrinter() {
            inPerm = next;
            next.cond.signal();
        }

        private void doPrint() {
            System.out.print(printChar);
            if(this.next == master){
                System.out.print('\t');
                if(printedCount % 3 == 0)
                    System.out.println();
            }
        }

        private void checkPrintOpportunity() {
            if(this != inPerm){
                cond.awaitUninterruptibly();
            }
            if(this == master){
                printedCount++;
            }
        }

        private boolean finished() {
            return printedCount == cycleCount;
        }
    }
}

测试结果如下:显然使用Condition的代码更加优雅且效率更高。

ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	
ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	ABCDEFGHIJKLMNOPQRSTUVWXYZ	

下面开始分析Condition的源码。

创建一个Condition,入口方法:newCondition

    public Condition newCondition() {
    	//调用ReentrantLock内部的Sync(继承AQS)的newCondition
        return sync.newCondition();
    }
    final ConditionObject newCondition() {
    	//返回一个ConditionObject实例(该类定义在AQS内部),下面看一下这个类
        return new ConditionObject();
    }

先看一下类ConditionObject的成员变量,一个类似Node结构的类,是一个单向链表结构
ConditionObject成员变量

  • firstWaiter:该条件队列上的第一个阻塞节点
  • lastWaiter:该条件队列上的队尾阻塞节点
  • REINTERRUPT:常量标识,当阻塞在这个队列上的结点阻塞期间没有发生中断,且在发生中断之前该节点已经被调用signal,则在重新获取锁后响应中断
  • THROW_IE:与REINTERRUPT相对应,发生中断时该节点还没有被signal,则将抛出中断异常

让线程阻塞在Condition上,入口方法:await

调用await前需要先获取锁,通过该方法将当前线程阻塞在Condition上并释放锁

	public final void await() throws InterruptedException {
		//响应中断
	    if (Thread.interrupted())
	        throw new InterruptedException();
	    //将当前线程封装成Node追加在Condition队列的队尾
	    Node node = addConditionWaiter();
	    //释放所有锁(包含重入锁)
	    int savedState = fullyRelease(node);
	    int interruptMode = 0;
	    /*循环阻塞当前队列,退出循环条件:
	    *	1.当前队列已经被追加到AQS队列上(通过中断或signal)
	    *	2.发生了中断
	    */
	    while (!isOnSyncQueue(node)) {
	    	//阻塞线程
	        LockSupport.park(this);
	        //如果发生了中断请求,则退出循环阻塞
	        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
	            break;
	    }
	    //被唤醒后将再次尝试获取锁且设置中断处理标识
	    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
	        interruptMode = REINTERRUPT;
	    //如果当前节点存在后继节点,则尝试清除后继节点中的CANCELED节点(如果有的话)
	    if (node.nextWaiter != null) 
	        unlinkCancelledWaiters();
	    //根据中断标识响应中断
	    if (interruptMode != 0)
	        reportInterruptAfterWait(interruptMode);
	}

addConditionWaiter将当前线程追加到Condition队尾

	private Node addConditionWaiter() {
	    Node t = lastWaiter;
	    // 如果队尾Node状态为CANCELED,清楚队列中的CANCELED节点
	    if (t != null && t.waitStatus != Node.CONDITION) {
	        unlinkCancelledWaiters();
	        t = lastWaiter;
	    }
	    //将当前线程添加到队尾,由于此时已经时独占锁状态,因此不需要cas自旋操作
	    Node node = new Node(Thread.currentThread(), Node.CONDITION);
	    if (t == null)
	        firstWaiter = node;
	    else
	        t.nextWaiter = node;
	    lastWaiter = node;
	    return node;
	}

添加到队尾后将尝试释放锁fullyRelease

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
        	//获取当前所有读锁数量进行释放,释放锁逻辑与独占锁逻辑相同,不在赘述
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

判断节点是否已经在同步队列上isOnSyncQueue

    final boolean isOnSyncQueue(Node node) {
    	//如果节点状态还是CONDITION,则肯定不在同步队列上,因此从condition队列添加到同步队列必然会将状态从CONDITION->0
    	//如果node.pre==null,说明同步队列没有任何节点,因此也不在同步队列上
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) //如果存在next属性,必然在同步队列上,只有同步队列才会设置next属性
            return true;
        //上述情况都不满足则遍历整个同步队列看是否存在该node节点,这种情况只有在该节点刚刚添加到同步队列时,此时状态为0且为队尾node
        return findNodeFromTail(node);
    }

循环阻塞期间检查中断信息checkInterruptWhileWaiting

	private int checkInterruptWhileWaiting(Node node) {
		/*如果发生中断,则尝试将该节点添加到同步队列中并根据以下情况返回中断标识
		*	1.如果该节点任然是一个CONDITION节点,则将其加入到同步队列中并返回THROW_IE
		*	2.否则返回REINTERRUPT标识,说明此时该节点已经被其它线程通过signal方法加入同步队列
		*如果没有发生中断则返回0继续循环阻塞
		*/
		
	    return Thread.interrupted() ?
	        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
	        0;
	}

condition队列上的节点发生中断则尝试调用transferAfterCancelledWait将其添加到同步队列

    final boolean transferAfterCancelledWait(Node node) {
    	//即根据是否是Condtion节点将其追加到同步队尾
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);
            return true;
        }
        //如果另一个线程通过signal操作改变了node的状态但还没有完成节点加入到同步队列,则当前线程让步直到节点被加入到同步队列中后返回false
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }

让线程从Condition上唤醒并重新尝试获取锁,入口方法:signal

重前面await逻辑可知,除了signal唤醒之外,中断也能让线程唤醒被尝试获取锁,区别在于signal唤醒后在发生中断仅在获取锁后重新执行中断请求,而后者在重新获取锁后抛出中断异常

	 public final void signal() {
	 	//仅独占锁才可以调用signal
	     if (!isHeldExclusively())
	         throw new IllegalMonitorStateException();
	     Node first = firstWaiter;
	     通知第一个线程
	     if (first != null)
	         doSignal(first);
	 }
     private void doSignal(Node first) {
         do {
         	//队首firstWaiter执行第二个节点,如果不存在第二个,则说明当前只存在一个节点,置空队尾节点
             if ( (firstWaiter = first.nextWaiter) == null)
                 lastWaiter = null;
             first.nextWaiter = null;//断开first的nextWaiter的链接
             //循环尝试将一个节点放入到同步队列,如果节点不满足Condition,则循环下一个,直到有一个节点成功、退出循环,这里的节点可能由于发生中断已经被加入到同步队列了,因此通过循环来操作
         } while (!transferForSignal(first) && (first = firstWaiter) != null);
     }

    final boolean transferForSignal(Node node) {
        如果不是正常的Condtion节点,则返回false,doSignal将尝试唤醒下一个节点
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        //节点入同步队尾
        Node p = enq(node);
        int ws = p.waitStatus;
        //如果前置节点已经删除则有必要唤醒节点重新寻找一个新的前置节点并尝试获取锁
        //如果前置节点ws=0,说明处于释放锁的中间状态,此时唤醒也很有可能获取到锁
        //如果ws<-2,则说明有线程释放锁但没有通知后继线程,这个时候唤醒也有很大机率得到所
        //因此!compareAndSetWaitStatus判断主要用于是否有现成正在释放锁导致前置节点状态发生变化而让cas失败
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

signalAll与signal逻辑相差不大,不再分析,自此Condition源码分析完成,最后再看以下前面使用Condition完成交替打印字母的代码:这么写的作用是什么


    @Override
    public void run() {
        printInfo.cdl.countDown();
        try{
            printInfo.lock.lock();
            if(current != printInfo.begin.current){
                printInfo.initialCnt--;
                current.await();
            }else{
                while(printInfo.initialCnt > 1){
                    current.await(1, TimeUnit.MILLISECONDS);
                }
            }
			···
			···
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值