10.Semaphore源码解析

129 阅读9分钟

Semaphore实现原理

构造源码

//默认是非公平
public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }
Semaphore semaphore=new Semaphore(2);

当调用new Semaphore(2) 方法时,会把初始令牌数量赋值给同步队列的state状态,state的值就代表当前所剩余的令牌数量。

初始化完成后同步队列信息如下图:即头尾是空,指针指向null

image.png

获取令牌源码

semaphore.acquire();

1.当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子的操作去修改同步队列的state ,获取到一个令牌则修改state=state-1。

2.当计算出来的state<0,则代表令牌数量不足,此时会创建一个Node节点加入阻塞队列,挂起当前线程。

3.当计算出来的state>=0,则代表获取令牌成功,如果是大于等于0说明获取令牌成功。

	//默认获取1个令牌
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
	//共享模式下获取令牌,获取成功则返回,失败则加入阻塞队列,挂起线程
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试获取令牌,arg为需要获取令牌个数 默认arg=1
        //tryAcquireShared有2种实现:公平&&非公平
        //tryAcquireShared的返回值是剩余令牌数
        if (tryAcquireShared(arg) < 0){
             //当(可用令牌数) 减 (需要获取令牌数) 小于0 即 剩余令牌数小于0
        	//创建一个节点加入阻塞队列
            //如果当前节点是头结点的下一个节点,则会尝试获取锁,获取失败挂起当前线程。
            //如果当前节点的不是头结点的下一个节点,则挂起当前线程。
            doAcquireSharedInterruptibly(arg);
        }
    }  

tryAcquireShared:非公平锁:由令牌数决定是否获取锁

来了就去占有锁,然后返回剩余的令牌数。如果剩余令牌数小于0,说明获取锁失败需要阻塞在等待队列。

protected int tryAcquireShared(int acquires) {
	return nonfairTryAcquireShared(acquires);
}

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        //如果可用令牌数 - 待获取的令牌数  小于0 直接返回剩余令牌数
        //如果可用令牌数 - 待获取的令牌数  大于0 设置剩余令牌数到state中
        //此处cas可能会失败,但是因为死循环的存在,所以cas失败会继续重试
        //这里也有可能获取不到
         if (remaining < 0 || compareAndSetState(available, remaining))
              return remaining;
         }
    }
}

tryAcquireShared:公平锁:由等待队列决定是否获取锁

如果等待队列存在数据,那么直接返回-1,代表获取锁失败直接挂起。

protected int tryAcquireShared(int acquires) {
            for (;;) {
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }
 public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

doAcquireSharedInterruptibly

第一种情况:当前节点头结点的下一个节点即当前节点是老二,那么尝试获取令牌:

如果获取令牌成功并且剩余令牌数大于等于0,那么执行setHeadAndPropagate方法唤醒等待队列节点并返回。

如果获取令牌失败那么进入shouldParkAfterFailedAcquire方法中然后陷入阻塞

问题:为什么剩余令牌数等于0也要唤醒节点?

等于0说明正好state=1即有1个令牌,然后当前线程获取到了这个令牌,剩余令牌数是0。

在setHeadAndPropagate方法中 除了判断剩余令牌数还判断了head的waitStatus,所以在这个方法里并不一定会唤醒节点。

完整判断:propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || h.waitStatus < 0)

第二种情况:当前节点不是老二,进入shouldParkAfterFailedAcquire方法,在shouldParkAfterFailedAcquire方法中:

先判断当前节点的前置节点的waitStatus是为-1:如果是那么直接返回true陷入阻塞。

再判断当前节点的前置节点的waitStatus是否大于0:

如果是大于0:说明前置节点是取消状态,会断开前置节点,再次进入循环。

如果不大于0:那么修改当前节点的前置节点的waitStatus=-1再次进入循环:判断当前节点是否是老二:

如果是就再尝试加一次锁,走第一种情况。

如果不是:进入shouldParkAfterFailedAcquire,判断当前节点的前置节点的waitStatus=-1,进入阻塞。


    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //创建节点加入阻塞队列
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            //注意这里是个死循环
            //注意这里是个死循环
            //注意这里是个死循环
            for (;;) {
                //获得当前节点的pre节点
                final Node p = node.predecessor();
                //如果是当前节点的pre节点是头结点
                //说明自己是老二
                if (p == head) {
                    //再次尝试获取锁
                    //首先获取剩余令牌数
                    int r = tryAcquireShared(arg);
                    //如果剩余令牌数r>=0 说明有可用的令牌
                    //为什么等于0也要唤醒呢?
                    //在setHeadAndPropagate方法中 除了判断剩余令牌数还判断了head的waitStatus。
                    //完整判断:propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || h.waitStatus < 0)
                    if (r >= 0) {
                        //设置自己为头结点并唤醒等待队列上的节点
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //重组双向链表,清空无效节点,挂起当前线程
                //重组双向链表,清空无效节点,挂起当前线程
                //重组双向链表,清空无效节点,挂起当前线程
                //现在在死循环里,等待队列中被阻塞的节点会从这里被唤醒,然后重新进入进入死循环
                //判断自己是不是老二,如果是那么就尝试获取资源
                //可打断模式
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()){
                     throw new InterruptedException();
                }
                   
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire

//pred是node的前置节点 
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
    	//对于没有获取到锁的节点是true 会被阻塞
        if (ws == Node.SIGNAL)
            return true;
    	//如果ws大于0 断开节点
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果ws小于0 修改ws为-1 
            //在下一轮循环返回true
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
    	//返回false说明不需要park
        return false;
    }

parkAndCheckInterrupt

节点被park在这里

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

setHeadAndPropagate

//node是当前节点的前置节点即头结点 propagate是剩余可用令牌数
//为什么叫setHeadAndPropagate?
//setHead我们看到了,那么propagate如何理解?
//setHeadAndPropagate()方法就是在一个线程获取到令牌之后,唤醒它之后排队获取令牌的线程的。
//该方法可以保证线程2获取令牌后,唤醒线程3获取令牌,线程3获取令牌后,唤醒线程4获取令牌,以此类推,直到所有的线程都获取一遍令牌。
private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
    	//设置当前节点为头结点
        setHead(node);
        //如果剩余可用令牌数大于0 或者 头结点不为空 或者头结点等待状态<0
    	//剩余可用令牌数大于0 说明有资源可以争抢 可以唤醒等待节点来获取
    	//头结点为空 或者头结点等待状态<0 说明还有节点在等待 需要唤醒等待节点来获取
        if (propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                //唤醒等待节点
                doReleaseShared();
        }
    }
//解释了为什么Thread-0被设置为头结点后thread是null
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
doReleaseShared
    private void doReleaseShared() {
        //注意这里是个死循环
        for (;;) {
            //每次循环都会获取新头
            Node h = head;
            if (h != null && h != tail) {
                //获取头结点等待状态。
                int ws = h.waitStatus;
                //是否需要唤醒后继节点:如果头结点等待状态为 Node.SIGNAL
                if (ws == Node.SIGNAL) {
                    //修改头结点状态为初始状态0。
                    //为什么要修改为0? 
                    //为什么要修改为0? 
                    //为什么要修改为0? 
                    //因为shouldParkAfterFailedAcquire会判断前置节点的waitStatus 只有waitStatus<=0才会尝试获取1次锁
                    //当线程获取到了锁会修改头结点的状态为-1
                    //compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
                    //假设并发执行下面的cas会发生什么?即2个锁同时释放然后执行doReleaseShared
                    //这个问题答案在最后
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        //修改失败 继续循环
                        continue;
                    //唤醒头结点的下一个节点线程
                    unparkSuccessor(h);
                }else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE));
            }
             // loop if head changed:如果头部改变就继续循环
            //  什么时候头发生变化? 
            // 其他的线程获取到了令牌,调用setHeadAndPropagate
            if (h == head)                  
                break;
        }
    }
  private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 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);
    }

释放令牌

 semaphore.release();

当调用semaphore.release() 方法时

1、线程会尝试释放一个令牌,释放令牌的过程也就是把同步队列的state修改为state=state+1的过程

2、释放令牌成功之后,同时会唤醒同步队列的所有阻塞节共享节点线程

3、被唤醒的节点会重新尝试去修改state=state-1 的操作,如果state>=0则获取令牌成功,否则重新进入阻塞队列,挂起线程。

源码:

release

	//释放令牌
    public void release() {
        sync.releaseShared(1);
    }

releaseShared

	//释放共享锁,同时唤醒阻塞队列共享节点线程
    public final boolean releaseShared(int arg) {
        //释放共享锁
        if (tryReleaseShared(arg)) {
            //唤醒共享节点线程
            doReleaseShared();
            return true;
        }
        return false;
    }

还回令牌

 protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }

doReleaseShared

 	// 唤醒共享节点线程
    private void doReleaseShared() {
        //注意这里是个死循环 释放所有的等待节点
        for (;;) {
            
            Node h = head;
            if (h != null && h != tail) {
                //获取头结点等待状态
                int ws = h.waitStatus;
                //是否需要唤醒后继节点
                if (ws == Node.SIGNAL) {
                    //修改状态为初始状态0 
                     //为什么要修改为0? 
                    //因为shouldParkAfterFailedAcquire会判断前置节点的waitStatus 只有waitStatus<=0才会尝试获取1次锁
                    //为什么会修改失败?
                    //修改失败说明其他线程已经修改了
                    //其他线程在哪里修改的?
                    //在unparkSuccessor方法
                    //思考:
                    //假设头结点的ws == Node.SIGNAL,线程1 和 线程2 同时执行到了这里 
                    //线程1 执行 compareAndSetWaitStatus 成功 waitStatus由 -2  ---> 0
                    //线程2 执行 compareAndSetWaitStatus 失败
                    //线程1 执行 unparkSuccessor(h) 唤醒头结点的下一个节点线程
                    //线程2 重新进入循环,执行 ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)
                    //ws==0为true 并且 compareAndSetWaitStatus(h, 0, Node.PROPAGATE) 也为true
                    //修改为-3的意义在哪呢?
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)){
                        continue;
                    }else{                      
                      //唤醒头结点的下一个节点线程   
                      unparkSuccessor(h);
                    }
                //如果其他线程设置为0,那么我们要设置状态为PROPAGATE = -3   
                //为什么设置为PROPAGATE??
                }else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE));
            }
             // loop if head changed:循环到头发生变化
            //  什么时候头发生变化? 其他的线程获取到了令牌
            if (h == head)                  
                break;
        }
    }
//node是头结点  
private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
    	//头结点的ws < 0,cas修改ws为0 和上面的  if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) 呼应
        if (ws < 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);
    }

节点被唤醒后的流程

首先思考:被唤醒的节点阻塞在哪了?

阻塞在doAcquireSharedInterruptibly方法中

再思考:被唤醒的节点接下来会做什么?

1.获得当前节点的pre节点 2.如果当前节点的pre节点是头结点说明自己是老二 3.再次尝试获取锁 4.如果获取锁成功,首先获取剩余令牌数 5.如果剩余令牌数大于0 说明有可用的许可量 6.设置自己为头结点并唤醒等待队列上的节点

注意被唤醒的节点不一定会竞争锁,,等待队列中被阻塞的节点会在死循环里被唤醒,然后重新进入进入死循环,判断自己是不是老二,如果不是老二,继续去唤醒。

/**
     * 1、创建节点,加入阻塞队列,
     * 2、重双向链表的head,tail节点关系,清空无效节点
     * 3、挂起当前节点线程
     */
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //创建节点加入阻塞队列
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            //注意这里是个死循环
            //注意这里是个死循环
            //注意这里是个死循环
            for (;;) {
                //获得当前节点的pre节点
                final Node p = node.predecessor();
                //如果是当前节点的pre节点是头结点
                //判断自己是不是老二,如果是那么就尝试获取资源
                if (p == head) {
                    //再次尝试获取锁
                    //首先获取剩余令牌数
                    int r = tryAcquireShared(arg);
                    //如果剩余令牌数大于等于0
                    if (r >= 0) {
                        //如果r>0 说明有可用的许可量 
                        //唤醒等待队列上的节点
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //重组双向链表,清空无效节点,挂起当前线程
                //重组双向链表,清空无效节点,挂起当前线程
                //重组双向链表,清空无效节点,挂起当前线程
                //现在在死循环里,等待队列中被阻塞的节点会从这里被唤醒,然后重新进入进入死循环
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()){
                     throw new InterruptedException();
                }
                   
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire

//pred是node的前置节点 
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
    	//对于没有获取到锁的节点是true 会被阻塞
        if (ws == Node.SIGNAL)
            return true;
    	//如果ws大于0 断开节点
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果ws小于0 修改ws为-1 
            //在下一轮循环返回true
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
    	//对于获取到锁的节点是false ,因为获取到锁的时候会设置头结点状态为0
        return false;
    }

setHeadAndPropagate

第一种情况:当前节点是老二,那么尝试获取令牌如果已经获取到令牌,那么执行唤醒等待队列节点的操作

//node是当前节点的前置节点即头结点 propagate是剩余可用令牌数
private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
    	//设置当前节点为头结点
        setHead(node);
        //如果剩余可用令牌数大于0
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                //唤醒等待节点
                doReleaseShared();
        }
    }
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

继上面的图,当我们线程1调用semaphore.release(); 时候整个流程如下图:

总结

加锁:

1.当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子的操作去修改同步队列的state ,获取一个令牌则修改为state=state-12.当计算出来的state<0,则代表令牌数量不足,此时会创建一个Node节点加入阻塞队列,挂起当前线程。

3.当计算出来的state>=0,则代表获取令牌成功,如果是大于0说明还有剩余的资源会唤醒阻塞队列的节点。

释放锁:

1.线程会尝试释放一个令牌,释放令牌的过程也就是把同步队列的state修改为state=state+1的过程

2.释放令牌成功之后,同时会死循环唤醒同步队列的所有阻塞节共享节点线程。

3.被唤醒的节点会重新尝试去修改state=state-1 的操作,如果state>=0则获取令牌成功唤醒其他节点,否则重新进入阻塞队列,挂起线程。

哪些地方调用了doReleaseShared

doReleaseShared的作用是唤醒等待队列上的节点。

1.加锁的时候,加锁成功且剩余令牌数大于等于0:调用setHeadAndPropagate。

2.解锁的时候, 唤醒同步队列上的线程,如果加锁成功且剩余令牌数大于等于0:调用setHeadAndPropagate。

为什么叫setHeadAndPropagate

代码案例1

 private static Semaphore semaphore = new Semaphore(1);
    public static void main(String[] args) {
        //模拟5辆车进入停车场
        for (int i = 0; i <5 ; i++) {
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("===="+Thread.currentThread().getName()+"来到停车场");
                        //获取令牌尝试进入停车场
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName()+"成功进入停车场");
                        //模拟车辆在停车场停留的时间
                        Thread.sleep(new Random().nextInt(10000));
                        System.out.println(Thread.currentThread().getName()+"驶出停车场");

                        // semaphore.release()在一个线程多次获取后必须多次释放
                        // 释放令牌,腾出停车场车位
                        semaphore.release();

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }},i+"号车");
            thread.start();
        }
    }
====0号车来到停车场
====3号车来到停车场
====2号车来到停车场
====1号车来到停车场
0号车成功进入停车场
====4号车来到停车场
0号车驶出停车场
3号车成功进入停车场
3号车驶出停车场
2号车成功进入停车场
2号车驶出停车场
1号车成功进入停车场
1号车驶出停车场
4号车成功进入停车场
4号车驶出停车场

0号车相当于是第一个抢占到令牌的线程,直接进入了停车场。

当0号车释放资源,通知4号车,4号车进入停车场,以此类推。

当前我们只有1个资源

Semaphore semaphore = new Semaphore(1);

假设我们有2个资源呢?必然有2个线程先获取到资源,这2个线程释放资源以后,都会通知其他的线程。

代码案例2

在获取令牌的doReleaseShared方法中我们提出了1个问题, 假设cas并发修改头部的waitStatus=0会发生什么?

即2个锁同时释放然后执行doReleaseShared方法中的这一句代码:if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))。

private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }
//假设线程1 成功执行cas 然后执行 unparkSuccessor方法,即线程1释放锁成功并唤醒线程3,此时线程2cas必然执行失败重新进入死循环
//线程3会判断自己是否是老二,然后获取令牌成功后执行setHeadAndPropagate方法
//如果进入死循环时 线程3还没执行 setHeadAndPropagate方法 即头还没改变
//线程2cas仍会执行失败重新进入死循环
//如果进入死循环时 线程3已经执行 setHeadAndPropagate方法 即头已经改变为线程3
//线程2cas会执行成功

这说明了,即使多个线程同时释放令牌,这些线程也要通过cas的方式一个一个抢占式地去唤醒同步队列上的节点。

只有同步队列上的节点获取锁成功并且设置自己为头部,后续的线程才可能cas成功从而唤醒下一个节点。

模拟cas失败的场景

 static Semaphore semaphore = new Semaphore(2);
    public static void main(String[] args) {
        for (int i = 0; i <2 ; i++) {
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("===="+Thread.currentThread().getName()+"来到停车场");
                        //获取令牌尝试进入停车场 先让t0和t1获取到锁
                        semaphore.acquire();
                        //睡眠5s 让线程2 3 4 入队
                        Thread.sleep(1000 * 5);
                        System.out.println(Thread.currentThread().getName()+"成功进入停车场");
                        //模拟车辆在停车场停留的时间
                        System.out.println(Thread.currentThread().getName()+"驶出停车场");
                        // semaphore.release()在一个线程多次获取后必须多次释放
                        // 释放令牌,腾出停车场车位
                        semaphore.release();

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }},i+"号车");
            thread.start();
        }

        for (int i = 2; i <5 ; i++) {
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //睡眠3s让 t0 t1 有足够的时间获取到锁
                        Thread.sleep(1000 * 5);
                        System.out.println("===="+Thread.currentThread().getName()+"来到停车场");
                        //加入队列
                        semaphore.acquire();
                        //再睡1分钟 让t0 和 t1 同时释放锁 观察结果
                        Thread.sleep(10000 * 60);
                        System.out.println(Thread.currentThread().getName()+"成功进入停车场");

                        System.out.println(Thread.currentThread().getName()+"驶出停车场");
                        // semaphore.release()在一个线程多次获取后必须多次释放
                        // 释放令牌,腾出停车场车位
                        semaphore.release();

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }},i+"号车");
            thread.start();
        }
    }

多线程打断点即可。

为什么设置等待状态为PROPAGATE?

参考:blog.csdn.net/tomakemysel…

参考:blog.csdn.net/tomakemysel…

为什么引入PROPAGETE,是为了解决JAVA6之前的一个bug。

可以想象这样一种场景:假如当前CLH队列中有一个空节点和两个被阻塞的节点(t1和t2想要获取信号量但获取不到被阻塞在CLH队列中:(state初始为0))

head(ws=0) --------> t1(ws=-1)-------->t2(ws=-1)-------->tail(t3,ws=-1),t4和t5是已经获取到锁的线程。

image.png

在JAVA6的时候,当时的代码如下


private void setHeadAndPropagate(Node node, int propagate) {
    //将当前节点设置为头节点
    setHead(node);
 
    if (propagate > 0 && h.waitStatus != 0 ) {
        Node s = node.next;
        //获取当前节点的后继节点,如果它为null或者它是共享节点,则唤醒头节点的后继节点
        if (s == null || s.isShared()) {
            //唤醒后继节点
            doReleaseShared();
        }
    }
}

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0) {
            //头节点不为空,且状态不为0时,调用唤醒节点方法
            unparkSuccessor(h);
        }
        return true;
    }
    return false;
}
时刻1:t4调用release->releaseShared->tryReleaseShared,将state+1变为1。在doReleaseShared方法中发现此时的head节点不为null并且waitStatus为-1,,会用cas将head的waitStatus改为0。然后调用unparkSuccessor方法唤醒t1。

时刻2:t1被上面t4调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。注意,此时t1还没有调用setHeadAndPropagate方法。

时刻3:t5调用release->releaseShared->tryReleaseShared,将state+1变为1,同时发现此时的head节点虽然不为null,但是waitStatus为0,所以就不会执行unparkSuccessor方法。

时刻4:t1执行setHeadAndPropagate->setHead,将头节点置为自己。但在此时propagate也就是剩余的state已经为0了(propagate是在时刻2时通过传参的方式传进来的,那个时候-1后剩余的state0),即propagate > 0 条件不满足。所以也不会执行unparkSuccessor方法。

至此可以发现一轮循环走完后,CLH队列中的t2线程永远不会被唤醒,主线程也就永远处在阻塞中,这里也就出现了bug。

其实对于线程1来说确实是可以不往下传播往下继续唤醒节点,但是线程4肯定是要往下继续传播唤醒节点的。

那么来看一下现在的AQS代码在引入了PROPAGATE状态后,在面对同样的场景下是如何解决这个bug的:


private void setHeadAndPropagate(Node node, int propagate) {
    //获取队列头节点
    Node h = head;
    //将当前节点设置为头节点
    setHead(node);
 
    //如果propagate>0表示该资源还可以被获取
    //如果旧头节点为null或者旧头节点的状态小于0
    //如果新头节点为null或者新头节点的状态小于0
    if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        //获取当前节点的后继节点,如果它为null或者它是共享节点,则唤醒头节点的后继节点
        //读读共享,读写互斥,写写互斥
        if (s == null || s.isShared()) {
            //唤醒后继节点
            doReleaseShared();
        }
    }
}

private void doReleaseShared() {
    for (; ; ) {
        Node h = head;
        if (h != null && h != tail) {
            //如果头节点不为空,且存在后继节点
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                //判断头节点状态是否为-1,如果不是,则继续循环
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
                    //判断头节点的状态是否为-1,不是的话,就等待下次循环
                    continue;
                }
                //如果头节点状态为-1,则将它更改为0,再来唤醒后继节点
                unparkSuccessor(h);
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
                //如果头节点状态为0,且未能将该节点的状态更改为-3,则继续下一次循环
                continue;
            }
        }
        if (h == head) {
            //如果头节点还未发生变化,则跳出循环
            break;
        }
    }
}
时刻1:t4调用release->releaseShared->tryReleaseShared,将state+1变为1,继续调用doReleaseShared方法,将head的waitStatus改为0,同时调用unparkSuccessor方法。

时刻2:t1被上面t4调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。注意,此时t1还没有调用的setHeadAndPropagate方法。

时刻3:t5调用release->releaseShared->tryReleaseShared,将state+1变为1,同时继续调用doReleaseShared方法,此时会将head的waitStatus改为PROPAGATE。

时刻4:t1执行setHeadAndPropagate->setHead,将新的head节点置为自己。虽然此时propagate依旧是0,但是“h.waitStatus < 0”这个条件是满足的(h现在是PROPAGATE状态),同时下一个节点也就是t2也是共享节点,所以会执行doReleaseShared方法,将新的head节点(t1)的waitStatus改为0,同时调用unparkSuccessor方法,此时也就会唤醒t2了。

至此就可以看出,在引入了PROPAGATE状态后,可以有效避免在高并发场景下可能出现的、线程没有被成功唤醒的情况出现。

继续思考上面的场景如果没有时刻3,执行流程会是什么样子的?

时刻1:t4调用release->releaseShared->tryReleaseShared,将state+1变为1,继续调用doReleaseShared方法,将head的waitStatus改为0,同时调用unparkSuccessor方法。

时刻2:t1被上面t4调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。

时刻4:t1执行setHeadAndPropagate->setHead,将新的head节点置为自己。虽然此时propagate依旧是0,但是“h.waitStatus < 0”这个条件是满足的(h现在是SIGNAL状态),同时下一个节点也就是t2也是共享节点,所以会执行doReleaseShared方法,将新的head节点(t1)的waitStatus改为0,同时调用unparkSuccessor方法,此时也就会唤醒t2了。

继续思考一种极端的场景:t1执行完setHeadAndPropagate的setHead方法后的一瞬间,被其他线程把状态由-1改为了0。

时刻1:t4调用release->releaseShared->tryReleaseShared,将state+1变为1,继续调用doReleaseShared方法,将head的waitStatus改为0,同时调用unparkSuccessor方法。

时刻2:t1被上面t3调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。

时刻3:t1执行setHeadAndPropagate->setHead,将新的head节点置为自己。此时t4修改了头的状态即t1的状态由-1变为0。

时刻4:t1发现 (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) 条件不满足
因为propagate==0&& h.waitStatus==0
    
时刻5:t4会调用unparkSuccessor(h);唤醒t2