AQS实现之CountDownLatch/Semaphore/CyclicBarrier

≡放荡痞女 提交于 2019-11-30 12:38:47

1 CountDownLatch

  • CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。

  • CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。调用该类await()方法的线程会一直处于阻塞状态,直到其他线程调用countDown()方法使当前计数器的值逐渐减少,到0为止,每次调用countDown计数器的值减1。

  • 当计数器值减至零时,所有因调用await()方法而处于等待状态的线程就会继续往下执行。这种现象只会出现一次,因为计数器不能被重置,如果业务上需要一个可以重置计数次数的版本,可以考虑使用CycliBarrier。

  • CountDownLatch实现的是AQS的共享锁机制。

  • CountDownLatch出现以前,类似功能我们使用线程的join()方法实现。

1.1 重要方法

1.1.1 构造器

    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
    
    Sync(int count) {
        setState(count);
    }

其实CountDownLatch的构造器很简单,count入参表示计数器的次数,CountDownLatch将其赋给了state字段,使用AQS的状态值来表示计数器值。

1.1.2 await()

当前线程调用了CountDownLatch对象的await方法后,当前线程会被阻塞,直到下面的情况之一才会返回:

  • 当所有线程都调用了CountDownLatch对象的countDown方法后,也就是说计时器值为 0 的时候。
  • 其他线程调用了当前线程的interrupt()方法中断了当前线程,当前线程会抛出InterruptedException异常后返回。
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);//这个1,其实在CountDownLatch里面没有用
    }

    //AQS的获取共享资源时候可被中断的方法
    public final void acquireSharedInterruptibly(int arg)throws InterruptedException {
    //如果线程被中断则抛异常
    if (Thread.interrupted())
         throw new InterruptedException();
        //尝试看当前是否计数值为0,为0则直接返回,否则进入AQS的队列等待
    if (tryAcquireShared(arg) < 0)
         doAcquireSharedInterruptibly(arg);//该方法前文已经论述,详见2.4.2.2 AQS.doAcquireSharedInterruptibly()
    }

     //CountDownLatch.sync类实现的AQS的接口
     protected int tryAcquireShared(int acquires) {
         return (getState() == 0) ? 1 : -1;
     }

tryAcquireShared方法是CountDownLatch.sync类实现AQS的接口,只判断了getState()是否等于0,这是计数器有别于其他传统共享锁的核心

1.1.3 await(long timeout, TimeUnit unit)

当线程调用了 CountDownLatch 对象的await(long timeout, TimeUnit unit)方法后,当前线程会被阻塞,直到下面的情况之一发生才会返回:

  • 当所有线程都调用了 CountDownLatch 对象的 countDown 方法后,也就是计时器值为 0 的时候,这时候返回 true
  • 设置的 timeout 时间到了,因为超时而返回 false;
  • 其它线程调用了当前线程的 interrupt()方法中断了当前线程,当前线程会抛出 InterruptedException 异常后返回。

也就是相比于await,引入了一个timeout的概念

public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

1.1.4 countDown()

当前线程调用了该方法后,会递减计数器的值,递减后如果计数器为0则会唤醒所有调用await方法而被阻塞的线程,否则什么都不做,接下来看一下countDown()方法内部是如何调用AQS的方法的,源码如下:

    public void countDown() {
        sync.releaseShared(1);//委托sync调用AQS的方法
    }


    //AQS的方法
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();//AQS的释放资源方法,会唤醒head的后继
            //后继争锁成功再唤醒后继,使得所有挂起的线程都被唤醒。详见2.4.4.3 AQS.doReleaseShared()
            return true;
        }
        return false;
    }

    protected boolean tryReleaseShared(int releases) {
       // Decrement count; signal when transition to zero
       //循环进行cas,直到当前线程成功完成cas使计数值(状态值state)减一并更新到state
       for (;;) {
           int c = getState();
           if (c == 0)//如果当前状态值为0则直接返回返回false,
                return false;//返回false就不用调用doReleaseShared()了
                //这里的if (c == 0)貌似是多余的,其实不然,之所以添加if (c == 0)是为了防止计数器值为 0 后,其他线程又调用了countDown方法,如果没有这里,状态值就会变成负数。
				
				
           int nextc = c-1;//否则,state-1
           if (compareAndSetState(c, nextc))
               return nextc == 0;//这里如果返回true,说明当前线程是最后一个调用countDown()方法的线程
               //那么该线程除了让计数器减一外,还需要唤醒调用CountDownLatch的await方法而被阻塞的线程。
               //所以它返回true,tryReleaseShared中则会调用doReleaseShared,唤醒其他节点。
        }
    }

1.2 使用demo

package com.lscherish;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class CountDownLatchTest {

    private static AtomicInteger id = new AtomicInteger();

    // 创建一个CountDownLatch实例,管理计数为ThreadNum
    private static volatile CountDownLatch countDownLatch = new CountDownLatch(3);

    public static void main(String[] args) throws InterruptedException {

        Thread threadOne = new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                System.out.println("【玩家" + id.getAndIncrement() + "】已入场");
                countDownLatch.countDown();
            }
        });

        Thread threadTwo = new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                System.out.println("【玩家" + id.getAndIncrement() + "】已入场");
                countDownLatch.countDown();

            }
        });

        Thread threadThree = new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                System.out.println("【玩家" + id.getAndIncrement() + "】已入场");
                countDownLatch.countDown();

            }
        });

        // 启动子线程
        threadOne.start();
        threadTwo.start();
        threadThree.start();
        System.out.println("等待斗地主玩家进场");

        // 等待子线程执行完毕,返回
        countDownLatch.await();

        System.out.println("斗地主玩家已经满人,开始发牌.....");

    }
}

运行结果

等待斗地主玩家进场
【玩家0】已入场
【玩家1】已入场
【玩家2】已入场
斗地主玩家已经满人,开始发牌.....

1.3 与join()相比

CountDownLatch 与 join 方法的区别,一个是调用一个子线程的 join()方法后,该线程会一直被阻塞直到该线程运行完毕,而 CountDownLatch 则使用计数器允许子线程运行完毕或者运行中时候递减计数,也就是 CountDownLatch 可以在子线程运行任何时候让 await 方法返回而不一定必须等到线程结束;另外使用线程池来管理线程时候一般都是直接添加 Runable 到线程池这时候就没有办法在调用线程的 join 方法了,countDownLatch 相比 Join 方法让我们对线程同步有更灵活的控制。

2 Semaphore

Semaphore也叫信号量,在JDK1.5被引入,用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。还可以用来实现某种资源池,或者对容器施加边界。

打个比喻,Semaphore就像一道阀门,可以控制同时进入某一逻辑的线程数量(构造方法中指定),我们使用acquire方法来争取通行票,使用release方法来归还通行票。通行票只是一个比喻,一般我们称之为许可。

  • Semaphore内部维护了一组虚拟的许可,许可的数量可以通过构造函数的参数指定。
    • public Semaphore(int permits) { sync = new NonfairSync(permits); }
  • 访问特定资源前,必须使用acquire方法获得许可,如果许可数量为0,该线程则一直阻塞,直到有可用许可。
  • 访问资源后,使用release释放许可。
  • Semaphore和ReentrantLock类似,获取许可有公平策略和非公平许可策略,默认情况下使用非公平策略。
    • public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }
  • 当初始值为1时,可以用作互斥锁,并具备不可重入的加锁语义。
  • Semaphore将AQS的同步状态(status字段)用于保存当前可用许可的数量。

我们调用Semaphore方法时,其实是在间接调用其内部类或AQS方法执行的。Semaphore类结构与ReetrantLock类相似,内部类Sync继承自AQS,然后其子类FairSync和NoFairSync分别实现公平锁和非公平锁的获取锁方法tryAcquireShared(int arg),而释放锁的tryReleaseShared(int arg)方法则有Sync类实现,因为非公平或公平锁的释放过程都是相同的。

2.1 重要方法

Semaphore在JAVA并发之AQS详解2.4节中有过描述,不论是其公平锁实现还是非公平锁实现,故本文不再赘述,欲了解源码可以阅读JAVA并发之AQS详解2.4节

2.2 使用demo

场景:老师需要4个学生到讲台上填写一张表,但是老师只有2支笔,因此同一时刻只能保证2个学生拿到笔进行填写,没有拿到笔的学生只能等前面的学生填写完毕,再去拿笔进行填写。

package com.lscherish;

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

public class SemaphoreDemo {
    // 2支笔
    private static Semaphore semaphore = new Semaphore(2, true);

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(5);
        // 5个学生
        for (int i = 1; i <=5; i++) {
            service.execute(() -> {
                try {
                    System.out.println("同学"+Thread.currentThread().getId() + "想要拿到笔===");
                    semaphore.acquire();
                    System.out.println("同学"+Thread.currentThread().getId() + "拿到笔---");
                    System.out.println("同学"+Thread.currentThread().getId() + "填写中...");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println("同学"+Thread.currentThread().getId() + "填写完毕,马上归还笔。。。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            });
        }
        service.shutdown();
    }
}

得到结果

同学10想要拿到笔===
同学10拿到笔---
同学10填写中...
同学11想要拿到笔===
同学11拿到笔---
同学11填写中...
同学12想要拿到笔===
同学13想要拿到笔===
同学14想要拿到笔===
同学10填写完毕,马上归还笔。。。
同学11填写完毕,马上归还笔。。。
同学13拿到笔---
同学13填写中...
同学12拿到笔---
同学12填写中...
同学13填写完毕,马上归还笔。。。
同学14拿到笔---
同学14填写中...
同学12填写完毕,马上归还笔。。。
同学14填写完毕,马上归还笔。。。

3 CyclicBarrier

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!