Java并发系列(9)——并发工具类

接上一篇《Java并发系列(8)——并发容器

7 并发工具类

除了之前讲到的并发容器,JDK 还提供了一些方便易用的并发工具类。

7.1 Semaphore

Semaphore 主要用于控制并发量。

7.1.1 核心方法

Semaphore 的核心方法就两个:

  • acquire():取走一个 permit,成功拿到了 permit 才能继续干活,否则就阻塞直到拿到为止;
  • release():放入一个 permit,通常干完活之后把之前 acquire 的 permit 再放回去。
7.1.2 demo
package per.lvjc.concurrent.util;

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {

    //小区门口一共 2 辆共享单车
    private static Semaphore bicycles = new Semaphore(2);

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            String name = Thread.currentThread().getName();
            System.out.println(name + ": 社畜的一天开始了");
            timeFlows();
            boolean findBicycle = bicycles.tryAcquire();
            if (findBicycle) {
                System.out.println(name + ": 竟然还有一辆共享单车,骑车上班");
                timeFlows();
                System.out.println(name + ": 下班还车");
                bicycles.release();
            } else {
                System.out.println(name + ": 没车了,走路上班吧");
            }
            System.out.println(name + ": 社畜的一天结束了");
        };
        for (int i = 1; i <= 6; i++) {
            System.out.println("------ 星期 " + i + " ------");
            new Thread(runnable, "Jack").start();
            new Thread(runnable, "Rose").start();
            new Thread(runnable, "Nobody").start();
            Thread.sleep(500);
        }
    }

    private static void timeFlows() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

------ 星期 1 ------
Jack: 社畜的一天开始了
Rose: 社畜的一天开始了
Nobody: 社畜的一天开始了
Jack: 竟然还有一辆共享单车,骑车上班
Rose: 竟然还有一辆共享单车,骑车上班
Nobody: 没车了,走路上班吧
Nobody: 社畜的一天结束了
Jack: 下班还车
Rose: 下班还车
Jack: 社畜的一天结束了
Rose: 社畜的一天结束了
------ 星期 2 ------
Jack: 社畜的一天开始了
Rose: 社畜的一天开始了
Nobody: 社畜的一天开始了
Jack: 竟然还有一辆共享单车,骑车上班
Rose: 竟然还有一辆共享单车,骑车上班
Nobody: 没车了,走路上班吧
Nobody: 社畜的一天结束了
Jack: 下班还车
Jack: 社畜的一天结束了
Rose: 下班还车
Rose: 社畜的一天结束了
------ 星期 3 ------
(省略......)

这里,通过 tryAcquire() 方法尝试骑走一辆共享单车,不阻塞,有就返回 true,否则返回 false;

因为 Semaphore 设置了只有 2,所以只有 2 个人可以骑车,剩下一个人得走路;

因为骑走车的人又还回来了,所以第二天又有 2 辆车可以骑。

7.1.3 注意点
  • 构造方法设的参数仅仅是初始值;
  • Semaphore 跟 Lock 不一样,Lock 在释放锁之前必须获得锁,Semaphore 可以不 acquire 直接 release;
  • fair 与 unfair 这点与 Lock 是一样的;
  • Semaphore 有个 acquireUninterruptibly 方法,acquire 时如果获取不到则阻塞并且不可被打断,但别真的没事就去 interrupt 一下,线程会从阻塞中被唤醒,然后发现还是 acquire 不到,又重新阻塞,损伤效率的。
7.1.4 手写

相比前面已经讲过的 AQS 和 ConcurrentHashMap 来说,Semaphore 算是非常简单的了,所以我们来自己实现一个 Semaphore。

先明确核心功能:

  • acquire:
    • 获取一个(或多个)permit——需要一个变量来存储当前 permit 数量,并且要保证并发可见性;
    • 取走 permit 之后,修改剩余 permit 数量——并发修改要保证原子性;
    • 如果 permit 数量不足,当前线程要阻塞——等待和通知;
  • release:
    • 放入一个(或多个)permit;
    • 修改 permit 数量;
    • 如果有线程在等待,将其唤醒。

实现方案:

  • 保证可见性:
    • synchronized;
    • volatile;
  • 保证原子性:
    • synchronized;
    • cas;
  • 线程的等待和通知:
    • wait / notify;
    • await / signal;
    • park / unpark(不同于上面两个,不会释放锁)。

所以实现方案有多种,这里选用 volatile + cas + park / unpark:

package per.lvjc.concurrent.util;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.LockSupport;

/**
 * @author lvjc
 * @date 2020/9/7
 */
public class LvjcSemaphore {

    /**
     * cas 操作需要 Unsafe,当然也可以用 JDK 封装好的 AtomicInteger
     */
    private static final long permitOffset;
    private static final Unsafe unsafe;
    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            permitOffset = unsafe.objectFieldOffset(LvjcSemaphore.class.getDeclaredField("permits"));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
    }

    /**
     * volatile 保证并发可见性
     */
    private volatile int permits;
    /**
     * 一个并发非阻塞容器来保存处于等待中的 Thread
     */
    private ConcurrentLinkedQueue<Thread> queue;

    public LvjcSemaphore(int permits) {
        this.permits = permits;
        this.queue = new ConcurrentLinkedQueue<>();
    }

    public void acquire() throws InterruptedException {
        //自旋
        for (;;) {
            int available = permits;
            // permit 数量不足
            if (available < 1) {
                // 当前线程进入等待队列
                queue.offer(Thread.currentThread());
                // 阻塞当前线程,
                // 不必担心其它线程已经先从队列取出当前 thread 进行了 unpark,导致下面的 park 无限阻塞,
                // 如果 unpark 发生在 park 之前,被 unpark 的线程(如果已经 start)下一次 park 不会阻塞
                // 但还是要在 park 之前再检查一次,以免在当前线程放入队列之前,其它线程已经 release,错过唤醒导致永远阻塞
                if (permits < 1) {
                    LockSupport.park();
                }
                // 醒来之后
                if (Thread.interrupted()) {
                    // 如果是被 interrupt 叫醒的,抛出 InterruptedException
                    throw new InterruptedException();
                }
            }
            // cas 修改 permit 变量 -1
            else if (casPermit(available, available - 1)) {
                // 修改成功,退出
                break;
            }
        }
    }

    public void acquireUninterruptibly() {
        for (;;) {
            int available = permits;
            if (available < 1) {
                queue.offer(Thread.currentThread());
                if (permits < 1) {
                    LockSupport.park();
                }
                if (Thread.interrupted()) {
                    //相比 acquire 方法,只在这里有区别,这里不抛异常,仅设置 interrupt 状态
                    Thread.currentThread().interrupt();
                }
            } else if (casPermit(available, available - 1)) {
                break;
            }
        }
    }

    public boolean tryAcquire() {
        for(;;) {
            int available = permits;
            if (available < 1) {
                return false;
            } else if (casPermit(available, available - 1)) {
                // cas 失败要重新进入循环,不能直接 return false
                return true;
            }
        }
    }

    public void release() {
        // 自旋
        for (;;) {
            int available = permits;
            // cas 修改 permit 数量 +1
            if (casPermit(available, available + 1)) {
                // 修改成功,因为只放入 1 个 permit,所以只叫醒 1 个线程
                Thread waitingThread = queue.poll();
                if (waitingThread != null) {
                    // 如果有线程正在等待,将其唤醒
                    LockSupport.unpark(waitingThread);
                }
                break;
            }
        }
    }

    private boolean casPermit(int expected, int permit) {
        return unsafe.compareAndSwapInt(this, permitOffset, expected, permit);
    }
}

以上,已经实现了 Semaphore 的基本功能,性能应该也还好。不过这只是一个 unfair 的 Semaphore,fair 模式大同小异。

这个实现与 JDK 没有太大区别,JDK 的 Semaphore 用的是 AQS,本质上也是 volatile + cas + park / unpark。不同的是 AQS 用的队列是 AQS 内部自己实现的,并且在何时入队和出队上面做了比较多的优化。

7.2 Exchanger

Exchanger 用于两个线程之间的数据交换。

7.2.1 核心方法

Exchanger 类的核心方法就一个:

  • exchange():提交自己的数据,阻塞,直到另一个线程提交数据,然后醒来,交换数据;或者对方线程先提交了数据,正在阻塞,自己线程提交数据,得到对方数据,对方线程被唤醒拿到自己的数据。
7.2.2 demo
package per.lvjc.concurrent.util;

import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class ExchangerDemo {

    private static Exchanger<String> exchanger = new Exchanger<>();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + ": 发起交易,等待对方同意");
            try {
                // 这里仅仅用来阻塞,无所谓对方给什么数据,等待对方调用 exchange 方法
                exchanger.exchange("");
            } catch (InterruptedException e) {
                System.out.println(threadName + ": 交易失败");
                return;
            }
            // 对方也调用 exchange 方法提交数据之后,当前线程就会被唤醒走到这里
            System.out.println(threadName + ": 对方已同意,交易开始");
            String goods = null;
            try {
                //自己提交了 “屠龙宝刀 × 1”, goods 为对方提交的数据
                goods = exchanger.exchange("屠龙宝刀 * 1", 30, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                // 打断走这里
                System.out.println(threadName + ": 交易失败");
            } catch (TimeoutException e) {
                // 超时走这里
                System.out.println(threadName + ": 交易超时,已取消");
            }
            System.out.println(threadName + ": 交易成功,获得 " + goods);
        }, "伊莉雅").start();
        Thread.sleep(200);
        new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + ": 收到交易申请");
            try {
                // exchange 方法可以提交 null,对方也会得到 null
                exchanger.exchange(null);
            } catch (InterruptedException e) {
                System.out.println(threadName + ": 交易失败");
                return;
            }
            System.out.println(threadName + ": 已接受申请,交易开始");
            String goods = null;
            try {
                goods = exchanger.exchange("2000000 金币", 30, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                System.out.println(threadName + ": 交易失败");
            } catch (TimeoutException e) {
                System.out.println(threadName + ": 交易超时,已取消");
            }
            System.out.println(threadName + ": 交易成功,获得 " + goods);
        }, "士郎").start();
    }
}

输出:

伊莉雅: 发起交易,等待对方同意
士郎: 收到交易申请
伊莉雅: 对方已同意,交易开始
士郎: 已接受申请,交易开始
士郎: 交易成功,获得 屠龙宝刀 * 1
伊莉雅: 交易成功,获得 2000000 金币
7.2.3 注意点
  • exchange 必须且仅仅发生在两个线程之间;
  • Exchanger 自己保护好,别被第三者横插一脚,数据就被第三者劫走了。
7.2.4 手写

实现思路:

  • 定义一个 data 变量存储 exchange 的值;
  • 当一个线程调用 exchange 方法时,如果它是先到的,就把自己的值存到 data 变量,然后阻塞自己等待后到的线程唤醒;
  • 如果一个线程调用 exchange 方法是,发现自己是后到的,就从 data 变量取出先到的线程放入的值,再把自己的值放入 data 变量,然后唤醒先到的线程,让它从 data 变量取出自己的值;
  • 难点在于,当存在多于两个线程时,要处理竞争,可能 AB 交换,可能 BC 交换,也可能 AC 交换,看谁竞争胜出。
package per.lvjc.concurrent.util;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LvjcExchanger<V> {

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    private Object data;
    private Thread thread;

    public V exchange(V value) throws InterruptedException {
        try {
            lock.lockInterruptibly();
            Thread currentThread = Thread.currentThread();
            // 其它线程还没有提交数据
            while (thread == null || thread == currentThread) {
                data = value;
                thread = currentThread;
                condition.await();
                if (thread != currentThread) {
                    Object o = data;
                    data = null;
                    thread = null;
                    return (V) o;
                }
            }
            Object o = data;
            data = value;
            thread = currentThread;
            condition.signal();
            return (V) o;
        } finally {
            lock.unlock();
        }
    }

    public V exchange(V value, long i, TimeUnit unit) throws InterruptedException, TimeoutException {
        long remaining = TimeUnit.MILLISECONDS.convert(i, unit);
        long deadline = System.currentTimeMillis() + remaining;
        try {
            lock.lockInterruptibly();
            Thread currentThread = Thread.currentThread();
            while (thread == null || thread == currentThread) {
                data = value;
                thread = currentThread;
                condition.await(remaining, TimeUnit.MILLISECONDS);
                if (thread != currentThread) {
                    Object o = data;
                    data = null;
                    thread = null;
                    return (V) o;
                } else {
                    remaining = deadline - System.currentTimeMillis();
                    if (remaining <= 0) {
                        throw new TimeoutException();
                    }
                }
            }
            Object o = data;
            data = value;
            thread = currentThread;
            condition.signal();
            return (V) o;
        } finally {
            lock.unlock();
        }    
    }
}

这里直接用锁实现了,因为 Exchanger 对多线程竞争的处理要比 Semaphore 复杂得多,用 cas 需要考虑各种并发场景。JDK 是用 cas 不加锁实现的,相应的,性能也要优秀得多。

7.3 CountDownLatch

主要用于两组线程之间的交互:

  • 一组是开关线程,当条件满足时,打开开关;
  • 一组是工作线程,当所有开关都打开后,开始执行相关逻辑。
7.3.1 核心方法
  • CountDownLatch(int):构造方法,设定一个初始 count;
  • await():使当前线程阻塞,直到 count == 0;
  • countDown():使 count - 1;

通常:

  • 预先设定好开关个数,即 count 值;
  • 工作线程使用 await 阻塞,等待所有开关都被打开,即 count == 0;
  • 开关线程判断工作条件是否满足,当条件满足时,打开开关,即 count - 1。
7.3.2 demo

小明和小红两个线程 await,股价变动和银证转账两个线程 countDown:

package per.lvjc.concurrent.util;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class CountDownLatchDemo {

    private static CountDownLatch countDownLatch = new CountDownLatch(2);

    private static int price = 70;
    private static int money = 10000;

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("小明: 当前股价: " + price + " 元");
            System.out.println("小明: 可用金额: " + money + " 元");
            //可以换成 if, 因为 CountDownLatch 不能重用,只能阻塞一次
            while (price * 1000 > money) {
                try {
                    //阻塞等待条件达成
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("小明: 知道啦,买了 10 手");
        }).start();
        
        new Thread(() -> {
            System.out.println("小红: 股价跌了,小明你买了吗?");
            try {
                //阻塞在同一个 CountDownLatch 上面
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("小红: " + price + " 啦");
            System.out.println("小红: 哦,买了呀,小明好厉害!");
        }).start();
        
        new Thread(() -> {
            System.out.println("股价变动...");
            while (price > 65) {
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                    System.out.println("股价: " + price);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                --price;
            }
            //条件 1 达成
            countDownLatch.countDown();
        }).start();
        
        new Thread(() -> {
            System.out.println("银证转账中...");
            try {
                TimeUnit.MILLISECONDS.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            money += 60000;
            System.out.println("证券账户转入 60000 元");
            //条件 2 达成
            countDownLatch.countDown();
        }).start();
    }
}

输出:

小明: 当前股价: 70 元
小明: 可用金额: 10000 元
小红: 股价跌了,小明你买了吗?
股价变动...
银证转账中...
股价: 70
股价: 69
股价: 68
证券账户转入 60000 元
股价: 67
股价: 66
小明: 知道啦,买了 10 手
小红: 65 啦
小红: 哦,买了呀,小明好厉害!
7.3.3 注意点
  • CountDownLatch 正常情况不可重用,因为 count 只能减不能加,但这个“因为”可以用反射来打破,所以如果考虑反射,CountDownLatch 也是可以重用的;

示例,使用反射修改 CountDownLatch 的 count 值:

package per.lvjc.concurrent.util;

import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class CountDownLatchTest {

    //初值为 1
    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("begin");
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("end");
        }).start();
        new Thread(() -> {
            try {
                //反射拿到 CountDownLatch 的 sync 变量
                Field sync = CountDownLatch.class.getDeclaredField("sync");
                sync.setAccessible(true);
                //反射获取 sync 变量值,是 AQS 的子类
                AbstractQueuedSynchronizer synchronizer = (AbstractQueuedSynchronizer) sync.get(countDownLatch);
                //反射拿到 sync 变量类型的父类,即 AQS 的 state 变量
                Field state = synchronizer.getClass().getSuperclass().getDeclaredField("state");
                state.setAccessible(true);
                //反射修改 AQS 的 state 变量值
                state.set(synchronizer, 5);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
            }
            //此处打印 5
            System.out.println(countDownLatch.getCount());
            //countDown 一次后还有 4,所以 await 线程醒不来
            countDownLatch.countDown();
        }).start();
    }
}
  • CountDownLatch 设置的初始 count 与线程数没有任何关系,不管使用什么手段,只要把 count 变成 0,再把 await 的线程叫醒,await 的线程就可以接着往下跑。
7.3.4 手写

JDK 实现方案:

  • 使用了 AQS,用 AQS 的 state 变量存储 count 值;
  • await:借助于 AQS 共享锁机制,利用获取共享锁失败来阻塞,因此要设定 state != 0 时获取共享锁失败;
  • countDown:释放共享锁,不过要反过来,释放锁时,state 不增反减。

这里的实现方案:

  • 不使用 AQS,定义一个 count 变量;
  • 因为不用 AQS,所以还需要一个队列存放 await 的所有线程;
  • await:考虑使用 park 阻塞,被唤醒时检查 count 值是否为 0,不为 0 继续阻塞;
  • countDown:countDown 一次,count - 1,减到 0 时使用 unpark 唤醒队列里阻塞着的所有线程。
package per.lvjc.concurrent.util;

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;

public class LvjcCountDownLatch {

    /**
     * 不使用 Unsafe 了,直接用 JDK 封装好的 Atomic 类
     */
    private AtomicInteger atomicInteger;
    private ConcurrentLinkedQueue<Thread> queue;

    public LvjcCountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.atomicInteger = new AtomicInteger(count);
        this.queue = new ConcurrentLinkedQueue<>();
    }

    public void await() throws InterruptedException {
        while (atomicInteger.get() != 0) {
            queue.offer(Thread.currentThread());
            //注意必须再判断一次
            if (atomicInteger.get() != 0) {
                LockSupport.park();
            }
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
        }
    }

    public void countDown() {
        if (atomicInteger.get() == 0) {
            return;
        }
        if (atomicInteger.decrementAndGet() == 0) {
            Thread thread;
            while ((thread = queue.poll()) != null) {
                LockSupport.unpark(thread);
            }
        }
    }
}

7.4 CyclicBarrier

Barrier:障碍,在一组线程的执行过程中设一个障碍,必须所有线程都跑到障碍处,才能继续往下跑,否则全都被阻塞在障碍处;

Cyclic:循环,可以重用。

7.4.1 核心方法

核心方法:

  • CyclicBarrier(int, Runnable):
    • int:线程数,必须有这个数量的线程数跑到障碍处,大家才能继续往下跑;
    • Runnable:barrier action,当所有线程都到达障碍处时,可以先完成一个任务,然后再让所有的线程继续往下跑;
  • await():表示当前线程到达障碍处;
  • await(long, TimeUnit):当前线程已到达障碍处,并且只会在障碍处等待指定的时间,超时还有线程没到,就不等了,自己先溜;
  • reset():障碍重置,原来的障碍失效,在原障碍处等待的所有线程就懵逼了,抛出 BrokenBarrierException;
  • isBroken():障碍破了,有三种情况:
    • timeout:有线程已经等不及先溜了;
    • interrupt:有线程被从阻塞中打断;
    • reset:障碍被重置,原障碍则 broken;
    • barrier action exception:所有线程都到达障碍处之后,在执行 barrier action 时抛异常。

异常场景:

  • 有线程还没 await 就已经抛异常了:对其它线程无影响,但有个副作用,有线程死在了 await 途中,所以这些线程永远到不齐了,可能导致其它线程永远在 await;
  • 执行 barrier action 抛异常:barrier action 由最后到的线程执行,所以 barrier action 中的异常会在最后到的线程中抛出,而其它线程全都被唤醒并抛出 BrokenBarrierException;
  • TimeoutException:等不及的线程自己先走,抛出 TimeoutException,其它所有已经 await 的线程全都被唤醒并抛出 BrokenBarrierException;
  • InterruptedException:有线程 await 被打断,被打断的线程抛出 InterruptedException,其它所有已经 await 的线程全都被唤醒并抛出 BrokenBarrierException;
  • reset:barrier 重置,所有已经 await 的线程全都被唤醒并抛出 BrokenBarrierException。
7.4.2 demo
package per.lvjc.concurrent.util;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {

    private static final CyclicBarrier barrier = new CyclicBarrier(2, () -> {
        System.out.println("补魔...");
    });

    public static void main(String[] args) {
        Runnable runnable = () -> {
            try {
                String name = Thread.currentThread().getName();
                System.out.println(name + ": 准备到房间补魔...");
                System.out.println(name + ": 进入房间");
                barrier.await();
                System.out.println(name + ": 准备到客厅补魔...");
                System.out.println(name + ": 进入客厅");
                barrier.await();
                System.out.println(name + ": 准备到浴室补魔...");
                System.out.println(name + ": 进入浴室");
                barrier.await();
                System.out.println(name + ": 魔补满了全身都是力量");
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        };
        new Thread(runnable, "士郎").start();
        new Thread(runnable, "凛").start();
    }
}

输出结果:

士郎: 准备到房间补魔...
士郎: 进入房间
凛: 准备到房间补魔...: 进入房间
补魔...: 准备到客厅补魔...: 进入客厅
士郎: 准备到客厅补魔...
士郎: 进入客厅
补魔...
士郎: 准备到浴室补魔...
士郎: 进入浴室
凛: 准备到浴室补魔...: 进入浴室
补魔...: 魔补满了全身都是力量
士郎: 魔补满了全身都是力量
7.4.3 注意点
  • 初始 count 不可变(不考虑反射);
  • 一个线程 await,count 固定 -1。
7.4.4 实现原理
  • ReentrantLock,Condition,await,signalAll;
  • await 一个线程,count - 1,
    • count != 0 时,Condition.await 阻塞
    • count == 0 时,Condition.signalAll 唤醒所有线程,重置 CyclicBarrier,如果 barrier action 不为空,在 signalAll 之前要先执行 barrier action 的 run 方法(所以是在最后一个 await 的线程里执行的)。

7.5 Phaser

Phaser 是比 CountDownLatch 和 CyclicBarrier 功能更加灵活的一种 barrier。

7.5.1 替代 CountDownLatch 使用

以下代码实现:必须 CDE 线程全都跑完,AB 线程才能继续往下跑。

package per.lvjc.concurrent.util.phaser;

import java.util.Random;
import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;

public class PhaserDemo1 {

    //因为有 CDE 三个线程作为控制条件,因此初值设为 3
    private static Phaser phaser = new Phaser(3);

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("A begin");
            //动态增加一个参与的线程数,现在从 3 变成 4 了
            phaser.register();
            //在此阻塞
            phaser.arriveAndAwaitAdvance();
            //阻塞被唤醒后做自己的业务
            System.out.println("A end");
        }, "A").start();

        Runnable runnable = () -> {
            String name = Thread.currentThread().getName();
            System.out.println(name + " begin...");
            randomSleep();
            System.out.println(name + " end...");
            //作为控制条件的线程,则是所有工作完成后 arrive,不需要 awaitAdvance
            phaser.arrive();
        };
        
        new Thread(runnable, "C").start();
        
        //B 线程穿插在 CDE 线程中间跑
        new Thread(() -> {
            System.out.println("B begin");
            phaser.register();
            phaser.arriveAndAwaitAdvance();
            System.out.println("B end");
        }, "B").start();
        
        new Thread(runnable, "D").start();
        new Thread(runnable, "E").start();
    }

    private static void randomSleep() {
        Random random = new Random(System.currentTimeMillis());
        try {
            TimeUnit.MILLISECONDS.sleep(random.nextInt(5000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
7.5.2 替代 CyclicBarrier 使用

上一节中 CyclicBarrier 的 demo,这里用 Phaser 实现:

package per.lvjc.concurrent.util.phaser;

import java.util.concurrent.Phaser;

public class PhaserTest2 {

    private static Phaser phaser = new Phaser(2) {
        // 对应 CyclicBarrier 的 barrier action,
        // 但相比 Runnable 的无参,这里有两个参数,可以做一些额外的判断
        // 相同的是,也是在最后一个到达的线程执行
        @Override
        protected boolean onAdvance(int phase, int registeredParties) {
            System.out.println("补魔...");
            return super.onAdvance(phase, registeredParties);
        }
    };

    public static void main(String[] args) {
        Runnable runnable = () -> {
            String name = Thread.currentThread().getName();
            System.out.println(name + ": 准备到房间补魔...");
            System.out.println(name + ": 进入房间");
            //对应 CyclicBarrier#await()
            phaser.arriveAndAwaitAdvance();
            System.out.println(name + ": 准备到客厅补魔...");
            System.out.println(name + ": 进入客厅");
            phaser.arriveAndAwaitAdvance();
            System.out.println(name + ": 准备到浴室补魔...");
            System.out.println(name + ": 进入浴室");
            phaser.arriveAndAwaitAdvance();
            System.out.println(name + ": 魔补满了真舒服");
        };
        new Thread(runnable, "士郎").start();
        new Thread(runnable, "凛").start();
    }
}

另外,对比 CyclicBarrier 的异常场景,Phaser 的行为有所不同:

  • 对于 CyclicBarrier:除了抛出异常的线程以外,其它在等待的线程都会抛出 BrokenBarrierException;
  • 对于 Phaser:除了抛出异常的线程以外,其它线程不会受到影响。
7.5.3 arrive,awaitAdvance,arriveAndAwaitAdvance,getPhase 方法

arrive:到达屏障处,记录已经 arrive 的数量 +1,然后不阻塞继续往下跑。

getPhase 和 arrive 的返回值有一定的联系:

  • getPhase 返回当前处于第几段;

  • arrive 返回当前到达的是第几段的屏障。

getPhase - arrive 可能有两个值:

  • 0:arrive 之后,由于 arrive 的数量不足,还不能越过屏障;
  • 1:当前就是最后一个 arrive 的,arrive 之后就越过屏障了,phase 就会进入下一个阶段,于是比 arrive 值大 1;
package per.lvjc.concurrent.util.phaser;

import java.util.concurrent.Phaser;

public class PhaserDemo3 {

    private static Phaser phaser = new Phaser(2);

    public static void main(String[] args) {
        // arrive 的是第 0 个屏障,因此 arrive = 0
        int arrive = phaser.arrive();
        // 当前是第 0 个阶段,因此 phase = 0
        int phase = phaser.getPhase();
        System.out.println("arrive = " + arrive);
        System.out.println("phase = " + phase);
        System.out.println("-----------");
        // 因为需要 arrive 两个才能越过屏障,所以上面第一次 arrive 没有越过屏障,当前仍处于第 0 阶段
        // 第 2 次 arrive,此时到达的仍是第 0 阶段的屏障,因此 arrive = 0
        arrive = phaser.arrive();
        // 第 2 次 arrive 之后,已经越过屏障,进入了第 1 阶段,因此 phase = 1
        phase = phaser.getPhase();
        System.out.println("arrive = " + arrive);
        System.out.println("phase = " + phase);
    }
}

arrive,awaitAdvance,arriveAndAwaitAdvance 三个方法也有一些联系:

  • arrive:到达屏障但不阻塞;
  • awaitAdvance(int):接收一个 int 值,在指定的屏障处阻塞;
  • arriveAndAwaitAdvance:到达屏障并阻塞;
  • awaitAdvance(arrive()) = 到达屏障,在当前屏障处阻塞 = arriveAndAwaitAdvance()。
7.5.4 root 和 parent

Phaser 不仅可以单独使用,还可以将多个 Phaser 建立成树形结构,因此就有了 root 和 parent 的概念。

主要目的是在高并发场景下分散成多个相关联的 Phaser 减少竞争。

关于 root 和 parent 可以总结如下几点:

  • 创建 Phaser 时,如果有 parent,并且设置的 parties(也就是构造方法中设置的那个 int 值,虽然后面也可以改)不为 0,那么将 parent 的 parties 加 1;
  • 当 child Phaser 的 parties 被减到 0 的时候,自动将 parent 的 parties 减 1(与上一条呼应);
  • 当 child Phaser 的所有 parties 都 arrive 的时候,在 parent 里面算作 1 个 arrive;
  • 即使 child Phaser 所有的 parties 都 arrive,也不能越过屏障,必须等 parent 先越过屏障(非阻塞方法除外);
  • 唤醒由 root Phaser 负责,遍历在队列中等待的线程逐一唤醒。

demo 如下:

package per.lvjc.concurrent.util.phaser;

import java.util.concurrent.Phaser;

public class PhaserTest4 {

    private static Phaser rootPhaser = new Phaser(2);
    private static Phaser phaser1 = new Phaser(rootPhaser, 1);
    private static Phaser phaser2 = new Phaser(phaser1, 1);
    private static Phaser phaser3 = new Phaser(phaser2, 1);

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println("root begin");
            System.out.println("root parties = " + rootPhaser.getRegisteredParties());
            System.out.println("root arrived parties = " + rootPhaser.getArrivedParties());
            rootPhaser.arriveAndAwaitAdvance();
            System.out.println("root end");
        }, "root").start();
        Thread.sleep(1000);
        new Thread(() -> {
            System.out.println("1 begin");
            System.out.println("phaser1 parties = " + phaser1.getRegisteredParties());
            System.out.println("phaser1 arrived parties = " + phaser1.getArrivedParties());
            phaser1.arriveAndAwaitAdvance();
            System.out.println("1 end");
        },"1").start();
        Thread.sleep(1000);
        new Thread(() -> {
            System.out.println("2 begin");
            System.out.println("phaser2 parties = " + phaser2.getRegisteredParties());
            System.out.println("phaser2 arrived parties = " + phaser2.getArrivedParties());
            phaser2.arriveAndAwaitAdvance();
            System.out.println("2 end");
        }, "2").start();
        Thread.sleep(1000);
        new Thread(() -> {
            System.out.println("3 begin");
            System.out.println("phaser3 parties = " + phaser3.getRegisteredParties());
            int arrive = phaser3.arrive();
            System.out.println("phaser3 arrived parties = " + phaser3.getArrivedParties());
            phaser3.awaitAdvance(arrive);
            System.out.println("3 end");
        }, "3").start();
        //rootPhaser.arrive();
    }
}

输出结果:

root begin
root parties = 3
root arrived parties = 0
1 begin
phaser1 parties = 2
phaser1 arrived parties = 0
2 begin
phaser2 parties = 2
phaser2 arrived parties = 0
3 begin
phaser3 parties = 1
phaser3 arrived parties = 1
(未完,永久阻塞...)

可以看出:

  • rootPhaser,phaser1,phaser2 调用 getRegisteredParties 方法得到的值都比构造方法中设的值多 1 个,而且也没有调用 register 方法,这就是上面总结的第一点;
  • phaser3 只有 1 个 parties,并且它自己已经 arrive 了,但它依然被阻塞在屏障处,因为它在等 phaser2 醒来,phaser2 在等 phaser1 醒来,phaser1 在等 rootPhaser 醒来,但 rootPhaser 因为还少一个 arrive 所以醒不来,导致所有线程都被阻塞着,只需把最后注释的一行代码打开就可以了;
  • 谁的 end 先打印出来是不确定的,因为 rootPhaser 会一下把在 phaser1,phaser2,phaser3 阻塞的线程全部唤醒。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值