并发工具类

概述

在JDK的并发包里提供了几个非常有用的并发工具类。CountDownLatch、CyclicBarrier和Semaphore工具类提供了一种并发流程控制的手段

CountDownLatch

如何能够保证T2在T1执行完后执行,T3在T2执行完后执行?

join方法

可以使用join方法解决这个问题。比如在线程A中,调用线程B的join方法表示的意思就是: A等待B线程执行完毕后(释放CPU执行权),在继续执行。

public class RunnableJob {
    public static void main(String[] args) throws InterruptedException {
        Worker runnableJob = new Worker();
        Thread t1 = new Thread(runnableJob, "T1");
        Thread t2 = new Thread(runnableJob, "T2");
        Thread t3 = new Thread(runnableJob, "T3");
        t1.start();
		//这里就是在main主线程中,调用t1线程的join方法。
		//也就是main主线程要等待t1执行完成后才能继续往下执行
        t1.join();
        t2.start();
        t2.join();
        t3.start();
        t3.join();
        System.out.println("主线程执行完毕----");
    }
}
class Worker implements Runnable{
    public void run() {
        Thread thread = Thread.currentThread();
        try {
            Thread.sleep(1000);
            System.out.println(thread.getName()+"正在执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:

T1正在执行
T2正在执行
T3正在执行
主线程执行完毕----

CountDownLatch

原理

CountDownLatch是基于 AQS 实现的

CountDownLatch对AQS的共享方式实现为:CountDownLatch 将任务分为N个子线程去执行,将 state 初始化为 N, N与线程的个数一致,N个子线程是井行执行的,每个子线程都在执行完成后 countDown() 1次, state 执行 CAS 操作并减1。在所有子线程都执行完成(state=0)时会unpark()主线程,然后主线程会从 await()返回,继续执行后续的动作。

具体使用

倒计时计数器
CountDownLatch用于某个线程等待其他线程执行完任务再执行,可以被认为是加强版的join()。

public class CountDownLatchTest {
    public static void main(String[] args) {
        final CountDownLatch countDownLatch = new CountDownLatch(3);
        new Thread("T1"){
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println(Thread.currentThread().getName()+"正在执行");
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            };
        }.start();
        new Thread("T2"){
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println(Thread.currentThread().getName()+"正在执行");
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            };
        }.start();
        new Thread("T3"){
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println(Thread.currentThread().getName()+"正在执行");
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            };
        }.start();
        System.out.println("等待三个线程执行完,主线程才能执行");
        try {
			//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行;
			//或者等待timeout时间后count值还没变为0的话也会继续执行
            countDownLatch.await();
//            countDownLatch.await(20000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程执行完毕");
    }
}

输出:

等待三个线程执行完,主线程才能执行
T1正在执行
T3正在执行
T2正在执行
主线程执行完毕

调用了await后,主线程被挂起,它会等待直到count值为0才继续执行;因此只影响主线程的执行顺序一定要在T1 T2 T3之后,但T1 T2 T3之间的顺序互不影响

应用场景: 开启多个线程同时执行某个任务,等到所有任务执行完再执行特定操作,如汇总统计结果。

CountDownLatch和join区别

相同点:都能等待一个或者多个线程执行完成操作,比如等待三个线程执行完毕后,第四个线程才能执行
不同点:join能让线程按我们预想的的顺序执行,比如线程1执行完了,线程2才能执行,线程2执行完,线程3才能执行,但是CountDownLatch就做不到.

当调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变为零(也就是线程都执行完了),由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多线程时,只需把这个CountDownLatch的引用传递到线程中即可

CyclicBarrier

循环屏障

用法:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行。也就是说,一组线程互相等待到某个状态,然后这组线程再同时执行

这个屏障之所以用循环修饰,是因为在所有的线程释放彼此之后,这个屏障是可以重新使用的(reset()方法重置屏障点)。这一点与CountDownLatch不同。

用法

构造函数

//parties表示屏障拦截的线程数量
public CyclicBarrier(int parties) {
}
//意思是在线程到达屏障时,优先执行barrierAction线程,再执行
public CyclicBarrier(int parties, Runnable barrierAction) {
}

举例:

public class CyclicBarrierTest {
    // 请求的数量
    private static final int threadCount = 10;
    // 需要同步的线程数量
    private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

    public static void main(String[] args) throws InterruptedException {
        // 创建线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            threadPool.execute(() -> {
                try {
                    test(threadNum);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            });
        }
        threadPool.shutdown();
    }

    public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
        System.out.println("threadnum:" + threadnum + "is ready");
        try {
            /**等待60秒,保证子线程完全执行结束*/
            cyclicBarrier.await(60, TimeUnit.SECONDS);
        } catch (Exception e) {
            System.out.println("-----CyclicBarrierException------");
        }
        System.out.println("threadnum:" + threadnum + "is finish");
    }

}

输出:

threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:3is finish
threadnum:2is finish
threadnum:1is finish
threadnum:0is finish
threadnum:5is ready
threadnum:6is ready
...

显然,循环屏障是可以重用的。
但是,如果以上的threadCount = 4,那么就会永远等待,因为 new CyclicBarrier(5)需要等待5个线程执行完毕,但是没有第5个线程执行await()方法,既没有第5个线程到达屏障,那么之前的线程达到屏障时都不会往下执行了

应用场景:
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。例如,用一个Excel保存了用户所有银行流水,每个Sheet保存一个账户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction根据这些线程的计算结果,计算出整个Excel的日均银行流水。

CyclicBarrier和CountDownLatch区别

  1. CyclicBarrier 和 CountDownLatch 都能够实现线程之间的等待
    • CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。
    • cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!
  2. CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

Semaphore

信号量

Semaphore类似于锁,它用于控制同时访问特定资源的线程数量,控制并发线程数。

public class SemaphoreDemo {
    public static void main(String[] args) {
        final int N = 7;
        Semaphore s = new Semaphore(3);
        MyRunnable myRunnable = new MyRunnable(s);
        for (int i = 0; i < N; i++) {
            new Thread(myRunnable, "Thread" + i).start();
        }
    }

    static class MyRunnable implements Runnable {
        private Semaphore s;

        public MyRunnable(Semaphore s) {
            this.s = s;
        }

        @Override
        public void run() {
            try {
                s.acquire();//获取许可证
                System.out.println("worker" + Thread.currentThread().getName() + " 开始下载");
                Thread.sleep(1000);
                System.out.println("worker" + Thread.currentThread().getName() + " 下载完毕");
                s.release();//释放许可证
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出:

workerThread0 开始下载
workerThread2 开始下载
workerThread1 开始下载
workerThread0 下载完毕
workerThread2 下载完毕
workerThread3 开始下载
workerThread1 下载完毕
workerThread5 开始下载
workerThread4 开始下载
workerThread5 下载完毕
workerThread6 开始下载
workerThread3 下载完毕
workerThread4 下载完毕
workerThread6 下载完毕

可以看出并非按照线程访问顺序获取资源的锁

应用场景:
Semaphore可以用于做流量控制,特别是公共资源有限的应用场景,比如数据库连接。
假如有一个需求要读取几万个文件的数据,因为都是IO密集型任务,可以启动几十个线程并发地读取,读到内存中后,还需要存储到数据库中,而数据库的连接数只有10个,那么就可以控制这几十个线程只有10个线程同时获取数据库连接来保存数据。这个时候,就可以使用Semaphore来做流量控制。

Exchanger

交换器

Exchanger是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchanger方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

应用场景:
Exchanger可用于校对工作,比如我们需要将纸质银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致。

public class ExchangerTest {
    private static final Exchanger<String> exchanger=new Exchanger<>();
    private static ExecutorService threadPool = Executors.newFixedThreadPool(2);
 
    public static void main(String[] args) {
 
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                String a = "银行流水A";
                try {
                    a=exchanger.exchange(a);
                    System.out.println("交换后,a="+a);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                String b = "银行流水B";
                try {
                    b=exchanger.exchange(b);
                    System.out.println("交换后,b="+b);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        threadPool.shutdown();
    }
}

输出:

交换后,a=银行流水B
交换后,b=银行流水A

如果两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以用exchange(V x, long timeout, TimeUnit unit)设置最大等待时长

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值