线程同步辅助类

转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/72354208
本文出自:【顾林海的博客】


前言

关于线程的基础知识可以查看《有关线程的相关知识(上)》和《有关线程的相关知识(下)》,线程同步synchronized和Lock可以查看《线程同步synchronized》和《线程同步Lock》,在并发工具类中提供了更加高级的同步机制来实现多线程间的同步,本篇文章就来讲解这些高级的同步机制如何使用。


CountDownLatch

CountDownLatch类是一个同步辅助类。CountDownLatch提供了一个带整型参数的构造器,这个整数就是用于线程要等待完成的操作的数目。当一个线程要等待某些操作先完成时,可以调用await()方法,这个方法让线程进入休眠直到等待的所有操作都完成,当某个操作完成后,调用countDown()方法将CountDownLatch类的内部计数器减1,当计数器为0时,CountDownLatch类将唤醒所有调用await()方法而进入休眠的线程。下面通过一个例子来展示CountDownLatch的用法。

public class Task implements Runnable {

    private CountDownLatch startCountDownLatch;
    private CountDownLatch endCountDownLatch;

    public Task(CountDownLatch start,CountDownLatch end) {
        this.startCountDownLatch=start;
        this.endCountDownLatch=end;
    }

    @Override
    public void run() {

        try {
            System.out.println("堂主:老大,有何吩咐...");
            TimeUnit.SECONDS.sleep(2);
            /*
             * 当startCountDownLatch内部计数为0时,
             * 将唤醒所有调用startCountDownLatch.wait()方法
             * 当线程,并继续执行下去。
             */
            startCountDownLatch.await();
            System.out.println("堂主:老大,我们分堂已经完成...");
            TimeUnit.SECONDS.sleep(2);
            endCountDownLatch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

创建一个实现了Runnable接口的Task类,内部定义两个CountDowanLatch实例,并通过构造器初始化,重写了run()方法,我们在run()方法中,可以
看到在调用了startCountDownLatch.await()时,如果startCountDownLatch内部计数器不为0,该线程就会进入休眠状态,在此之前会输出”堂主:老大,有何吩咐…”。如果startCountDownLatch内部计数器为0,就会唤醒该线程,继续执行下去并输出”堂主:老大,我们分堂已经完成…”,接着调用endCountDownLatch.countDown()方法,使得endCountDownLatch内部计数器减1,当endCountDownLatch内部计数器为0时,就会唤醒endCountDownLatch调用await()方法所在的线程。

public class Client {

    public static void main(String[] args) {
        CountDownLatch start=new CountDownLatch(1);
        CountDownLatch end=new CountDownLatch(3);
        Task task=new Task(start, end);
        ExecutorService executor=Executors.newFixedThreadPool(3);
        for(int i=0;i<3;i++){
            executor.execute(task);
        }
        System.out.println("老大吩咐三个堂主做任务...");
        try {
            TimeUnit.SECONDS.sleep(2);
            start.countDown();
            System.out.println("剩下的就等消息了...");
            end.await();
            System.out.println("老大:大家完成的不错...");
            executor.shutdownNow();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

在主类中,定义了两个CountDownLatch实例,并给start指定了计数器起始为1,end计数器起始为3,最后将这两个实例通过 实例化Task时传入,接下来创建线程池来执行3个线程,从上面代码可以看出,当主线程的start.countDown调用时,由于start在初始化时就已经指定了计数器为1,因此这里执行了countDown()方法后,所有调用了await()方法的线程将被唤醒。接着又调用了end的await()方法,这时主线程处于休眠状态,除非end内部计数器为0,才会继续执行下去,在上面的代码中end在初始化时就已经指定了计数器为3,而上面代码中开启了3个线程并在这三个线程中调用了countDown()方法,正好执行了3此,end内部计数器为0。

最后我们看看输出结果:

老大吩咐三个堂主做任务…
堂主:老大,有何吩咐…
堂主:老大,有何吩咐…
堂主:老大,有何吩咐…
剩下的就等消息了…
堂主:老大,我们分堂已经完成…
堂主:老大,我们分堂已经完成…
堂主:老大,我们分堂已经完成…
老大:大家完成的不错…


CyclicBarrier

CyclicBarrier称为同步屏障,也是一个同步辅助类,允许一组线程彼此互相等待,直到到达某个公共的屏障点,此屏障在等待线程被释放之后可重用,所以称它为可循环使用的屏障。

CyclicBarrier类使用一个整型数进行初始化,这个数是需要在某个点上同步的线程数,当一个线程到达指定的点后,调用await()方法等待其他的线程,当线程调用await()方法后,CyclicBarrier类将阻塞这个线程并使之休眠知道所有其他线程到达。

CyclicBarrier可以传入一个Runnable对象作为初始化参数,当所有线程都到达集合点之后,CyclicBarrier类将这个Runnable对象作为线程执行。

public class Worker implements Runnable {

    private String taskName;
    private CyclicBarrier cyclicBarrier;

    public Worker(String name,CyclicBarrier barrier) {
        this.taskName=name;
        this.cyclicBarrier=barrier;
    }

    @Override
    public void run() {
        try {
            work();
            cyclicBarrier.await();
            other();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }

    public void work(){
        System.out.println(taskName+"正在执行...");
    }

    public void other(){
        System.out.println(taskName+"执行完毕,接下来执行其他任务...");
    }

}

创建了一个实现Runnable接口的Worker类,定义了一个taskName用以表示工作名称,又创建了一个CyclicBarrier对象,并在构造器中初始化,在run()方法中,先是执行work()方法,接着调用屏障CyclicBarrier的await()方法,等待其他线程的到达,当所有线程到达后,执行other()方法。

public class Client {

    public static void main(String[] args) {
        // 模拟创建十个任务
        ArrayList<String> taskList = new ArrayList<>();
        for (int i = 1; i <= 5; i++) {
            taskList.add("任务" + i);
        }

        CyclicBarrier barrier = new CyclicBarrier(taskList.size(), new Runnable() {

            @Override
            public void run() {
                System.out.println("10个任务都执行完毕了...");
            }
        });

        for (int i = 0, length = taskList.size(); i < length; i++) {
            new Thread(new Worker(taskList.get(i), barrier)).start();
        }
    }

}

在主类中,定义了10个任务,并创建了一个CyclicBarrier对象,在实例化CyclicBarrier时,指定了同步线程数为10,以及一个Runnable对象,当所有调用CyclicBarrier的await()方法的线程到达集合点之后,就会将Runnable对象作为线程来执行。最后创建开启10个线程。

输出结果:

任务2正在执行…
任务5正在执行…
任务4正在执行…
任务3正在执行…
任务1正在执行…
10个任务都执行完毕了…
任务1执行完毕,接下来执行其他任务…
任务2执行完毕,接下来执行其他任务…
任务4执行完毕,接下来执行其他任务…
任务5执行完毕,接下来执行其他任务…
任务3执行完毕,接下来执行其他任务…


Exchanger

Exchanger是Java并发API提供的一个同步辅助类,允许在并发任务之间交换数据,Exchanger类允许在两个线程之间定义同步点,当两个线程都到达同步点时,它们交换数据结构,因此第一个线程的数据结构进入到第二个线程中,同时第二个线程的数据结构进入到第一个线程中。

下面介绍一下交换器交换的几种方式:

1public V exchange(V x) throws InterruptedException

    在这个交互点上等待其他线程到达(除非调用线程被中断),之后将所给对象传入其中,接收其他线程的对象作为返回,如果其他的线程已经在交换点上等待,为了线程调度它会从中恢复并且接受调用线程所传入的对象。当前线程会立即返回,接收其他线程传入交换器中的对象。当调用线程被中断,会抛出InterruptedException异常。



(2)public V exchange(V x, long timeout, TimeUnit unit)
        throws InterruptedException, TimeoutException

    该方法等同于上面的方法,只是在等待时会指定等待时间,一旦超时,会抛出TimeoutException异常。

下面实现一个生产者和消费者的问题。

生产者:

public class Producer implements Runnable{

    private List<String> buffer;

    private Exchanger<List<String>> exchanger;

    public Producer(List<String> _buffer,Exchanger<List<String>> _exchanger){
        this.buffer=_buffer;
        this.exchanger=_exchanger;
    }

    @Override
    public void run() {
        for(int i=0;i<5;i++){
            for(int j=0;j<5;j++){
                buffer.add("buffer"+j);
            }
            try {
                buffer=exchanger.exchange(buffer);
                System.out.println("生产者:交换完毕后缓冲区数据还剩"+buffer.size()+"个");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }


}



消费者:

public class Consumer implements Runnable {

    private List<String> buffer;

    private Exchanger<List<String>> exchanger;

    public Consumer(List<String> _buffer, Exchanger<List<String>> _exchanger) {
        this.buffer = _buffer;
        this.exchanger = _exchanger;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                buffer = exchanger.exchange(buffer);
                System.out.println("消费者:交换完毕后缓冲区数据还剩" + buffer.size() + "个");
                buffer.clear();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}



实现类:

public class Client {

    public static void main(String[] args) {

        List<String> buffer1=new ArrayList<>();
        List<String> buffer2=new ArrayList<>();

        Exchanger<List<String>> exchanger=new Exchanger<>();

        Producer producer=new Producer(buffer1, exchanger);
        Consumer consumer=new Consumer(buffer2, exchanger);

        Thread producerThread=new Thread(producer);
        Thread consumerThread=new Thread(consumer);

        producerThread.start();
        consumerThread.start();

    }

}

在具体的场景类中定义了两个buffer分别用于生产者和消费者,定义了Exchanger对象,用来同步生产者和消费者,在生产者线程中,循环5次,每次循环向buffer中添加5个数据,添加完毕后,通过exchange方法将数据传入消费者线程中去,在消费者线程中,循环5次,每次循环都将空的 buffer穿入给生产者线程中,并且拿到生产者线程传入到buffer,在获取生产者线程传入的buffer后,进行消费掉。下图的序列图很好的说明了使用exchange方法生产者线程和消费者线程的数据的同步。

Created with Raphaël 2.1.0 生产者 生产者 消费者 消费者 添加10个数据到缓存列表,并通过exchange方法传递给消费者线程 将空的缓存列表传入生产者线程中

运行程序:

消费者:交换完毕后缓冲区数据还剩5个
生产者:交换完毕后缓冲区数据还剩0个
生产者:交换完毕后缓冲区数据还剩0个
消费者:交换完毕后缓冲区数据还剩5个
生产者:交换完毕后缓冲区数据还剩0个
消费者:交换完毕后缓冲区数据还剩5个
生产者:交换完毕后缓冲区数据还剩0个
消费者:交换完毕后缓冲区数据还剩5个
生产者:交换完毕后缓冲区数据还剩0个
消费者:交换完毕后缓冲区数据还剩5个

运行时输出的顺序可能不一样,但这不妨碍我们验证生产者与消费者的数据同步问题,查看输出结果,可以发现每次生产者的5个数据与消费者的空数据交换后,生产者打印缓冲数为0,在消费者线程中获取生产者数据后打印,发现缓冲数据为5并再次请空缓冲区,这样一直反复(生产者生产数据,消费者消费数据)。


Semaphore(信号量)

信号量是一种计数器,用来保护一个或者多个共享资源的访问。信号量内部的计数器大于0说明有可以使用的资源,当信号量的计数器等于0,信号量将会把线程置入休眠直到计数器大于0。当线程使用完某个共享资源时,信号量必须被释放,以便其它线程能够访问共享资源,释放时信号量计数器增加1。下面给出信号量的相关方法:

1public void acquire() throws InterruptedException

    从这个信号量中获取一个许可证,否则阻塞直到有一个许可证可用或者调用线程被中断。当调用线程中断,抛出InterruptedException。

(2)public void acquire(int permits) throws InterruptedException

    从这个信号量中获取permits数量的许可证,否则阻塞直到这些许可证可用或者调用线程被中断。当调用线程中断,抛出InterruptedException,当permits小于0时,抛出IllegalArgumentException。

(3)public void acquireUninterruptibly()

    获取一个许可证,否则阻塞直到有一个许可证可用。

(4)public void acquireUninterruptibly(int permits)

    获取permits个许可证,否则阻塞直到这些许可证可用,当permits小于0,抛出IllegalArgumentException。

(5)public int availablePermits()

    返回当前可用许可证的数目。该方法适用于调试和测试。

(6)public int drainPermits()

    获取并返回立即可用的许可证的数量。

(7)public final int getQueueLength()

    返回等待获取许可证的大致线程数。由于线程数在该方法遍历内部数据结构的时候可能会改变,返回的值只能是估算值。

(8)public boolean isFair()

    返回公平性设置(公平返回true,不公平返回false)。

(9)public void release()

    释放一个许可证,将其返回给信号量。可用许可证的数目增加1。如果任何线程正在尝试获取一个许可证,被选到的线程就会被给予刚刚释放的许可证。那条线程就会因为线程调度而被重新启用。

(10)public boolean tryAcquire()

    仅当调用时有一个许可证可用的情况,才能从这个信号量中获取这个许可证。当获取许可证后返回true,否则,立刻返回false。

(11)public boolean tryAcquire(int permits)

    仅当调用时有permits个许可证可用的情况,才能从这个信号量中获取这些许可证。当获取到这些许可证后,返回true,否则立即返回false。当permits小于0时,抛出IllegalArgumentException。

(12)public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException

    仅当调用时有permits个许可证可用的情况,才能从这个信号量中获取这些许可证。当permits个许可证尚不可用时,调用线程会等待。当全部许可证可用时等待结束。若timeout超时或者调用线程中断,抛出InterruptedException。

(13)public boolean tryAcquire(long timeout, TimeUnit unit)
        throws InterruptedException

    和tryAcquire(int permits)方法类似,不过调用线程会一直等待直到有一个许可证可用。当许可证可用时,等待结束。若timeout超时或者调用线程中断,抛出InterruptedException。

下面使用信号量来模拟打印机的打印队列:

public class PrintQueue {

    //声明一个信号量对象semaphore
    private final Semaphore semaphore;
    //用于存放打印机的状态
    private boolean freePrinters[];
    //声明一个锁对象
    private Lock lock;

    public PrintQueue(){
        semaphore=new Semaphore(3);
        freePrinters=new boolean[3];
        lock=new ReentrantLock();
        for(int i=0;i<3;i++){
            freePrinters[i]=true;
        }
    }

    public void printJob(String task){
        try {
            //获取信号量
            semaphore.acquire();
            //获取打印编号
            int assignedPrinter=getPrinter();
            //模拟耗时的打印任务
            System.out.println("模拟耗时的打印任务..."+new Date());
            TimeUnit.SECONDS.sleep(2);
            freePrinters[assignedPrinter]=true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            semaphore.release();
        }
    }

    private int getPrinter(){
        int number=-1;
        try{
            lock.lock();
            for(int i=0,length=freePrinters.length;i<length;i++){
                if(freePrinters[i]){
                    number=i;
                    //该打印机执行打印任务
                    freePrinters[i]=false;
                    break;
                }
            }
        }catch(Exception e){

        }finally{
            lock.unlock();
        }
        return number;
    }

}
public class Job implements Runnable{

    private PrintQueue printQueue;

    public Job(PrintQueue _printQueue){
        this.printQueue=_printQueue;
    }

    @Override
    public void run() {
        printQueue.printJob("打印文档");
    }

}
public class Client {

    public static void main(String[] args) {

        PrintQueue printQueue=new PrintQueue();

        Thread thread[]=new Thread[10];

        for(int i=0;i<10;i++){
            thread[i]=new Thread(new Job(printQueue));
        }
        for(int i=0;i<10;i++){
            thread[i].start();;
        }
    }

}

上面代码实现了一个打印队列被三个不同当打印机使用,在打印队列类中定义了一个内部计数器为3的信号量,当调用acquire方法的3个线程将获得对临界区的访问,其它线程将被阻塞。

运行结果:

模拟耗时的打印任务…Wed May 17 23:06:38 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:38 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:38 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:40 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:40 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:40 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:42 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:42 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:42 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:44 CST 2017


Phaser

Phaser是一个更加弹性的同步屏障,一个phaser使得一组线程在屏障上等待,在最后一条线程到达之后,这些线程得以继续执行。具体使用请用户查阅相关文档。

总结

CyclicBarrier和CountDownLatch有很多共性,但它们之间有一定的差异。其中最为明显的是CyclicBarrier对象可以被重置回初始化状态,并把它的内部计数器重置成初始化时的值,而CountDownLatch只准许进入一次,一旦CountDownLatch的内部计数器为0,在调用这个方法将不起作用。
使用Semaphore(信号量)时,定义内部计数器只有0和1两个值时,称这种信号量为二进制信号量,这种信号量可以保护对单一共享资源,或者单一临界区的访问,从而使得保护的资源在同一个时间内只能被一个线程访问。上面信号量例子实现了保护一个资源的多个副本。

©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:上身试试 返回首页