ReentrantLock和AQS源码解读系列四

共享锁

CountDownLatch为例子来讲比较容易理解,我们先来看个例子,有5个线程一期阻塞,然后需要CountDownLatch来唤醒,而且会同时唤醒所有线程,5个线程在等同一把锁,所有叫共享锁:

public class ShareLockTest {
        //等待的数量
    private static int num = 5;

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(num);
        //两个线程共享一把锁,即可以一起获得锁
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread() + "等待线程启动");
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "等待线程停止");
            }, "阻塞线程" + i).start();
        }
        Thread.sleep(1000);
        //工作子线程,只要完成5次任务就可以唤醒所有等待线程
        new Thread(() -> {
            for (int i = 0; i < num; i++) {
                latch.countDown();
                System.out.println(latch.getCount());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "工作子线程").start();
    }
}

CountDownLatch

里面也是有一个AQS同步器的实现类帮助实现的:
在这里插入图片描述

CountDownLatch构造方法

我们首先调用了构造方法CountDownLatch latch = new CountDownLatch(num);

 public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");//小于非法
        this.sync = new Sync(count);//初始化同步器
    }
  //AQS同步器
        Sync(int count) {
            setState(count);//设置状态
        }

实际上就是设置了同步器的状态为5,其实也就是说明起码要释放成功5次。

await

在这里插入图片描述
调用同步器的acquireSharedInterruptibly方法,一看就是可被打断的,而且会抛出异常:

 public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)//尝试获取共享状态如果是0的话就中断了
            doAcquireSharedInterruptibly(arg);
    }
tryAcquireShared

里面就是查看状态是不是0,而不会去修改锁的状态,修改锁的状态不是由自己来完成的,是由其他线程组来完成的:

 //如果状态为0,就表示获得共享,否则就没获得锁
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
doAcquireSharedInterruptibly

这里基本和独占的差不多,只是有setHeadAndPropagate这个,设置头之后还要传播:

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);//添加一个共享结点,nextWaiter为null,即没有条件等待结点,只有独占锁有
        try {//下面的大部分和独占的一样,自旋尝试获取锁,或者阻塞
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);//尝试获取共享锁
                    if (r >= 0) {//如果结果是>=0,表示获取到了共享锁,CountDownLatch的实现返回1或者-1,1表示状态为0成功获得,-1表示不为0
                        setHeadAndPropagate(node, r);//设置头结点并传播到后面结点
                        p.next = null; // help GC
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();//可中断,抛出异常
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }
setHeadAndPropagate

从这里开始,就是进入共享锁难的地方了,也是很精髓的地方,方可见作者的牛逼之处:

   private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below 缓存一下老的头结点
        setHead(node);//设置新头结点
 
        /*
        propagate > 0 也就是可以获取锁,而且可以同时多线程都获取锁,这个也算是共享锁的一个点吧。
        
        h为null 也就是说有新头结点了,老头结点h被回收了

		老头结点h存在且状态是PROPAGATE或者SIGNAL 

		新头结点head为null,就是此时有另外一个线程来了,他也进入了这个方法,setHead后,又设置了更新的结点,老的新结点也被回收了
		
		新头结点head状态是PROPAGATE或者SIGNAL
        */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;//获取后继结点
            if (s == null || s.isShared())//获取当前结点的后继,可能没有,当前结点就是最后一个,也可能有其他结点加进来了,但是必须是共享的结点
                doReleaseShared();//继续释放
        }
    }
doReleaseShared

这里就是共享传播的重点了,涉及到很多多线程的情况,这里主要是个无限循环,要的就是把所有共享的结点全部唤醒,退出循环的条件就是头结点不改变了,也就意味着没有能唤醒的结点,或者结点被唤醒了还没进入设置头的地方,或者是没有新加入的结点了,因为所有结点都唤醒后,他们都会去设置头结点,意味着头结点就是在变了,从队头到队尾,直到最后循环发现头结点不变了,那就是没有可以唤醒的了,于是就开始有线程退出循环,注意这里可能是多线程在执行,因为你唤醒了其他线程,其他线程也会到这里来执行,也就是说多个线程在一起换新后续结点,可能A唤醒了B,如果B还没设置头结点setHead的时候,A就结束了,因为此时头结点没变,A退出循环了,不过没关系,只要B在,他就会到这里来继续释放。如果B设置了头结点,还没进入到doReleaseSharedA也会继续释放B的后继。这种多线程并发唤醒就保证了极限的性能,因为只要一个线程setHead后,其他线程就可以进行唤醒,而不需要等到该线程执行doReleaseSharedunparkSuccessor的方法,基本是可以达到性能极限了,有点像CPU指令的多级流水线,只要条件满足,立马可以并行执行,大大提高了执行的效率。

 private void doReleaseShared() {
        /*
         * 这里要注意几点:
         * 1。只会唤醒SIGNAL的后继,而且不会重复唤醒,提高了性能
         * 2。如果唤醒SIGNAL的后继失败,就再次循环去设置PROPAGATE:
         * 		设置成功的话就等着继续唤醒,或者直接退出循环
         * 		设置失败就继续循环:
         * 			此时如果head变了,那就继续处理新head了
         * 			如果head没变,状态已经别的线程被设置成PROPAGATE了,那就什么都没做,退出循环了
         * 3.无论中间有多少多线程一起来参与这个唤醒工作,最后都会给head结点留下PROPAGATE状态
         * 
         */
        for (;;) {//无限循环,直到头结点不变了,线程才退出
            Node h = head;//获取头结点,多线程情况,头结点可能时刻在变的,因为后续被唤醒的线程都会去设置头结点
            if (h != null && h != tail) {//头结点不等于尾结点的情况,即除了头结点外有结点排队
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {//如果头结点状态是SIGNAL,后续有结点排队呢
                    if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))//CAS保证多线程的情况只有一个线程能修改这个状态,如果其他线程修改了,就不改了,否则重复唤醒了,浪费资源
                        continue;            // loop to recheck cases 循环去设置状态,成功为止
                    unparkSuccessor(h);//如果设置成功就唤醒后续的结点
                }
                else if (ws == 0 &&
                         !h.compareAndSetWaitStatus(0, Node.PROPAGATE))//如果在设置为0后,不能再设置为PROPAGATE,就继续,直到能设置为止,因为最后一个头结点状态是0,为了要继续传播唤醒,所以要留下状态,也就是设置为PROPAGATE,设置成功后最后一次循环就可以跳出循环了
                    continue;                // loop on failed CAS 循环设置,成功为止
            }
            if (h == head)                   // loop if head changed
                break;//头结点没有变化就退出,否则继续
        }
    }

基本一些思路理解了,但是这可能是冰山一角啊,毕竟大神写的东西,而且是多线程的,很多情况要考虑,还得慢慢看,多想想,不是一蹴而就的,或许我也只是分析了不到50%的情况,感觉多线程确实是比较难的一块。
比如简单的可能是这样:
在这里插入图片描述
也可能这样,多线程一起唤醒嘛:
在这里插入图片描述

countDown

主要先尝试释放,锁状态改了才去释放共享的线程doReleaseShared

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

这里保证了能够将锁释放完,CAS保证的原子性,只有有状态释放完了才返回true,如果已经释放完了就返回false,也就是只保证一次真的释放,否则什么都不做:

        //释放锁
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c - 1;
                if (compareAndSetState(c, nextc))//最终改为0才返回true,否则为false
                    return nextc == 0;
            }
        }

Semaphore

其实他内部和ReentrantLock很像,也有公平和非公平区分,默认也是非公平的:
在这里插入图片描述

简单例子

用了一个信号量,只有2个许可,但是有5个线程需要,所以应该是2个2个1个的情况执行:

public class SemaphoreTest {
    //就给2个通道
    private static Semaphore semaphore = new Semaphore(2);
    public static void main(String[] args) {
        //5个线程抢
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {

                try {
                    semaphore.acquire();
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName()+"执行");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }, "线程"+i).start();
        }
   }  
}

FairSync和NonfairSync

这个代码比较简单,看过ReentrantLock之后这个就很好理解了,我就不多说了,比如:
在这里插入图片描述
在这里插入图片描述
所以我们知道remaining>=0才算是成功。

acquire

我们来讲下常用的几个方法,其他都差不多acquire
一看就是可以中断的,会抛出异常,跟CountDownLatchawait一样

  public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

调用AQS内部的,跟CountDownLatch一样:

   public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)//尝试获取共享状态如果是0的话就中断了
            doAcquireSharedInterruptibly(arg);
    }
tryAcquireShared

内部调用了非公平锁的nonfairTryAcquireShared,CAS保证了状态的原子操作,返回状态<0获取失败,成功可能是0或者>0,而CountDownLatch成功一定是1

     //非公平直接获取
        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

如果条件不满足就调用doAcquireSharedInterruptibly,这个在文章前面已经讲过,就不说了。

release
  public void release() {
        sync.releaseShared(1);
    }

内部有调用了和CountDownLatchcountDown一样的方法,只是tryReleaseShared实现不同:

//增加一定数量
        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;
            }
        }

其他一些方法就是超时,不可被中断,还有获取一些信息的都比较好理解,我就不多说了。注意里面有acquire(int permits)这种带参数的,明显就是可以一次新申请多个资源,当然你释放的时候也要释放多个release(int permits),否则就可能死锁啦。

PROPAGATE的作用

其实一直没搞明白PROPAGATE状态到底是怎么用的,后来看了一篇大神的文章才豁然开朗。原来是因为早期出的BUG,后来才引入了这个状态:
在这里插入图片描述
左边是早期的,没有对头结点进行验证会有bug,随后才添加了头结点验证,以及到现在有新老头结点验证。我们先来看看那个时候到底是什么情况引起的bug。简单来说,就是现在有2个线程A,B阻塞在,有两个线程C,D将要去唤醒他们,理论上是肯定可以的,但是一种特殊情况就会出BUG。
我还是画下图比较好理解,刚开始的情况:
在这里插入图片描述
然后C线程来释放,锁状态+1,发现头结点状态是SIGNAL,唤醒后继,把A唤醒:
在这里插入图片描述
然后D线程来释放,发现头结点没变,状态是0,于是就退出了,此时C线程也发现头结点没变,退出了,而此时就剩线程A,当它刚好运行到了propagate > 0 发现自己的propagate = 0 ,而那时候条件是:
在这里插入图片描述
这下没的进doReleaseShared了,后续结点也就不释放了:
在这里插入图片描述
上面的三个过程就使得2个线程释放资源,2个线程用资源,最后只唤醒了一个,所以后来又补了一个PROPAGATE状态,配合这个:
在这里插入图片描述
在这里插入图片描述
遇到刚才那种情况,线程D就可以把头结点状态从0改成PROPAGATE状态,而且如果失败会自旋,只要头结点没变,就一定能设置成功。但是发现后续的版本居然又补不了两个条件:
在这里插入图片描述
我猜也应该是有类似的情况,会发现老头结点状态也是0,然后这个时候只能判断新头结点了,不然又不满足条件。有兴趣可以去看看作者的更新BUG记录,应该可以想象出

总结

本篇主要讲了下共享锁的原理,简单来说就是一旦第一个线程被唤醒了,那么共享锁里的所有共享结点都会被唤醒,而且可以是一种多线程流水线的方式,当然里面还是有很多细节值得思考的。还讲解了PROPAGATE状态的作用,这个还真的花点时间理解理解,不过首先得理解共享锁的一些机制啦。

CountDownLatch可以被用于一组线程等待另外一组线程完成任务后继续执行任务,而且是等待的线程会被一并唤醒并发执行,这里才是共享的体现。比如说要做查询汇总的功能,主线程阻塞,开10个线程去查询,全部完成后唤醒主线程,再进行汇总。

Semaphore可以用于限流,比如我就两个通道,5个线程去抢,每个线程需要一个通道,那就是同时只能有2个线程在工作,其他的等待,直到释放后继续抢。比如一个线程池,只有10个线程在工作,如果来了20个请求,那就另外10个等待了,这样就限流了,作为一种保护机制。当然可能还有比较特殊的情况,比如来了一个很紧急的请求,他希望可以尽快的计算完成,希望CPU就给他服务,这样他可以直接就申请所有现有的资源,不让其他线程拿资源了,尽量保证自己现在可以获得尽可能多的资源。

CountDownLatch可以把共享的结点全放出来,而Semaphore只能放有限个。

参考

https://www.cnblogs.com/micrari/p/6937995.html

好了,今天就到这里了,希望对学习理解有帮助,大神看见勿喷,仅为自己的学习理解,能力有限,请多包涵。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值