并发编程从零开始(十)-同步工具类

并发编程从零开始(十)-同步工具类

6 同步工具类

6.1 Semaphore

Semaphore也就是信号量,提供了资源数量的并发访问控制,其使用代码很简单,如下所示:

image-20211028133157001

image-20211028133207255

有参方法tryAcquire(long timeout, TimeUnit unit)的作用是在指定的时间内尝试地获取1个许可,如果获取不到就返回false。

可以使用Semphore实现一个简易的抢座:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 5; i++) {
            Thread.sleep(500);
            new MySemphore(semaphore).start();
        }

    }
}


public class MySemphore extends Thread{
    private Semaphore semaphore;

    public MySemphore(Semaphore semaphore){
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+": working");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName()+": releasing");
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

如下图所示,假设有n个线程来获取Semaphore里面的10份资源(n > 10),n个线程中只有10个线程能获取到,其他线程都会阻塞。直到有线程释放了资源,其他线程才能获取到。

image-20211028134800801

当初始的资源个数为1的时候,Semaphore退化为排他锁。正因为如此,Semaphone的实现原理和锁十分类似,是基于AQS,有公平和非公平之分。Semaphore相关类的继承体系如下图所示:

image-20211028134908195

由于Semaphore和锁的实现原理基本相同。资源总数即state的初始值,在acquire里对state变量进行CAS减操作,减到0之后,线程阻塞;在release里对state变量进行CAS加操作。

image-20211028135204155

image-20211028135215811

image-20211028135243422

image-20211028135257878


6.2 CountDownLatch

6.2.1 CountDownLatch使用场景

假设一个主线程要等待5个 Worker 线程执行完才能退出,可以使用CountDownLatch来实现:

线程:

public class MyThread extends Thread{
    private final CountDownLatch countDownLatch;
    private final Random random = new Random();

    public MyThread(String name ,CountDownLatch countDownLatch){
        super(name);
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(random.nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 运行结束");
        countDownLatch.countDown();
    }
}

Main类:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            new MyThread("THREAD"+i,countDownLatch).start();
        }
        //当前线程等待
        countDownLatch.await();
        System.out.println("程序运行结束");
    }
}

下图为CountDownLatch相关类的继承层次,CountDownLatch原理和Semaphore原理类似,同样是基于AQS,不过没有公平和非公平之分。

image-20211028144540727

6.2.2 await()实现分析

await()调用的是AQS 的模板方法,CountDownLatch.Sync重新实现了tryAccuqireShared方法。

image-20211028144853968

从tryAcquireShared(...)方法的实现来看,只要state != 0,调用await()方法的线程便会被放入AQS的阻塞队列,进入阻塞状态。

6.2.3 countDown()实现分析

image-20211028145152483

image-20211028145201450

countDown()调用的AQS的模板方法releaseShared(),里面的tryReleaseShared(...)由CountDownLatch.Sync实现。从上面的代码可以看出,通过CAS减少state的值,只有state=0,tryReleaseShared(...)才会返回true,然后执行doReleaseShared(...),一次性唤醒队列中所有阻塞的线程。

总结:由于是基于AQS阻塞队列来实现的,所以可以让多个线程都阻塞在state=0条件上,通过countDown()一直减state,减到0后一次性唤醒所有线程。如下图所示,假设初始总数为MN个线程await(),M个线程countDown(),减到0之后,N个线程被唤醒。

image-20211028145238592


6.3 CyclicBarrier-循环屏障

6.3.1 CyclicBarrier 使用场景

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做 的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一 个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

CyclicBarrier使用方式比较简单:

//创建CyclicBarrier
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                // 当所有线程被唤醒时,执行Runnable。
                System.out.println("do something");
            }
        });
        
//线程进入到屏障进行阻塞
cyclicBarrier.await();

实现阶段运行:

Main:

public class Main {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                System.out.println("finish");
            }
        });
        for (int i = 0; i < 3; i++) {
            new MyThread(cyclicBarrier).start();
        }
    }
}

MyThread:

public class MyThread extends Thread{
    private final CyclicBarrier cyclicBarrier;
    private final Random random = new Random();

    public MyThread(CyclicBarrier cyclicBarrier){
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(random.nextInt(2000));
            System.out.println(Thread.currentThread().getName()+" A begin");
            cyclicBarrier.await();
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+" B begin");
            cyclicBarrier.await();
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+" C begin");
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

在整个过程中,有2个同步点。只有所有的线程全部到达了同步点之后,最后一个进入的线程将唤醒所有被阻塞的线程。

6.3.2 CyclicBarrier实现原理

CyclicBarrier基于ReentrantLock+Condition实现

image-20211028154116894

//该内部类用于表明当前循环屏障的状态,当broken为true时表示障碍器发生了异常
    private static class Generation {
        boolean broken = false;
    }
    //CyclicBarrier内部的显示锁
    private final ReentrantLock lock = new ReentrantLock();
    //通过上面的显式锁得到的Condition变量,障碍器能够阻塞和唤醒多个线程完全得益于这个Condition
    private final Condition trip = lock.newCondition();
    //临界值,当障碍器阻塞的线程数等于parties时即count=0,障碍器将会通过trip唤醒目前所有阻塞的线程
    private final int parties;
    //条件线程,当屏障被打破时,在障碍器通过trip唤醒所有正被阻塞的的线程之前,执行该线程,这个线程可以充当一个主线程,那些被阻塞的线程可以充当子线程,即可以实现当所有子线程都达到屏障时调用主线程的作用
    private final Runnable barrierCommand;
    //内部类Generation变量表示当前循环屏障CyclicBarrier的状态
    private Generation generation = new Generation();
    //计数器,用于计算还剩多少个线程还没有达到屏障处,初始值应该等于临界值parties
    private int count;

构造方法:

image-20211028154427020

await()方法实现过程:

    //timed:表示是否设置了等待时间
    //nanos等待的时间(纳秒)
    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        //使用CyclicBarrier定义的显示锁,加锁避免并发问题
        lock.lock();
        try {
            //当前循环屏障的状态
            final Generation g = generation;
            //如果为true,表示障碍器之前发生了异常,抛出异常BrokenBarrierException
            if (g.broken)
                throw new BrokenBarrierException();
            //当前线程是否被中断
            if (Thread.interrupted()) {
                breakBarrier();//该方法会重置计数值count为parties,并且唤醒所有被阻塞的线程,并改变状态Generation
                throw new InterruptedException();
            }
            //屏障计数器减一
            int index = --count;
            //如果index等于0 ,达到屏障的线程的数量等于最开始设置的数量parties
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    //如果条件线程不为空,则执行条件线程
                    if (command != null)
                        command.run();
                    ranAction = true;
                    //唤醒所有被阻塞的线程,并且重置计数器count,生成新的状态generation
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)//如果ranAction为true,表示上面的代码没有顺利执行结束,表示障碍器发生了异常,调用breakBarrier重置计数器,并设置generation.broken=true表示当前的状态
                        breakBarrier();
                }
            }
 
            // 当计数器为零调用了Condition的唤醒方法、或者broken为true、或者线程中断、或者等待超时时跳出异常
            for (;;) {
                try {
                    //阻塞当前线程,如果timed为false表示没有设置等待的时间
                    if (!timed)
                        //不限时阻塞线程,只有当调用唤醒方法后才会继续执行
                        trip.await();
                    else if (nanos > 0L)
                        //等待nanos毫秒
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    //调用await方法如果发生异常,并且此时CyclicBarrier还没有调用nextGeneration()方法重置计数器和generation
                    if (g == generation && ! g.broken) {
                        breakBarrier();//该方法会唤醒所有阻塞的线程,并且重置计数器,而且设置generation.broken = true表示障碍器发生了异常。
                        throw ie;
                    } else {
                        //中断当前线程
                        Thread.currentThread().interrupt();
                    }
                }
                //g.broken为true,表示障碍器发生了异常,抛出异常
                if (g.broken)
                    throw new BrokenBarrierException();
                //index=0的唤醒操作顺利执行完了,所以通过nextGeneration()方法更新了generation,而由于generation是线程中的共享变量,所以当前线程此时 g!=generation
                if (g != generation)
                    return index;
                //如果timed为true表示设置了线程阻塞的时间,然后时间nanos却小于等于0,
                if (timed && nanos <= 0L) {
                    breakBarrier();//此时重置计数器,并且设置generation.broken=true表示障碍器发生异常
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }
 
    //唤醒所有线程,重置计数器count,重新生成generation
    private void nextGeneration() {
        trip.signalAll();
        count = parties;
        generation = new Generation();
    }
 
    //设置generation.broken=true表示障碍器发生的异常,重置计数器count,唤醒所有阻塞的线程
    private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }

关于上面的方法,有几点说明:

  1. CyclicBarrier是可以被重用的。以应聘场景为例,来了10个线程,这10个线程互相等待,到齐后一起被唤醒,各自执行接下来的逻辑;然后,这10个线程继续互相等待,到齐后再一起被唤醒。每一轮被称为一个Generation,就是一次同步点。

  2. CyclicBarrier 会响应中断。10 个线程没有到齐,如果有线程收到了中断信号,所有阻塞的线程也会被唤醒,就是上面的breakBarrier()方法。然后count被重置为初始值(parties),重新开始。

  3. breakBarrier()只会被第10个线程执行1次(在唤醒其他9个线程之前),而不是10个线程每个都执行1次。


6.4 Exchanger

6.4.1 使用场景

Exchanger用于线程之间交换数据,其使用代码很简单,是一个exchange(...)方法,使用示例如下:

public class Main {
    private static final Random random = new Random();
    public static void main(String[] args) {
        // 建一个多线程共用的exchange对象
        // 把exchange对象传给3个线程对象。每个线程在自己的run方法中调用exchange,把自己的数据作为参数
        // 传递进去,返回值是另外一个线程调用exchange传进去的参数
        Exchanger<String> exchanger = new Exchanger<>();
        new Thread("线程1") {
            @Override
            public void run() {
                while (true) {
                    try {
                        // 如果没有其他线程调用exchange,线程阻塞,直到有其他线程调 用exchange为止。
                        String otherData = exchanger.exchange("交换数据1");
                        System.out.println(Thread.currentThread().getName() + "得到<==" + otherData);
                        Thread.sleep(random.nextInt(2000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread("线程2") {
            @Override
            public void run() {
                while (true) {
                    try {
                        String otherData = exchanger.exchange("交换数据2");
                        System.out.println(Thread.currentThread().getName() + "得到<==" + otherData);
                        Thread.sleep(random.nextInt(2000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread("线程3") {
            @Override
            public void run() {
                while (true) {
                    try {
                        String otherData = exchanger.exchange("交换数据3");
                        System.out.println(Thread.currentThread().getName() + "得到<==" + otherData);
                        Thread.sleep(random.nextInt(2000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }   
}

在上面的例子中,3个线程并发地调用exchange(...),会两两交互数据,如1/2、1/3和2/3。

6.4.2 实现原理

Exchanger的核心机制和Lock一样,也是CAS+park/unpark。

park/unpark推荐阅读:https://www.cnblogs.com/set-cookie/p/9582547.html

首先,在Exchanger内部,有两个内部类:Participant和Node,代码如下:

image-20211028162944278

每个线程在调用exchange(...)方法交换数据的时候,会先创建一个Node对象。

这个Node对象就是对该线程的包装,里面包含了3个重要字段:第一个是该线程要交互的数据,第二个是对方线程交换来的数据,最后一个是该线程自身。

一个Node只能支持2个线程之间交换数据,要实现多个线程并行地交换数据,需要多个Node,因此在Exchanger里面定义了Node数组:

private volatile Node[] arena;
6.4.3 exchange( V x )实现分析

image-20211028164435417

一对一交换数据使用slotExchange,其余情况使用arenaExchange。

上面方法中,如果arena不是null,表示启用了arena方式交换数据。如果arena不是null,并且线程被中断,则抛异常。

如果arena不是null,但是arenaExchange的返回值为null,则抛异常。对方线程交换来的null值是封装为NULL_ITEM对象的,而不是null。

如果slotExchange的返回值是null,并且线程被中断,则抛异常。

如果slotExchange的返回值是null,并且areaExchange的返回值是null,则抛异常。

slotExchange的实现:

image-20211028164710351

image-20211028164851557

image-20211028164929565

arenaExchange的实现:

image-20211028165207814

image-20211028165233217

image-20211028165308955

image-20211028165326872


6.5 Phaser-移相器

6.5.1 用Phaser代替CyclicBarrier 和 CountDownLatch

从JDK7开始,新增了一个同步工具类Phaser,其功能比CyclicBarrier和CountDownLatch更加强大。

CyclicBarrier解决了CountDownLatch不能重用的问题,但是仍有以下不足:

1)不能动态调整计数器值,假如线程数不足以打破barrier,就只能reset或者多加些线程,在实际运用中显然不现实

2)每次await仅消耗1个计数器值,不够灵活

Phaser就是用来解决这些问题的。Phaser将多个线程协作执行的任务划分为多个阶段,每个阶段都可以有任意个参与者,线程可以随时注册并参与到某个阶段。

代替CountDownLatch:

public class PhaserInsteadOfCountDownLatch {
    public static void main(String[] args) {
        Phaser phaser = new Phaser(5);
        for (int i = 0; i < 5; i++) {
            new Thread("thread-"+(i+1)){
                private final Random random = new Random();

                @Override
                public void run() {
                    System.out.println(getName()+" start");
                    try {
                        Thread.sleep(random.nextInt(1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName()+" end");
                    phaser.arrive();
                }
            }.start();
        }
        System.out.println("threads start finish");
        phaser.awaitAdvance(phaser.getPhase());
        System.out.println("threads end finish");
    }
}

代替CyclicBarrier:

Main:

public class PhaserInsteadOfCyclicBarrier {
    public static void main(String[] args) {
        Phaser phaser = new Phaser(5);
        for (int i = 0; i < 5; i++) {
            new MyThread(phaser).start();
        }
        phaser.awaitAdvance(0);
    }
}

MyThread:

public class MyThread extends Thread{
    private final Phaser phaser;
    private final Random random = new Random();

    public MyThread(Phaser phaser){
        this.phaser = phaser;
    }

    @Override
    public void run() {
        try {
            System.out.println("start a");
            Thread.sleep(500);
            System.out.println("end a");
            phaser.arriveAndAwaitAdvance();
            System.out.println("start b");
            Thread.sleep(500);
            System.out.println("end b");
            phaser.arriveAndAwaitAdvance();
            System.out.println("start c");
            Thread.sleep(500);
            System.out.println("end c");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

arriveAndAwaitAdance()就是 arrive()与 awaitAdvance(int)的组合,表示“我自己已到达这个同步点,同时要等待所有人都到达这个同步点,然后再一起前行”。

6.5.2 Phaser新特性
  1. 动态调整线程个数

    CyclicBarrier 所要同步的线程个数是在构造方法中指定的,之后不能更改,而 Phaser 可以在运行期间动态地调整要同步的线程个数。Phaser 提供了下面这些方法来增加、减少所要同步的线程个数。

    image-20211028180827562

  2. 层次Phaser

    多个Phaser可以组成如下图所示的树状结构,可以通过在构造方法中传入父Phaser来实现。

    public Phaser(Phaser parent, int parties){
    //....
    }
    

    image-20211028231442729

    通过parent节点来存储树状结构:

    private final Phaser parent;
    

可以发现,在Phaser的内部结构中,每个Phaser记录了自己的父节点,但并没有记录自己的子节点列表。所以,每个 Phaser 知道自己的父节点是谁,但父节点并不知道自己有多少个子节点,对父节点的操作,是通过子节点来实现的。

树状的Phaser怎么使用呢?考虑如下代码,会组成下图的树状Phaser。

Phaser root = new Phaser(2);
Phaser c1 = new Phaser(root,3);
Phaser c2 = new Phaser(root,2);
Phaser c3 = new Phaser(c1,0);

image-20211028231751510

本来root有两个参与者,然后为其加入了两个子Phaser(c1,c2),每个子Phaser会算作1个参与者,root的参与者就变成2+2=4个。c1本来有3个参与者,为其加入了一个子Phaser c3,参与者数量变成3+1=4个。c3的参与者初始为0,后续可以通过调用register()方法加入。

对于树状Phaser上的每个节点来说,可以当作一个独立的Phaser来看待,其运作机制和一个单独的Phaser是一样的。

父Phaser并不用感知子Phaser的存在,当子Phaser中注册的参与者数量大于0时,会把自己向父节点注册;当子Phaser中注册的参与者数量等于0时,会自动向父节点解除注册。父Phaser把子Phaser当作一个正常参与的线程就即可。

6.5.3 state变量解析

大致了解了Phaser的用法和新特性之后,下面仔细剖析其实现原理。Phaser没有基于AQS来实现,但具备AQS的核心特性:state变量、CAS操作、阻塞队列。先从state变量说起。

private volatile long state;

这个64位的state变量被拆成4部分:

image-20211028232022536

Phaser提供了一系列的成员方法来从state中获取上图中的几个数字。下面再看一下state变量在构造方法中是如何被赋值的:

image-20211028232156994

其中,已经定义了:

private static final int EMPTY = 1;
private static final int PHASE_SHIFT = 32;
private static final int PARTIES_SHIFT = 16;

当parties=0时,state被赋予一个EMPTY常量,常量为1;

当parties != 0时,把phase值左移32位;把parties左移16位;然后parties也作为最低的16位,3个值做或操作,赋值给state。

6.5.4 阻塞与唤醒(Treiber Stack)

基于上述的state变量,对其执行CAS操作,并进行相应的阻塞与唤醒。如下图所示,右边的主线程会调用awaitAdvance()进行阻塞;左边的arrive()会对state进行CAS的累减操作,当未到达的线程数减到0时,唤醒右边阻塞的主线程。

image-20211028232508510

在这里,阻塞使用的是一个称为Treiber Stack的数据结构,而不是AQS的双向链表。Treiber Stack是一个无锁的栈,它是一个单向链表,出栈、入栈都在链表头部,所以只需要一个head指针,而不需要tail指针,如下的实现:

image-20211028232606767

image-20211028232618505

为了减少并发冲突,这里定义了2个链表,也就是2个Treiber Stack。当phase为奇数轮的时候,阻塞线程放在oddQ里面;当phase为偶数轮的时候,阻塞线程放在evenQ里面。代码如下所示:

image-20211028232706401

6.5.5 arrive() 方法分析

下面看arrive()方法是如何对state变量进行操作,又是如何唤醒线程的。

其中,定义了变量:

private static final int ONE_ARRIVAL = 1;
private static final int ONE_PARTY = 1 << PARTIES_SHIFT;
private static final int ONE_DEREGISTER = ONE_ARRIVAL\ONE_PARTY;
private static final int PARTIES_SHIFT = 16;

arrive()和 arriveAndDeregister()内部调用的都是 doArrive(boolean)方法。

区别在于前者只是把“未达到线程数”减1;后者则把“未到达线程数”和“下一轮的总线程数”都减1。下面看一下doArrive(boolean)方法的实现。

image-20211028233015433

image-20211028233123408

关于上面的方法,有以下几点说明:

  1. 定义了2个常量如下。当 deregister=false 时,只最低的16位需要减 1,adj=ONE_ARRIVAL;当deregister=true时,低32位中的2个16位都需要减1,adj=ONE_ARRIVAL|ONE_PARTY。
  2. 把未到达线程数减1。减了之后,如果还未到0,什么都不做,直接返回。如果到0,会做2件事情:第1,重置state,把state的未到达线程个数重置到总的注册的线程数中,同时phase加 1;第2,唤醒队列中的线程。

下面看一下唤醒方法:

image-20211028233401617

遍历整个栈,只要栈当中节点的phase不等于当前Phaser的phase,说明该节点不是当前轮的,而是前一轮的,应该被释放并唤醒。

6.5.6 awaitAdvance()方法分析

image-20211028233532517

下面的while循环中有4个分支:

初始的时候,node==null,进入第1个分支进行自旋,自旋次数满足之后,会新建一个QNode节点;

之后执行第3、第4个分支,分别把该节点入栈并阻塞。

image-20211028233705935

image-20211028233737118

这里调用了ForkJoinPool.managedBlock(ManagedBlocker blocker)方法,目的是把node对应的线程阻塞。ManagerdBlocker是ForkJoinPool里面的一个接口,定义如下:

image-20211028233800266

QNode实现了该接口,实现原理还是park(),如下所示。之所以没有直接使用park()/unpark()来实现阻塞、唤醒,而是封装了ManagedBlocker这一层,主要是出于使用上的方便考虑。一方面是park()可能被中断唤醒,另一方面是带超时时间的park(),把这二者都封装在一起。

image-20211028233900815

理解了arrive()和awaitAdvance(),arriveAndAwaitAdvance()就是二者的一个组合版本。

posted @ 2021-10-28 23:46  会编程的老六  阅读(90)  评论(0编辑  收藏  举报