代码改变世界

Java多线程20:多线程下的其他组件之CountDownLatch、Semaphore、Exchanger

2017-11-28 13:08  faunjoe88  阅读(212)  评论(0编辑  收藏  举报

前言

在多线程环境下,JDK给开发者提供了许多的组件供用户使用(主要在java.util.concurrent下),使得用户不需要再去关心在具体场景下要如何写出同时兼顾线程安全性与高效率的代码。之前讲过的线程池、BlockingQueue都是在java.util.concurrent下的组件,Timer虽然不在java.util.concurrent下,但也算是。后两篇文章将以例子的形式简单讲解一些多线程下其他组件的使用,不需要多深刻的理解,知道每个组件大致什么作用就行。

本文主要讲解的是CountDownLatch、Semaphore、Exchanger。

 

CountDownLatch

CountDownLatch主要提供的机制是当多个(具体数量等于初始化CountDownLatch时count参数的值)线程都达到了预期状态或完成预期工作时触发事件,其他线程可以等待这个事件来触发自己的后续工作。值得注意的是,CountDownLatch是可以唤醒多个等待的线程的。

到达自己预期状态的线程会调用CountDownLatch的countDown方法,等待的线程会调用CountDownLatch的await方法。如果CountDownLatch初始化的count值为1,那么这就退化为一个单一事件了,即是由一个线程来通知其他线程,效果等同于对象的wait和notifyAll,count值大于1是常用的方式,目的是为了让多个线程到达各自的预期状态,变为一个事件进行通知,线程则继续自己的行为。

看一个例子:

private static class WorkThread extends Thread
{
    private CountDownLatch cdl;
    private int sleepSecond;
        
    public WorkThread(String name, CountDownLatch cdl, int sleepSecond)
    {
        super(name);
        this.cdl = cdl;
        this.sleepSecond = sleepSecond;
    }
        
    public void run()
    {
        try
        {
            System.out.println(this.getName() + "启动了,时间为" + System.currentTimeMillis());
            Thread.sleep(sleepSecond * 1000);
            cdl.countDown();
            System.out.println(this.getName() + "执行完了,时间为" + System.currentTimeMillis());
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}
    
private static class DoneThread extends Thread
{
    private CountDownLatch cdl;
        
    public DoneThread(String name, CountDownLatch cdl)
    {
        super(name);
        this.cdl = cdl;
    }
        
    public void run()
    {
        try
        {
            System.out.println(this.getName() + "要等待了, 时间为" + System.currentTimeMillis());
            cdl.await();
            System.out.println(this.getName() + "等待完了, 时间为" + System.currentTimeMillis());
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}
    
public static void main(String[] args) throws Exception
{
    CountDownLatch cdl = new CountDownLatch(3);
    DoneThread dt0 = new DoneThread("DoneThread1", cdl);
    DoneThread dt1 = new DoneThread("DoneThread2", cdl);
    dt0.start();
    dt1.start();
    WorkThread wt0 = new WorkThread("WorkThread1", cdl, 2);
    WorkThread wt1 = new WorkThread("WorkThread2", cdl, 3);
    WorkThread wt2 = new WorkThread("WorkThread3", cdl, 4);
    wt0.start();
    wt1.start();
    wt2.start();
}

看一下运行结果:

DoneThread2要等待了, 时间为1444563077434
DoneThread1要等待了, 时间为1444563077434
WorkThread1启动了,时间为1444563077434
WorkThread3启动了,时间为1444563077435
WorkThread2启动了,时间为1444563077435
WorkThread1执行完了,时间为1444563079435
WorkThread2执行完了,时间为1444563080435
WorkThread3执行完了,时间为1444563081435
DoneThread1等待完了, 时间为1444563081435
DoneThread2等待完了, 时间为1444563081435

效果十分明显,解释一下:

1、启动2个线程DoneThread线程等待3个WorkThread全部执行完

2、3个WorkThread全部执行完,最后执行完的WorkThread3执行了秒符合预期

3、后三句从时间上看几乎同时出现,说明CountDownLatch设置为3,WorkThread3执行完,两个wait的线程马上就执行后面的代码了

这相当于是一种进化版本的等待/通知机制,它可以的实现的是多个工作线程完成任务后通知多个等待线程开始工作,之前的都是一个工作线程完成任务通知一个等待线程或者一个工作线程完成任务通知所有等待线程。

CountDownLatch其实是很有用的,特别适合这种将一个问题分割成N个部分的场景,所有子部分完成后,通知别的一个/几个线程开始工作。比如我要统计C、D、E、F盘的文件,可以开4个线程,分别统计C、D、E、F盘的文件,统计完成把文件信息汇总到另一个/几个线程中进行处理

 

Semaphore

Semaphore是非常有用的一个组件,它相当于是一个并发控制器,是用于管理信号量的。构造的时候传入可供管理的信号量的数值,这个数值就是控制并发数量的,我们需要控制并发的代码,执行前先通过acquire方法获取信号,执行后通过release归还信号 。每次acquire返回成功后,Semaphore可用的信号量就会减少一个,如果没有可用的信号,acquire调用就会阻塞,等待有release调用释放信号后,acquire才会得到信号并返回。

Semaphore分为单值和多值两种:

1、单值的Semaphore管理的信号量只有1个,该信号量只能被1个,只能被一个线程所获得,意味着并发的代码只能被一个线程运行,这就相当于是一个互斥锁了

2、多值的Semaphore管理的信号量多余1个,主要用于控制并发数

看一下代码例子:

public static void main(String[] args)
{
    final Semaphore semaphore = new Semaphore(5);
        
    Runnable runnable = new Runnable()
    {
        public void run()
        {
            try
            {
                semaphore.acquire();                    
          System.out.println(Thread.currentThread().getName() + "获得了信号量,时间为" + System.currentTimeMillis());
                Thread.sleep(2000);
          System.out.println(Thread.currentThread().getName() + "释放了信号量,时间为" + System.currentTimeMillis());
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            finally
            {
                semaphore.release();
            }
        }
    };
    
    Thread[] threads = new Thread[10];
    for (int i = 0; i < threads.length; i++)
        threads[i] = new Thread(runnable);
    for (int i = 0; i < threads.length; i++)
        threads[i].start();
}

看一下运行结果:

Thread-1获得了信号量,时间为1444557040464
Thread-2获得了信号量,时间为1444557040465
Thread-0获得了信号量,时间为1444557040464
Thread-3获得了信号量,时间为1444557040465
Thread-4获得了信号量,时间为1444557040465
Thread-2释放了信号量,时间为1444557042466
Thread-4释放了信号量,时间为1444557042466
Thread-0释放了信号量,时间为1444557042466
Thread-1释放了信号量,时间为1444557042466
Thread-3释放了信号量,时间为1444557042466
Thread-9获得了信号量,时间为1444557042467
Thread-7获得了信号量,时间为1444557042466
Thread-6获得了信号量,时间为1444557042466
Thread-5获得了信号量,时间为1444557042466
Thread-8获得了信号量,时间为1444557042467
Thread-9释放了信号量,时间为1444557044467
Thread-6释放了信号量,时间为1444557044467
Thread-7释放了信号量,时间为1444557044467
Thread-5释放了信号量,时间为1444557044468
Thread-8释放了信号量,时间为1444557044468

前10行为一部分,运行的线程是1 2 0 3 4,看到时间差也都是代码约定的2秒;后10行为一部分,运行的线程是9 7 6 5 8,时间差也都是约定的2秒,这就体现出了Semaphore的作用了。

这种通过Semaphore控制并发并发数的方式和通过控制线程数来控制并发数的方式相比,粒度更小,因为Semaphore可以通过acquire方法和release方法来控制代码块的并发数。

最后注意两点:

1、Semaphore可以指定公平锁还是非公平锁

2、acquire方法和release方法是可以有参数的,表示获取/返还的信号量个数

 

Exchanger

Exchanger,从名字上理解就是交换。Exchanger用于在两个线程之间进行数据交换,注意也只能在两个线程之间进行数据交换。线程会阻塞在Exchanger的exchange方法上,直到另外一个线程也到了同一个Exchanger的exchange方法时,二者进行数据交换,然后两个线程继续执行自身相关的代码。

Exchanger只有一个exchange方法,用于交换数据。看一下例子:

public static class ExchangerThread extends Thread
{
    private String str;
    private Exchanger<String> exchanger;
    private int sleepSecond;
    
    public ExchangerThread(String str, Exchanger<String> exchanger, int sleepSecond)
    {
        this.str = str;
        this.exchanger = exchanger;
        this.sleepSecond = sleepSecond;
    }
        
    public void run()
    {
        try
        {
            System.out.println(this.getName() + "启动, 原数据为" + str + ", 时间为" + System.currentTimeMillis());
            Thread.sleep(sleepSecond * 1000);
            str = exchanger.exchange(str);
            System.out.println(this.getName() + "交换了数据, 交换后的数据为" + str + ", 时间为" + System.currentTimeMillis());
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}
    
public static void main(String[] args)
{
    Exchanger<String> exchanger = new Exchanger<String>();
    ExchangerThread et0 = new ExchangerThread("111", exchanger, 3);
    ExchangerThread et1 = new ExchangerThread("222", exchanger, 2);
    et0.start();
    et1.start();
}

看一下运行结果:

Thread-0启动, 原数据为111, 时间为1444560972303
Thread-1启动, 原数据为222, 时间为1444560972303
Thread-0交换了数据, 交换后的数据为222, 时间为1444560975303
Thread-1交换了数据, 交换后的数据为111, 时间为1444560975303

看到两个线程交换了数据,由于一个线程睡2秒、一个线程睡3秒,既然要交换数据,肯定是睡2秒的要等待睡3秒的,所以看到时间差是3000ms即3s。

从这个例子看来,Exchanger有点像之前Java多线程15:Queue、BlockingQueue以及利用BlockingQueue实现生产者/消费者模型一文中的SynchronousQueue的双向形式,它可能在遗传算法和管道设计中很有用。