Java同步工具类

本文转自:https://blog.csdn.net/caoyue_new/article/details/75012794

同步工具类可以是任意一个对象,只要它可以根据自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量(Semaphore)、栅栏(Barrier)以及闭锁。在平台类库中还包含一些其他同步工具类,如果还是不能满足需要,我们可以创建自己的同步工具类。

一、闭锁

闭锁可以延迟线程的进度直到其达到终止状态。闭锁可以用来确保某些活动直到其他活动都完成后才继续执行。例如:

  • 某个计算在其需要的资源都初始化完成之后执行;
  • 某个服务在其所有依赖的服务都启动之后才启动;
  • 游戏中所有的玩家都就绪才继续执行。

CountDownLatch是一种灵活的闭锁实现,它可以使一个或多个线程等待一组时间发生。闭锁状态包括一个计数器,该计数器初始化为一个正数,表示需要等待的事件数量,countDown方法表示递减计数器,表示一个事件发生了,而await方法等待直到计数器为0,表示所有事件都已经发生。如果计数器的值非零,那么就会一直等待下去,或者等待中被打断,或者超时。
例子(计算所有线程运行时间):

import java.util.concurrent.CountDownLatch;

public class TestHarness {

    public long timeTasks(int nThread) throws InterruptedException {
        final CountDownLatch startGate = new CountDownLatch(1);
        final CountDownLatch endGate = new CountDownLatch(nThread);

        Thread t;
        for(int i=0; i<nThread; i++) {
            t = new Thread() {
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName() + " ready....");
                        startGate.await();
                        try {
                            System.out.println(Thread.currentThread().getName() + " running ....");
                        } finally {
                            endGate.countDown();
                        }
                    } catch (InterruptedException ignored) {
                    }
                }
            };

            t.start();
        }

        Thread.sleep(2000);

        long start = System.nanoTime();
        System.out.println("xxxxx");
        startGate.countDown();
        endGate.await();
        long end = System.nanoTime();
        return end - start;
    }

    public static void main(String[] args) throws InterruptedException {
        TestHarness test = new TestHarness();
        long time = test.timeTasks(5);
        System.out.println("time spent: " + time);
    }

}

运行结果:

Thread-0 ready....
Thread-1 ready....
Thread-2 ready....
Thread-3 ready....
Thread-4 ready....
xxxxx
Thread-1 running ....
Thread-0 running ....
Thread-4 running ....
Thread-3 running ....
Thread-2 running ....
time spent: 502278

上例中使用了两个闭锁,一个起始门(startGate),一个结束门(endGate)。起始门的计数器值初始化为1,结束门是线程数,每个线程首先要做的就是在起始门上等待多有的线程都就绪后才开始执行。而每个线程最后要做的事就是调用结束门countDown方法减1,这能高效的等待所有的线程都工作完成,这样可以统计消耗的时间。

其他方法

如果有某个线程处理的比较慢,我们不可能让主线程一直等待,所以我们可以使用另外一个带指定时间的await方法,await(long time, TimeUnit unit): 这个方法等待特定时间后,就会不再阻塞当前线程。join也有类似的方法。

注意:计数器必须大于等于0,只是等于0时候,计数器就是零,调用await方法时不会阻塞当前线程。CountDownLatch不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。

二、信号量

计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量。计数信号量还可以用来实现某种资源池(如:数据库连接池),或者对容器施加边界。
Semaphore中管理着一组虚拟的许可(permit),许可的数量可以通过构造器来指定。在执行操作时可以先获取许可(只要还有剩余的许可),并在使用之后释放许可。如果没有许可,那么aquire将阻塞指定获取许可(或者直到被中断或者操作超时)。release将返回一个许可给信号量。计算信号量的一种简化形式就是二值信号量,即初始值为1的Semaphore。二值信号量可以用作互斥体(mutex),并具备不可重入的语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。

例子(流量控制):
要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发的读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有十个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class SemaphoreTest {

    private static final int THREAD_COUNT = 12;

    private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);

    private static Semaphore s = new Semaphore(4);

    public static void main(String[] args) {
        for(int i=0; i<THREAD_COUNT; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        s.acquire();
                        System.out.println("save data");
                        Thread.sleep(new Random().nextInt(2000));
                        s.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        threadPool.shutdown();
    }

}

其他方法:

  • int availablePermits() :返回此信号量中当前可用的许可证数。
  • int getQueueLength():返回正在等待获取许可证的线程数。
  • boolean hasQueuedThreads() :是否有线程正在等待获取许可证。
  • void reducePermits(int reduction) :减少reduction个许可证。是个protected方法。
  • Collection getQueuedThreads() :返回所有等待获取许可证的线程集合。是个protected方法。

三、栅栏(同步屏障)

1. CyclicBarrier

栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个时间发生。栅栏和闭锁的区别在于,所有线程必须都到达栅栏位置之后才能继续执行。闭锁用于等待事件,二栅栏用于等待其他线程。
闭锁是一次性操作,一旦进入终止状态就不能重置。CyclicBarrier可以使一定数量的线程反复在栅栏位置汇集,它在并行迭代算法中非常有用:这种算法通常将一个问题拆分成多个互不相关的子问题。当线程执行到栅栏位置时将调用await方法等待其他线程,这个方法阻塞直到所有线程都到达栅栏位置。当所有线程都到达栅栏位置,那么栅栏打开,所有线程都被释放。而栅栏将被重置以便下次使用。如果await被中断或者超时,那么栅栏被认为是打破了,所有线程的await都将被终止并抛出BrokenBarrierException。如果成功的通过栅栏,那么await将为每个线程返回一个唯一的到达索引号,我们可以利用这些索引来“选举”产生一个领导线程,并在下一次迭代中由该领导线程执行一些特殊的工作。
CyclicBarrier还可以利用构造函数传递一个Runnable,当成功通过栅栏时会执行它,但在阻塞线程被释放前不会执行。

例子(10个人去春游,规定达到一个地点后才能继续前行)

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierWorker implements Runnable {

    private int id;
    private CyclicBarrier cyclicBarrier;

    public CyclicBarrierWorker(int id, CyclicBarrier cyclicBarrier) {
        this.id = id;
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        try {
            System.out.println(id + "th people wait, waiting " + cyclicBarrier.getNumberWaiting());
            int returnIndex = cyclicBarrier.await(); // 大家等待最后一个线程到达
            System.out.println(id + " th people go, returnIndex:" + returnIndex);
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        final int NUM = 10;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUM,new Runnable() {
            @Override
            public void run() {
                System.out.println("go on together!");
            }
        });

        for (int i=1; i<=NUM; i++) {
            new Thread(new CyclicBarrierWorker(i, cyclicBarrier)).start();
        }
    }

}

运行结果:

5th people wait, waiting 0
6th people wait, waiting 0
4th people wait, waiting 0
1th people wait, waiting 0
3th people wait, waiting 0
7th people wait, waiting 5
8th people wait, waiting 6
10th people wait, waiting 7
2th people wait, waiting 8
9th people wait, waiting 9
go on together!
9 th people go, returnIndex:0
4 th people go, returnIndex:7
6 th people go, returnIndex:8
5 th people go, returnIndex:9
1 th people go, returnIndex:6
3 th people go, returnIndex:5
7 th people go, returnIndex:4
8 th people go, returnIndex:3
10 th people go, returnIndex:2
2 th people go, returnIndex:1

2.Exchanger(两个线程进行数据交换)

另一种栅栏是Exchanger,它是一种两方(two-party)栅栏,各方在栅栏位置互换数据。当两方执行不对称操作时Exchanger会非常有用,例如一个线程向缓存中写数据,另一线程读数据,这两个线程可以使用Exchanger汇合,并将满的缓冲区和空的缓冲区互换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

例子(生产者和消费者交换数据):

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Exchanger;

/**
* 两个线程间的数据交换
*/
public class ExchangerDemo {

    private static final Exchanger<List<String>> ex = new Exchanger<List<String>>();

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
    * 内部类,数据生成者
    */
    class DataProducer implements Runnable {
        private List<String> list = new ArrayList<String>();

        @Override
        public void run() {
            System.out.println("生产者开始生产数据");
            for (int i = 1; i <= 5; i++) {
                System.out.println("生产了第" + i + "个数据,耗时1秒");
                list.add("生产者" + i);
                sleep(1000);
            }

            System.out.println("生产数据结束");
            System.out.println("开始与消费者交换数据");

            try {
                //将数据准备用于交换,并返回消费者的数据
                list = ex.exchange(list);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("结束与消费者交换数据");

            System.out.println("\n遍历生产者交换后的数据");
            for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
                System.out.println(iterator.next());
            }

        }

    }


    /**
     * 内部类,数据消费者
     */
    class DataConsumer implements Runnable {
        private List<String> list = new ArrayList<String>();

        @Override
        public void run() {
            System.out.println("消费者开始消费数据");
            for (int i = 1; i <= 5; i++) {
                System.out.println("消费了第" + i + "个数据");
                // 消费者产生数据,后面交换的时候给生产者
                list.add("消费者" + i);
             }

            System.out.println("消费数据结束");
            System.out.println("开始与生产者交换数据");

            try {
                // 进行数据交换,返回生产者的数据
                list = (List<String>) ex.exchange(list);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            sleep(1000);
            System.out.println("\n开始遍历消费者交换后的数据");
            for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
                System.out.println(iterator.next());
            }
        }

    }


    public static void main(String[] args) {
         ExchangerDemo et = new ExchangerDemo();
         new Thread(et.new DataProducer()).start();
         new Thread(et.new DataConsumer()).start();
    }

}

运行结果如下:

生产者开始生产数据
生产了第1个数据,耗时1秒
消费者开始消费数据
消费了第1个数据
消费了第2个数据
消费了第3个数据
消费了第4个数据
消费了第5个数据
消费数据结束
开始与生产者交换数据
生产了第2个数据,耗时1秒
生产了第3个数据,耗时1秒
生产了第4个数据,耗时1秒
生产了第5个数据,耗时1秒
生产数据结束
开始与消费者交换数据
结束与消费者交换数据

遍历生产者交换后的数据
消费者1
消费者2
消费者3
消费者4
消费者5

开始遍历消费者交换后的数据
生产者1
生产者2
生产者3
生产者4
生产者5

该例子来自:Java并发——同步工具类

其他方法

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

四、FutureTask

FutureTask也可以被用作闭锁(FutureTask实现了Future接口,实际上使用了多线程设计模式中的Future Pattern,之前写过,链接点击此处)。

Future.get()的行为取决于任务的状态。任务完成该方法会立即返回结果,否则该方法将进入阻塞状态,直到任务完成,然后返回结果或者抛出异常。FutureTask将计算结果从执行计算的线程传递到获取这个结果的线程,而Future的规范确保了这种传递过程能实现结果的安全发布。

Future是一个接口,下面约定了一个V call() throws Exception方法,Runnable接口则是约定了void run()方法。FutureTask实现了这两个接口,其中Callable的实现需要传入,而run()方法在FutureTask内部实现。我们传入Callable的实现,就可以使用了。通过FutureTask的get()方法就可以阻塞获得call()方法返回的计算结果。

FutureTask在Executor框架中表示异步任务,我们可以利用这个特点把FutureTask当作闭锁使用。举个例子:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class FutureTaskTest {

    public static void main(String[] args)  {
        long startTime = System.currentTimeMillis(); 
        System.out.println("主线程开始...");

        FutureTask<Integer> future = new FutureTask<>(new Task());
        System.out.println("进行Task任务计算的子线程开始...");
        new Thread(future).start();;

        try {
            System.out.println("主线程正在执行自己的任务...");
            Thread.sleep(1000);
            System.out.println("主线程尝试获取Task结果...");

            System.out.println("时间过去"+(System.currentTimeMillis()-startTime));
            System.out.println("主线程获取到结果为:"+future.get());
            System.out.println("时间过去"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

    }

}

class Task implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        //花3s模拟计算过程
        Thread.sleep(3000);
        //模拟计算结果是1
        return 1;
    }

}

运行结果如下:

主线程开始...
进行Task任务计算的子线程开始...
主线程正在执行自己的任务...
主线程尝试获取Task结果...
时间过去1003
主线程获取到结果为:1
时间过去3003

我们来分析一下代码和运行结果。我们在主线程创建FutureTask实例future,传入Callable接口。启动匿名线程new Thread(future)。然后模拟主线程花费1s的时间执行其他的任务,之后使用future.get()阻塞获取Task的任务执行结果。从尝试future.get()开始,2s之后获取到执行结果,最后主线程结束。

我们可以看到实际上我们可以使用FutureTask的实例future的get()方法作为一个门闩,在callable接口的call()方法执行完之前future.get()会一直阻塞,下面的代码不会执行。直到该方法获取到了结果,下面代码才会执行,因此也可以当作一个闭锁。

FutureTask 部分摘自: TimeTDIT - Java同步工具类

参考:

  • 《java并发编程实战》
  • 并发编程网

猜你喜欢

转载自blog.csdn.net/xxc1605629895/article/details/81106920