Java多线程之CountDownLatch、CyclicBarrier、Semaphore与Exchanger

Java多线程有Runnable、Thread、Callable、线程池、synchronized、volatile、Lock等可以直接使用。也有线程的直接实现可用。

下边主要讲下CountDownLatch、CyclicBarrier、Semaphore与Exchanger

CountDownLatch

从名字可以知道,是个倒计数锁。通过一个计数器,每个线程完成则减一,并在原地等待。直至减到0,开始后续工作。

应用场景:应用场景:A、B、C三个任务,可以并发执行,然后都执行完后才可以执行任务D。

public class TryCountDownLatch implements Runnable {
    private int sequence;

    public TryCountDownLatch(int sequence) {
        this.sequence = sequence;
    }

    // 初始化计数器,注意这里是 static 的。为了共用
    static final CountDownLatch latch = new CountDownLatch(10);

    @Override
    public void run() {
        try {
            Thread.sleep(new Random().nextInt(10) * 1000);
            System.out.println("Complete Run " + sequence);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 计数减一
            latch.countDown();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            exec.submit(new TryCountDownLatch(i));
        }
        // 等待检查
        System.out.println("All Thread Wait " + System.currentTimeMillis());
        latch.await();

        System.out.println("All Thread Completed " + System.currentTimeMillis());
        // 关闭线程池
        exec.shutdown();
    }
}

这里主线程会阻塞在 await() 的地方,然后所有线程类执行后都调用CountDownLatch的countDown()方法,即数字减一。直到为0,主线程开始继续工作。帮我们解决了多线程的执行依赖关系。

CyclicBarrier

也即是我们常说的栅栏类,线程走到栅栏后阻塞等待,直到所有线程都满足才能继续往下执行。至于它与CountDownLatch 的区别,网上说CyclicBarrier是N个线程相互等待,而
CyclicBarrier 是一个后续线程等待N个线程。我觉得没事区别。唯一的区别是:CountDownLatch采用计算器,只能使用一次。而CyclicBarrier 即循环栅栏,也就是说它可以循环使用。

它的用法跟CountDownLatch 差不多,首先它的构造函数需要一个等待线程数,和一个后续线程任务

public class TestCyclicBarrier {

    public static void main(String[] args) {
        // 注意这里栅栏数和下边的线程数
        CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("所有线程均完成,此处进行栅栏后的收尾工作");
            }
        });

        for (int i = 0; i < 5; i++) {
            TryCyclicBarrier thread = new TryCyclicBarrier(barrier, i);
            new Thread(thread).start();
        }
        System.out.println("主线程结束 " + System.currentTimeMillis());
    }
}

在所有要先执行的线程里调用await() 方法

public class TryCyclicBarrier implements Runnable {

    private CyclicBarrier cyclicBarrier;

    private int sequence;

    public TryCyclicBarrier(CyclicBarrier cyclicBarrier, int sequence) {
        this.cyclicBarrier = cyclicBarrier;
        this.sequence = sequence;
    }

    @Override
    public void run() {
        try {
            System.out.println("线程 " + sequence + " 开始工作");
            Thread.sleep(new Random().nextInt(10) * 1000);
            System.out.println("线程 " + sequence + " 到达栅栏");

            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

在有指定线程数 await() 后,等待的线程任务才开始执行。输出如下:

线程 2 开始工作
线程 1 开始工作
线程 4 开始工作
线程 3 开始工作
线程 0 开始工作
主线程结束 1585920830794
线程 3 到达栅栏
线程 0 到达栅栏
线程 4 到达栅栏
线程 1 到达栅栏
线程 2 到达栅栏
所有线程均完成,此处进行栅栏后的收尾工作

注意:如果超过指定线程数,等待的线程任务有可能会再次执行。

假如CyclicBarrier 指定了需要3个线程await()和一个后续线程任务D,当A、B、C三个await后,CyclicBarrier会执行D。之后A、B、C三个再次 await() 后,还会再次执行一次任务D。

 

Semaphore

即信号量类。我们知道synchronized 用来控制方法或者代码块互斥的,同一时间只有一个线程进入。Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。

新建一个信号量对象,并设置最多可进入的线程数。在要控制并发的代码之前调用 acquire() ,之后调用 release() 方法。

public class TrySemaphore {
    // 同步关键类,构造方法传入的数字是多少,则同一个时刻,最多允许多少个进程同时运行
    private Semaphore semaphore = new Semaphore(2);
    
    public void process(String threadName) throws Exception {
        // 在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许指定个数线程进入,
        semaphore.acquire();
        System.out.println(System.currentTimeMillis() + " 进入互斥区 " + threadName);
        Thread.sleep(new Random().nextInt(10) * 1000);
        System.out.println(System.currentTimeMillis() + " 离开互斥区 " + threadName);
        semaphore.release();

    }
}

然后将该任务放入多线程中执行:

public class TestSemaphore extends Thread {

    private TrySemaphore work;

    private int sequence;

    public TestSemaphore(TrySemaphore work, int sequence) {
        this.work = work;
        this.sequence = sequence;
    }

    @Override
    public void run() {
        try {
            this.work.process("Thread " + sequence);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        TrySemaphore trySemaphore = new TrySemaphore();
        for (int i = 0; i < 5; i++) {
            TestSemaphore thread = new TestSemaphore(trySemaphore, i);
            thread.start();
        }
    }
}

执行结果如下:

1585920227612 进入互斥区 Thread 0
1585920227612 进入互斥区 Thread 1
1585920233615 离开互斥区 Thread 1
1585920233616 进入互斥区 Thread 2
1585920235616 离开互斥区 Thread 0
1585920235616 进入互斥区 Thread 3
1585920239618 离开互斥区 Thread 2
1585920239618 进入互斥区 Thread 4
1585920242619 离开互斥区 Thread 3
1585920242622 离开互斥区 Thread 4

从上边日志输出可以看出,最开始只有俩线程进入互斥区,然后有线程离开后,其他线程才能进去该代码区。从这点来说,它跟synchronized 效果一模一样,只是允许的线程数量大于1而已。

Exchanger

Exchanger 是一个交换服务,允许原子性的交换两个(多个)对象,但同时只有一对才会成功。

当一个线程到达 exchange 调用点时,如果其他线程此前已经调用了此方法,则其他线程会被调度唤醒并与之进行对象交换,然后各自返回;
如果其他线程还没到达交换点,则当前线程会被挂起,直至其他线程到达才会完成交换并正常返回,或者当前线程被中断或超时返回
例如

public class TestExchange {
    static class Processer extends Thread {
        private Exchanger<String> exchanger;

        public Processer(String name, Exchanger<String> exchanger) {
            super(name);
            this.exchanger = exchanger;
        }

        @Override
        public void run() {
            for (int i = 1; i < 5; i++) {// 注意:这里从1 开始,每个线程去去交换4次
                try {
                    TimeUnit.SECONDS.sleep(1);
                    String preData = "From" + getName() + " data" + i;
                    String postData = exchanger.exchange(preData);
                    System.out.println(getName() + " 交换前:" + preData + " 交换后:" + postData);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Exchanger<String> exchanger = new Exchanger<String>();
        new Processer("Processer-1", exchanger).start();
        new Processer("Processer-2", exchanger).start();
        new Processer("Processer-3", exchanger).start();
        // TODO 3个线程 产生 4 * 3 = 12 次交换。因为两两直接才能交换。所以能结束.
        // 如果 这里产生了奇数个交换,则某个线程将用于处于等等状态
        TimeUnit.SECONDS.sleep(7);
    }
}

上边正好构成偶数个交换,两两成功。因此可以结束。

 

Lock

Lock经常用来跟synchronized 比较:synchronized 可以加在类、方法、代码块上,报错后自动释放锁,可以防止JVM对代码重排序。而Lock 加在代码块上,且需要主动释放,是Java的类。主要用的是ReentrantLock,即可重入锁。ReadWriteLock 读写锁。

对比上边代码,改成Lock方式:

public class TryLoack {
    private int fromValue;

    private int toValue;

    public TryLoack(int fromValue, int toValue) {
        this.fromValue = fromValue;
        this.toValue = toValue;
    }

    public int balance(int offset) {
        Lock lock = new ReentrantLock();
        try {
            lock.lock();
            fromValue -= offset;
            toValue += offset;
            return fromValue + toValue;
        } finally {
            lock.unlock();
        }
    }
}

volatile

严格来说volatile 不能解决多线程并发互斥问题。它只涉及变量在线程中的可见行。

假设变量 var 被A、B两个线程使用,当A使用并修改var 时,它修改的只是var 在当前线程中的副本,对此线程B是不可见的,直到本次修改被定期同步到主内存。为了让 A 对 var  的修改立刻被 B 感知,就需要对 var 加 volatile 修饰符。

单例设计模式中,volatile 与 synchronized 一起使用,双重检查来生成单例对象。参考单例模式双重检查

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章