java并发编程之线程同步(CountDownLatch、CyclicBarrier)

多线程

线程:类似执行一个命令,多线程:并发执行多条命令。

多线程的优点:
1.充分利用cpu的性能。
2.提高系统性能。
3.同一时刻处理可以处理不同的命令

线程同步

即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,为什么需要它呢?

1.多线程会出现线程安全问题,线程同步可以有效的保证线程安全。
        2.当主线程依赖两个子线程结果的时候,需要线程同步

如何实现线程同步?
1.加锁,如:synchronized。
2.通过wait和和notify(和notifyAll),推荐使用notifyAll。
3.线程池callback。
4.join()。
5.CountDownLatch(java SDK包)。
6.CyclicBarrier(java SDK包)。
等等。。。。。。。。。。。。。。。。

这里只介绍5、6两种

我们先来看一个没有加入线程同步的代码:

public static  void hello(){
        System.out.println("线程:"+Thread.currentThread().getName()+" 执行了。。。。。。。。。");
    }

    public static void main(String[] args) {
        //线程1
        Thread t1 = new Thread(() -> {
            hello();
        });
        t1.start();
        //线程2
        Thread t2 = new Thread(() -> {
            hello();
        });
        t2.start();
        System.out.println("主函数执行完毕。。。。。。。。。");
    }

打印结果:


main方法的输出语句居然比两个子线程先执行,为什么呢?因为main是主线程,t1、t2是两个子线程,由于线程的执行顺序是无序的,所以就会导致每次的执行结果都不相同,现在我想实现当t1、t2执行完成之后在执行main方法的输出语句,该如何实现呢?只需要给t1、t2分别加一个方法即可:

public static  void hello(){
        System.out.println("线程:"+Thread.currentThread().getName()+" 执行了。。。。。。。。。");
    }

    public static void main(String[] args) {
        //线程1
        Thread t1 = new Thread(() -> {
            hello();
        });
        t1.start();
        //线程2
        Thread t2 = new Thread(() -> {
            hello();
        });
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主函数执行完毕。。。。。。。。。");
    }

结果如下:

为什么join(),可以实现线程同步呢?join()源码如下:

很明显,这里使用while做了循环等待,让线程不往下执行,达到线程同步(等待)的效果。

然而我们平时的开发过程中基本不会这么创建线程,一般都是使用线程池,那在使用线程池的情况下如何让线程实现同步呢?

我们先试试自己写一个方法让它实现同步,代码如下:


    public static  void hello(){
        String name = Thread.currentThread().getName();
        try {
            System.out.println("线程:"+name+" 休眠开始。。。。。。。。。。。。");
            Thread.sleep(1000);
            System.out.println("线程:"+name+" 休眠结束。。。。。。。。。。。。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static  void main(String[] args)  {
        // 计数器初始化为2
        AtomicInteger count = new AtomicInteger(2);
        executor.execute(() ->{
            hello();
            count.decrementAndGet();
        });
        executor.execute(() ->{
            hello();
            count.decrementAndGet();
        });
        //等待两个线程执行完毕
        while(count.get() != 0){

        }

        System.out.println("我是在两个线程执行之后才执行的内容");

    }
count:用于统计线程执行的数量,线程执行-1;AtomicInteger:原子类,可以在多线程中保证共享变量的安全。
decrementAndGet:自减并返回自减以后的结果(原子操作)。
while:线程同步的重点:这里主要是让主线程处于循环状态,直到count被减为0,也就意味着两个子线程都已执行完毕。

但是我不推荐这么做,为什么呢?因为java SDK给我们提供了现成的方法,我们为啥还要自己去手动实现呢?下面我们就来看看 CountDownLatch是如何实现线程同步:
 

// 创建2个线程的线程池
    private static  Executor executor =   Executors.newFixedThreadPool(2);

    public static  void hello(){
        String name = Thread.currentThread().getName();
        try {
            System.out.println("线程:"+name+" 休眠开始。。。。。。。。。。。。");
            Thread.sleep(1000);
            System.out.println("线程:"+name+" 休眠结束。。。。。。。。。。。。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static  void main(String[] args)  {
        
        //这里需要注意一点,那就是实例化CountDownLatch的初始大小,一定要和你需要等待线程的数量相同,
        //小了会导致等待的线程提前执行。
        //大了会导致线程一直处于无限循环当中
        CountDownLatch countDownLatch = new CountDownLatch(2);
        executor.execute(() ->{
            hello();
            countDownLatch.countDown();
        });
        executor.execute(() ->{
            hello();
            countDownLatch.countDown();
        });
        //等待两个线程执行完毕
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("我是在两个线程执行之后才执行的内容");

    }

为了效果明显,我特意在hello方法让线程休眠1秒。
countDownLatch:实现线程同步的关键,实例化一个需要等待的线程数量
countDownLatch.countDown():等待线程数-1。
countDownLatch.await();让主线程处于等待状态,直到等待的线程被减为0(注意:这里必须要做异常捕获线程中断的异常:(InterruptedException);

上面代码结果如下:

这里需要注意一点:CountDownLatch的初始大小是不会被重置的,所以使用这个解决方案的时候需要手动重置CountDownLatch线程等待的初始大小。
实现原理:

其实查看源码,他的实现方式和我之前使用的while类似,他这里用了for的无限循环,直到等待的线程被减为0;

那有没有不需要重新设置线程等待的工具类呢?肯定是有的,那就是接下来要说的:CyclicBarrier

CyclicBarrier

主要通过线程回调来实现线程等待,这里的实现方式稍微做了一下修改:

// 创建3个线程的线程池,其中一个线程用于回调处理主线程的事情
    private static  Executor executor =   Executors.newFixedThreadPool(3);

    public static  void hello(){
        String name = Thread.currentThread().getName();
        try {
            System.out.println("线程:"+name+" 休眠开始。。。。。。。。。。。。");
            Thread.sleep(1000);
            System.out.println("线程:"+name+" 休眠结束。。。。。。。。。。。。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static  void main(String[] args)  {

        //这里需要注意一点,那就是实例化CountDownLatch的初始大小,一定要和你需要等待线程的数量相同,
        //小了会导致等待的线程提前执行。
        //大了会导致线程一直处于无限循环当中
        final CyclicBarrier barrier = new CyclicBarrier(2, ()->{ executor.execute(()->printAfter()); });
        executor.execute(() ->{
            hello();
            try {
                barrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        executor.execute(() ->{
            hello();
            try {
                barrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 两个线程执行完毕之后执行此方法
     */
    private static void printAfter() {

        System.out.println("我是在两个线程执行之后才执行的内容");
    }

这里需要注意一点,那就是主函数的输出语句已经不放在mian方法中了,而是写在了barrier的回调方法中。当等待的线程执行完毕之后CyclicBarrier的等待线程数会被重置。

CyclicBarrier与CountDownLatch区别
CountDownLatch:解决一个线程等待多个线程场景。
CyclicBarrier:解决一组线程之间的等待场景。
CyclicBarrier支持重置功能,CountDownLatch不支持,这点需要特别注意。

发布了41 篇原创文章 · 获赞 79 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_33220089/article/details/102952725