JUC中对线程的协同合作控制

在使用多线程的时候,我们可以使用一些工具来达到对资源的访问量控制线程之间的相互等待线程之间的通信唤醒

控制线程的合作并发,主要有四大工具;CountDownLatch,Semaphore,Condition以及CyclicBarrier。下面对这四个工具进行逐一介绍。

CountDownLatch

CountDownLatch主要是用于线程之间的等待协作。可以实现多等一,也可以实现一等多

例如,我们需要启动多个线程进行资源加载,等资源加载完成后主线程才能继续执行;这就可以用CountDownLatch的一等多的机制来完成;或者我们想自实现并发情况可以通过多等一的方式来使线程阻塞,等主线程经过一系列操作(休眠,或者准备其他资源)后,进行放行

使用

CountDownLatch在构造的时候需要传入一个参数,这个参数就是需要等待线程的个数,并且提供了一个阻塞的方法await()来让线程进行阻塞,只有当等待数减为0的时候,被阻塞的线程才能往下执行

而等待数可以通过CountDownLatch的countdown()来进行减一的操作。

代码演示

一等多代码示例:
可用于等待资源

public class CountDownLatchTest {

    // 构造一个倒数器
    static CountDownLatch countDownLatch = new CountDownLatch(2);
    public static void main(String[] args) throws InterruptedException {
        // 启动两个线程来加载资源
        new Thread(new TaskOne()).start();
        new Thread(new TaskTwo()).start();
        System.out.println("资源加载线程启动完成,Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
        countDownLatch.await();
        System.out.println("主线程执行完成,Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));

    }
}

// 任务二
class TaskOne implements  Runnable{
    @Override
    public void run() {
        // 模拟处理资源要5秒
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("TaskOne Finish");
        // 完成工作,线程数减一
        CountDownLatchTest.countDownLatch.countDown();
    }
}

// 任务一
class TaskTwo implements  Runnable{
    @Override
    public void run() {
        // 模拟处理资源要10秒
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 完成工作,线程数减一
        CountDownLatchTest.countDownLatch.countDown();
        System.out.println("TaskTwo Finish");

    }
}

结果演示:
在这里插入图片描述
可以看到,主线程确实等待了子线程完成了对应的工作才能继续走下去。否则,会一直阻塞在当前状态下等待

多等一代码示例:
可用于模拟并发的场景

public class CountDownLatchMuch {
    // 构造一个倒数器
    static CountDownLatch countDownLatch = new CountDownLatch(1);
    public static void main(String[] args) throws InterruptedException {
        // 用newCachedThreadPool 开多个任务线程(这里可以先了解一下JDK提供的线程池)
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 100 ; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("线程已到达 , Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
                        // 先让所有线程在这里阻塞
                        countDownLatch.await();
                        System.out.println("线程出发 , Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
                        //TODO 这里可以调用任意接口HttpClient来模拟高并发
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        // 休眠5秒,让所有线程都准备好
        Thread.sleep(5000);
        // 放行所有的线程
        countDownLatch.countDown();
    }
}

执行结果:
在这里插入图片描述

我们可以看到,子线程在我们休眠五秒后,主线程调用countdown()方法,然后同一时间被放行的

小结

CountDownLatch通过我们实现定义好的计数器,当我们达到某种条件后可让计数器-·;最终当计数器为0时,激活调用await()方法的线程进行下一步操作。

Semaphore

Semaphore可以简单的理解为许可证

只有拿到对应数量许可证的人才能通行,其他拿不到对应数量的人统统会被阻塞住;并且,还要等别人释放了许可证,自己才有机会拿到。

这就好比,有一些大的资源要限制更少的人拿去,有一些较小的资源允许多人拿去,并且这两个资源共用一种许可证。

使用

Semaphore提供了许多方法可以使用:
在这里插入图片描述
常用的

  • acquire():拿取一个许可证
  • acquire(int permits):拿取permits个许可证
  • release():释放一个许可证
  • release(int permits):释放permits个许可证
  • tryAcquire():尝试拿取一个许可证
  • tryAcquire(int permits):尝试拿取permits个许可证
  • tryAcquire(long timeout, TimeUnit unit):尝试在timeout unit 内拿取一个许可证;unit表示时间单位
  • tryAcquire(int permits, long timeout, TimeUnit unit):尝试在timeout unit 内拿取permits个许可证
  • 带有try的获取许可证的方法都是尝试去拿,会立马返回布尔值类型的结果;而不带try的则会阻塞等待别人释放,在去拿。

代码演示:

不释放许可证

public class SemaphoreOneTest {
    // 允许开放两个人去操作
    static Semaphore semaphore = new Semaphore(2);
    public static void main(String[] args) throws InterruptedException {
        new Thread(new SemaTaskOne()).start();
        new Thread(new SemaTaskTwo()).start();
        Thread.sleep(500);
        semaphore.acquire();
        System.out.println(Thread.currentThread().getName()+"拿到了许可证");
        System.out.println("Main Finish");
    }
}

class SemaTaskOne implements Runnable{

    @Override
    public void run() {
        try {
            SemaphoreOneTest.semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+"拿到了许可证");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class SemaTaskTwo implements Runnable{

    @Override
    public void run() {
        try {
            SemaphoreOneTest.semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+"拿到了许可证");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
我们看到如果不释放许可证的话,即使子线程执行完,主线程仍然拿不到许可证;如果主线程有机会先拿到许可证,即使主线程执行完了,程序仍然处于运行状态。因为拿到许可证的线程都没有释放,仍有子线程处于阻塞状态。

增加释放:

class SemaTaskOne implements Runnable{
    @Override
    public void run() {
        try {
            SemaphoreOneTest.semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+"拿到了许可证");
            Thread.sleep(5000);
            
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
       		SemaphoreOneTest.semaphore.release();
        }
    }
}

信号的释放必须放在finally块中,否则程序发生异常不会释放许可证

正确释放许可证结果:
在这里插入图片描述

错误释放许可证结果:
在这里插入图片描述

带有参数的许可证演示:

public class SemaphoreOneTest {
    // 允许开放两个人去操作
    static Semaphore semaphore = new Semaphore(2);
    public static void main(String[] args) throws InterruptedException {
        new Thread(new SemaTaskOne()).start();
        new Thread(new SemaTaskTwo()).start();
        System.out.println(Thread.currentThread().getName()+"等待中,Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
        semaphore.acquire(2);
        System.out.println(Thread.currentThread().getName()+"拿到了许可证"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
        Thread.sleep(4000);
        semaphore.release(2);
        System.out.println("Main Finish");
    }
}

class SemaTaskOne implements Runnable{
    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName()+"等待中,Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
            SemaphoreOneTest.semaphore.acquire(2);
            System.out.println(Thread.currentThread().getName()+"拿到了许可证"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
            Thread.sleep(4000);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            SemaphoreOneTest.semaphore.release(2);
        }
    }
}

class SemaTaskTwo implements Runnable{

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName()+"等待中,Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
            SemaphoreOneTest.semaphore.acquire(2);
            System.out.println(Thread.currentThread().getName()+"拿到了许可证"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
            Thread.sleep(4000);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            SemaphoreOneTest.semaphore.release(2);
        }
    }
}

演示结果:
在这里插入图片描述

小结

非阻塞的尝试拿取就不做演示了,大体上差不多。只不过不会阻塞,也可以设置超时时间的拿,如果在这段时间拿不到的话线程就继续往下执行

Semaphore在使用的过程中,一定要保证许可证能正确的释放,否则会出现死锁的问题导致程序无法运行下去。

Condition

Condition本质上是一个接口,所以只能实例化其子类或者根据需求重写对应的方法

使用

其实跟Object的wait和notify方法类似,Condition提供了await()signal()。在await()中提供了可设置超时时间的重载方法。

代码

下面用Condition来演示一个生产者和消费者模式


public class ConditionTest {

    static ReentrantLock reentrantLock = new ReentrantLock();
    // 生产者锁
    static Condition conditionProvier =  reentrantLock.newCondition();
    // 消费者锁
    static Condition conditionConsumer =  reentrantLock.newCondition();
    static Queue arrayList = new ArrayBlockingQueue(10);

    public static void main(String[] args) {

        new Thread(new ConditionTask()).start();
        new Thread(new ConditionTaskTwo()).start();
    }
}

// 消费者
class ConditionTask implements Runnable{
    @Override
    public void run() {
        while(true) {
            try {
                ConditionTest.reentrantLock.lock();
                if (ConditionTest.arrayList.size() <= 0) {
                    System.out.println("无了,先不拿");
                    ConditionTest.conditionConsumer.await();
                }
                int poll = (int) ConditionTest.arrayList.poll();
                System.out.println("弹出元素," + poll);
                ConditionTest.conditionProvier.signalAll();
            } catch (Exception e) {
            } finally {
                ConditionTest.reentrantLock.unlock();
            }
        }
    }
}

// 生产者
class ConditionTaskTwo implements Runnable{
    int i = 0;
    @Override
    public void run() {
        while(true) {
            try {
                ConditionTest.reentrantLock.lock();
                //Thread.sleep((long) (Math.random() * 10000));
                if (ConditionTest.arrayList.size() >= 10) {
                    System.out.println("满了,暂停一下");
                    ConditionTest.conditionProvier.await();
                }
                System.out.println("插入元素");
                ConditionTest.arrayList.offer(i++);
                ConditionTest.conditionConsumer.signalAll();
            } catch (Exception e) {
            } finally {
                ConditionTest.reentrantLock.unlock();
            }
        }
    }
}

结果演示:
在这里插入图片描述

CyclicBarrier

CyclicBarrier比较容易理解,凑够了一拨人就出发

使用:

  • 只需要要在构造参数中传入要凑够多少人(线程),具体线程调用await()方法,如果凑够了指定数的线程就一起出发。
  • 也可以传入凑够的人数和执行线程;这个执行线程是凑够人数后,await()线程会继续往下执行,当前线程开启执行线程,相当于多一个线程用于提醒或执行其他操作
  • 后面凑不够指定的也会全部出发了

代码演示

无执行线程

public class CyclicBarrierOneTask {

    public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
		// 一次凑够5个人出发
        CyclicBarrier cyclicBarrierWithOutRunnable = new CyclicBarrier(5);
		// 开10个线程
        for (int i = 0; i < 10; i++) {
            new Thread(new CyclicBarrierSmailTask(cyclicBarrierWithOutRunnable)).start();
        }
    }
}

class CyclicBarrierSmailTask implements Runnable{
    private  CyclicBarrier cyclicBarrier;

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

    @Override
    public void run() {
        try {
            // 让线程随机休眠,显示出先来后到的凑够一拨人
            long time = (long) (Math.random() * 10000);
            Thread.sleep(time);
            System.out.println(Thread.currentThread().getName()+"等待中");
            cyclicBarrier.await();
            System.out.println(Thread.currentThread().getName()+"出发了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

在这里插入图片描述

增加执行线程

public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5,new Runnable(){
            @Override
            public void run() {
                System.out.println("凑够了一拨人,出发");
            }
        });
        for (int i = 0; i < 10; i++) {
            new Thread(new CyclicBarrierSmailTask(cyclicBarrier)).start();
        }
    }

结果演示:
在这里插入图片描述

当调用await()的线程数达到指定数时,就会出发执行线程。并且之前调用await()的线程会一起运行,不在阻塞。

小结

合理运用好线程控制工具可以很好的帮助我们使用线程的合作,如果需要搞懂内部原理的话需要对AQS进一步掌握。

因为里面的控制数就是通过AQS中的state类变量来计数。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值