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.首先看看类的继承关系:
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 + " 起跑!"); } }}
运行结果:
从上面的例子中我们可以看出以下几点:
1、CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。
2、 CyclicBarrier就象它名字的意思一样,可看成是个障碍, 所有的线程必须到齐后才能一起通过这个障碍。
3、 CyclicBarrier初始时还可以带一个Runnable的参数, 此Runnable任务在CyclicBarrier指定的初始化的数目达到后,所有其它线程被唤醒前被执行。
代码的Github地址:
- J.U.C--同步工具类
- J.U.C--同步容器类Collections
- J.U.C之并发工具类:CyclicBarrier
- J.U.C之AQS:CLH同步队列
- J.U.C原子工具类AtomicXXX中,set和lazySet的区别
- 【死磕Java并发】-----J.U.C之并发工具类:CyclicBarrier
- 【死磕Java并发】-----J.U.C之并发工具类:CountDownLatch
- 【死磕Java并发】-----J.U.C之并发工具类:Semaphore
- 【死磕Java并发】-----J.U.C之并发工具类:Exchanger
- J.U.C体系
- J.U.C
- “J.U.C”:ReentrantReadWriteLock
- “J.U.C”:Condition
- J.U.C重入锁
- J.U.C包介绍
- J.U.C包介绍
- J.U.C之Future
- J.U.C之CopyOnWriteArrayList
- autoit 跨行符号
- js多文件上传
- 阮一峰——未来世界的幸存者摘要
- [Fixed] Unable to parse YAML file in Unity 3d project
- Nodejs中“循环+异步” 好深的坑!!
- J.U.C--同步工具类
- 邮票分你一半
- 关于Android 方法数超过64K的解决办法,终究要知道
- 字符串处理
- BZOJ 1101 莫比乌斯函数+分块
- ZCMU-1273-夫妻
- [易飞]历史预收款单已审核未生成分录,未核销单据处理方式。
- 面试--java+数据转型(76)
- Atom插件开发入门教程(四)