J.U.C--同步工具类

来源:互联网 发布:dotamax查不到数据 编辑:程序博客网 时间:2024/05/18 09:25

前面已经介绍了:同步容器类、并发容器类、阻塞队列(BlockingQueue),接下来介绍同步工具类。

本文主要介绍下面四种同步工具类:
1)CountDownLatch
2)FutureTask
3)Semaphore
4)CyclicBarrier

同步工具类可以是任何一个对象,只要根据自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类,其余的还包括:信号量(Semaphore)、栅栏(Barrier)以及闭锁(Latch)。

1.闭锁–CountDownLatch

闭锁(Latch)是一种同步工具类,可以延迟线程的进度,直到其到达终止状态。

闭锁相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程可以通过;当闭锁到达结束状态时,这扇门会打开并允许所有的线程通过。并且当闭锁到达结束状态后不可逆转,这扇门会一直保持打开的状态。

应用场景:确保某些活动直到其他活动都结束了才继续执行。

CountDownLatch是一种灵活的闭锁实现:这是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

CountDownLatch类的状态包括一个设置为初始正值的初始值计数值。
1、任何在此CountDownLatch对象上调用await()方法的方法都将阻塞,直到计数值到达0。
2、被等待的工作任务在其工作结束之后可以在run()方法中调用CountDownLatch对象上的countDown()方法,将计数值减一。

等待任务和工作任务使用的调用的必须是同一个CountDownLatch对象,所以这个对象我们一般通过构造器传入。当CountDownLatch对象的计数器变成0了之后,所有调用await()的等待任务都会被唤醒而执行。

下面是一个使用CountDownLatch的示例:

package thread;import java.util.concurrent.*;import java.util.*;/** * CountDownLatch的一个用法示例, * 在完成一组正在其他线程中执行的操作之前, * 它允许一个或多个线程一直等待 *//** * work线程 */class TaskPortion implements Runnable {    private static int counter = 0;    private final int id = counter++;//唯一id    private static Random rand = new Random(47);    private final CountDownLatch latch;    TaskPortion(CountDownLatch latch) {        this.latch = latch;    }    public void run() {        try {            doWork();            latch.countDown();//递减锁存器的计数,如果计数到达零,则释放所有等待的线程        } catch(InterruptedException ex) {            // Acceptable way to exit        }    }    private void doWork() throws InterruptedException {        //做任务耗时        TimeUnit.MILLISECONDS.sleep(rand.nextInt(2000));        System.out.println(this + "completed");    }    public String toString() {        return String.format("%1$-3d ", id);    }}/** * 等待线程 */class WaitingTask implements Runnable {    private static int counter = 0;    private final int id = counter++;//唯一id    private final CountDownLatch latch;    WaitingTask(CountDownLatch latch) {        this.latch = latch;    }    public void run() {        try {            latch.await();//使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。            System.out.println("Latch barrier passed for " + this);        } catch(InterruptedException ex) {            System.out.println(this + " interrupted");        }    }    public String toString() {        return String.format("WaitingTask %1$-3d ", id);    }}/** * main thread */public class CountDownLatchDemo {    static final int SIZE = 100;    public static void main(String[] args) throws Exception {        ExecutorService exec = Executors.newCachedThreadPool();        // All must share a single CountDownLatch object:        CountDownLatch latch = new CountDownLatch(SIZE);//构造一个用给定计数初始化的 CountDownLatch。        //10个等待线程        for(int i = 0; i < 10; i++){            exec.execute(new WaitingTask(latch));        }        //100个计数线程        for(int i = 0; i < 100; i++){            exec.execute(new TaskPortion(latch));        }        System.out.println("Launched all tasks");        exec.shutdown(); // Quit when all tasks complete    }}/** (Execute to see output)Launched all tasks95  completed47  completed>...................64  completed83  completed16  completedLatch barrier passed for WaitingTask 0   Latch barrier passed for WaitingTask 1   Latch barrier passed for WaitingTask 2   Latch barrier passed for WaitingTask 3   Latch barrier passed for WaitingTask 4   Latch barrier passed for WaitingTask 5   Latch barrier passed for WaitingTask 6   Latch barrier passed for WaitingTask 7   Latch barrier passed for WaitingTask 8   Latch barrier passed for WaitingTask 9  */

上面的例子中,先等待100个工作线程TaskPortion先执行完,等待它们执行完了之后,再执行WaitingTask等待的线程。从输出结果我们可以看出正确性。
github的完整源代码:

2. FutureTask

1.首先看看类的继承关系:

FutureTask

2.概念

先来看看JDK是怎么介绍这个类的:

可取消的异步计算。利用开始和取消计算的方法、查询计算是否完成的方法和获取计算结果的方法,此类提供了对 Future 的基本实现。仅在计算完成时才能获取结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。

可使用 FutureTask 包装 Callable 或 Runnable 对象。因为 FutureTask 实现了 Runnable,所以可将 FutureTask 提交给 Executor 执行。

从上面我们可以知道:FutureTask表示的计算可以处于以下3种状态之一:等待运行、正在运行和运行完成。注意这里的“执行完成”表示所有的可能结束方式:正常结束、由于取消而结束、由于异常而结束。当FutureTask到达完成状态后会一直停留在这个状态,也就是说FutureTask是一种一次性的计算。

3.FutureTask的应用场景:

1)FutureTask执行多任务计算的使用场景

利用FutureTask和ExecutorService,可以用多线程的方式提交计算任务,主线程继续执行其他任务,当主线程需要子线程的计算结果时,在异步获取子线程的执行结果。

package thread;/** * Created by louyuting on 17/1/11. */import java.util.ArrayList;import java.util.List;import java.util.concurrent.*;/** * FutureTask执行多任务计算的使用场景 */public class FutureTaskForMultiCompute {    public static void main(String[] args) {        FutureTaskForMultiCompute inst = new FutureTaskForMultiCompute();        // 创建任务集合        List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>();        // 创建线程池        ExecutorService exec = Executors.newFixedThreadPool(5);        for (int i = 0; i < 10; i++) {            // 传入Callable对象创建FutureTask对象            FutureTask<Integer> ft = new FutureTask<Integer>(inst.new ComputeTask(i, ""+i));            taskList.add(ft);            // 提交给线程池执行任务,也可以通过exec.invokeAll(taskList)一次性提交所有任务;            exec.submit(ft);        }        System.out.println("所有计算任务提交完毕, 主线程接着干其他事情!");        // 开始统计各计算线程计算结果        Integer totalResult = 0;        for (FutureTask<Integer> ft : taskList) {            try {                //FutureTask的get方法会自动阻塞,直到获取计算结果为止                totalResult = totalResult + ft.get();            } catch (InterruptedException e) {                e.printStackTrace();            } catch (ExecutionException e) {                e.printStackTrace();            }        }        // 关闭线程池        exec.shutdown();        System.out.println("多任务计算后的总结果是:" + totalResult);    }    /**     * 计算线程     */    private class ComputeTask implements Callable<Integer> {        private Integer result = 0;        private String taskName = "";        public ComputeTask(Integer iniResult, String taskName){            result = iniResult;            this.taskName = taskName;            System.out.println("生成子线程计算任务: "+taskName);        }        public String getTaskName(){            return this.taskName;        }        /**         * 带返回值的call调用         * @return         * @throws Exception         */        @Override        public Integer call() throws Exception {            for (int i = 1; i <= 100; i++) {                result =+ i;            }            // 休眠5秒钟,观察主线程行为,预期的结果是主线程会继续执行,到要取得FutureTask的结果是等待直至完成。            TimeUnit.SECONDS.sleep(1);            //Thread.sleep(5000);            System.out.println("子线程计算任务: "+taskName+" 执行完成!");            return result;        }    }}

github完成源代码地址

2) FutureTask在高并发环境下确保任务只执行一次

在很多高并发的环境下,往往我们只需要某些任务只执行一次。这种使用情景FutureTask的特性恰能胜任。举一个例子,假设有一个带key的连接池,当key存在时,即直接返回key对应的对象;当key不存在时,则创建连接。对于这样的应用场景,通常采用的方法为使用一个Map对象来存储key和连接池对应的对应关系,典型的代码如下面所示:

private Map<String, Connection> connectionPool = new HashMap<String, Connection>();private ReentrantLock lock = new ReentrantLock();public Connection getConnection(String key){    try{        lock.lock();        if(connectionPool.containsKey(key)){            return connectionPool.get(key);        }        else{            //创建 Connection            Connection conn = createConnection();            connectionPool.put(key, conn);            return conn;        }    }    finally{        lock.unlock();    }}//创建Connectionprivate Connection createConnection(){    return null;}

在上面的例子中,我们通过加锁确保高并发环境下的线程安全,也确保了connection只创建一次,然而确牺牲了性能。改用ConcurrentHash的情况下,几乎可以避免加锁的操作,性能大大提高,但是在高并发的情况下有可能出现Connection被创建多次的现象。这时最需要解决的问题就是当key不存在时,创建Connection的动作能放在connectionPool之后执行,这正是FutureTask发挥作用的时机,基于ConcurrentHashMap和FutureTask的改造代码如下:

private ConcurrentHashMap<String,FutureTask<Connection>>connectionPool = new ConcurrentHashMap<String, FutureTask<Connection>>();public Connection getConnection(String key) throws Exception{    FutureTask<Connection>connectionTask=connectionPool.get(key);    if(connectionTask!=null){        return connectionTask.get();    }    else{        Callable<Connection> callable = new Callable<Connection>(){            @Override            public Connection call() throws Exception {                // TODO Auto-generated method stub                return createConnection();            }        };        FutureTask<Connection>newTask = new FutureTask<Connection>(callable);        connectionTask = connectionPool.putIfAbsent(key, newTask);        if(connectionTask==null){            connectionTask = newTask;            connectionTask.run();        }        return connectionTask.get();    }}//创建Connectionprivate Connection createConnection(){    return null;}

经过这样的改造,可以避免由于并发带来的多次创建连接及锁的出现。

3. 信号量

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

Semaphore分为单值和多值两种:
1、单值的Semaphore管理的信号量只有1个,该信号量只能被一个线程所获得,意味着并发的代码只能被一个线程运行,这就相当于是一个互斥锁了,而且该锁是不可重入的。

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

下面看一个并发控制器的示例:

package thread;import java.util.concurrent.Semaphore;/** * Created by louyuting on 17/1/11. */public class SemaphoreTest{    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();    }}

看一下运行结果:
运行结果

从结果来看:最开始只能是有5个线程0 1 2 3 4获得了信号量资源,但是这之后就没有线程可以获得了,直到有这5个线程中的某些线程释放了,后面的5 6 7 8 9才可以继续获得线程,也就是保证了最多只有5个线程别执行。这就体现出了Semaphore的作用了。

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

最后注意两点:
1、Semaphore可以指定公平锁还是非公平锁

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

4. CyclicBarrier 类

一个同步辅助类,它允许一组线程互相等待,直到都到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

CyclicBarrier与CountDownLatch都是关于线程计数器的,他们的重要区别之一就是CountDownLatch是单次使用的,任务执行完成之后结束了。而CyclicBarrier是循环使用的

CyclicBarrier 支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令只在每个屏障点运行一次。若在继续所有参与线程之前更新共享状态,此屏障操作 很有用。

下面是一个示例:赛跑时,等待所有人都准备好时,才起跑.

package thread;import java.io.IOException;import java.util.Random;import java.util.concurrent.BrokenBarrierException;import java.util.concurrent.CyclicBarrier;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * Created by louyuting on 17/1/11. */public class CyclicBarrierTest {    public static void main(String[] args) throws IOException, InterruptedException {        //如果将参数改为4,但是下面只加入了3个选手,这永远等待下去        CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {            @Override            public void run() {                System.out.println("提示:所有线程都已经到达.可以执行下一步了.");            }        });        ExecutorService executor = Executors.newFixedThreadPool(3);        executor.submit(new Thread(new Runner(barrier, "1号选手")));        executor.submit(new Thread(new Runner(barrier, "2号选手")));        executor.submit(new Thread(new Runner(barrier, "3号选手")));        executor.shutdown();    }    static class Runner implements Runnable {        // 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)        private CyclicBarrier barrier;        private String name;        public Runner(CyclicBarrier barrier, String name) {            super();            this.barrier = barrier;            this.name = name;        }        @Override        public void run() {            try {                Thread.sleep(1000 * (new Random()).nextInt(8));                System.out.println(name + " 准备好了...");                // barrier的await方法,在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。                barrier.await();            } catch (InterruptedException e) {                e.printStackTrace();            } catch (BrokenBarrierException e) {                e.printStackTrace();            }            System.out.println(name + " 起跑!");        }    }}

运行结果:
output

从上面的例子中我们可以看出以下几点:
1、CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。

2、 CyclicBarrier就象它名字的意思一样,可看成是个障碍, 所有的线程必须到齐后才能一起通过这个障碍。

3、 CyclicBarrier初始时还可以带一个Runnable的参数, 此Runnable任务在CyclicBarrier指定的初始化的数目达到后,所有其它线程被唤醒前被执行。

代码的Github地址:

0 0
原创粉丝点击