闭锁/栅栏/信号量/FutureTask分析及使用

1、闭锁

 

用途:可用于命令一组线程在同一个时刻开始执行某个任务,或者等待一组相关的操作结束。尤其适合计算并发执行某个任务的耗时。

 

Java代码   收藏代码
  1. public class CountDownLatchTest {  
  2.   
  3.     public void timeTasks(int nThreads, final Runnable task) throws InterruptedException{  
  4.         final CountDownLatch startGate = new CountDownLatch(1);  
  5.         final CountDownLatch endGate = new CountDownLatch(nThreads);  
  6.           
  7.         for(int i = 0; i < nThreads; i++){  
  8.             Thread t = new Thread(){  
  9.                 public void run(){  
  10.                     try{  
  11.                         startGate.await();  
  12.                         try{  
  13.                             task.run();  
  14.                         }finally{  
  15.                             endGate.countDown();  
  16.                         }  
  17.                     }catch(InterruptedException ignored){  
  18.                           
  19.                     }  
  20.                       
  21.                 }  
  22.             };  
  23.             t.start();  
  24.         }  
  25.           
  26.         long start = System.nanoTime();  
  27.         System.out.println("打开闭锁");  
  28.         startGate.countDown();  
  29.         endGate.await();  
  30.         long end = System.nanoTime();  
  31.         System.out.println("闭锁退出,共耗时" + (end-start));  
  32.     }  
  33.       
  34.     public static void main(String[] args) throws InterruptedException{  
  35.         CountDownLatchTest test = new CountDownLatchTest();  
  36.         test.timeTasks(5, test.new RunnableTask());  
  37.     }  
  38.       
  39.     class RunnableTask implements Runnable{  
  40.   
  41.         @Override  
  42.         public void run() {  
  43.             System.out.println("当前线程为:" + Thread.currentThread().getName());  
  44.               
  45.         }     
  46.     }  

 

Java代码   收藏代码
  1. 执行结果为:  
  2. 打开闭锁  
  3. 当前线程为:Thread-0  
  4. 当前线程为:Thread-3  
  5. 当前线程为:Thread-2  
  6. 当前线程为:Thread-4  
  7. 当前线程为:Thread-1  
  8. 闭锁退出,共耗时1109195  

 2、栅栏

 

用途:用于阻塞一组线程直到某个事件发生。所有线程必须同时到达栅栏位置才能继续执行下一步操作,且能够被重置以达到重复利用。而闭锁式一次性对象,一旦进入终止状态,就不能被重置

 

Java代码   收藏代码
  1. public class CyclicBarrierTest {  
  2.     private final CyclicBarrier barrier;  
  3.     private final Worker[] workers;  
  4.   
  5.     public CyclicBarrierTest(){  
  6.         int count = Runtime.getRuntime().availableProcessors();  
  7.         this.barrier = new CyclicBarrier(count,  
  8.                 new Runnable(){  
  9.   
  10.                     @Override  
  11.                     public void run() {  
  12.                         System.out.println("所有线程均到达栅栏位置,开始下一轮计算");  
  13.                     }  
  14.               
  15.         });  
  16.         this.workers = new Worker[count];  
  17.         for(int i = 0; i< count;i++){  
  18.             workers[i] = new Worker(i);  
  19.         }  
  20.     }  
  21.     private class Worker implements Runnable{  
  22.         int i;  
  23.           
  24.         public Worker(int i){  
  25.             this.i = i;  
  26.         }  
  27.   
  28.         @Override  
  29.         public void run() {  
  30.             for(int index = 1; index < 3;index++){  
  31.                 System.out.println("线程" + i + "第" + index + "次到达栅栏位置,等待其他线程到达");  
  32.                 try {  
  33.                     //注意是await,而不是wait  
  34.                     barrier.await();  
  35.                 } catch (InterruptedException e) {  
  36.                     e.printStackTrace();  
  37.                     return;  
  38.                 } catch (BrokenBarrierException e) {  
  39.                     e.printStackTrace();  
  40.                     return;  
  41.                 }  
  42.             }  
  43.         }  
  44.           
  45.     }  
  46.       
  47.     public void start(){  
  48.         for(int i=0;i<workers.length;i++){  
  49.             new Thread(workers[i]).start();  
  50.         }  
  51.     }  
  52.       
  53.     public static void main(String[] args){  
  54.         new CyclicBarrierTest().start();  
  55.     }  
  56. }  

 

Java代码   收藏代码
  1. 执行结果为:  
  2. 线程01次到达栅栏位置,等待其他线程到达  
  3. 线程11次到达栅栏位置,等待其他线程到达  
  4. 线程21次到达栅栏位置,等待其他线程到达  
  5. 线程31次到达栅栏位置,等待其他线程到达  
  6. 所有线程均到达栅栏位置,开始下一轮计算  
  7. 线程32次到达栅栏位置,等待其他线程到达  
  8. 线程22次到达栅栏位置,等待其他线程到达  
  9. 线程02次到达栅栏位置,等待其他线程到达  
  10. 线程12次到达栅栏位置,等待其他线程到达  
  11. 所有线程均到达栅栏位置,开始下一轮计算  

 3、信号量

 

用途:用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。计数信号量可以用来实现某种资源池,或者对容器施加边界。

 

Java代码   收藏代码
  1. public class SemaphoreTest<T> {  
  2.     private final Set<T> set;  
  3.       
  4.     private final Semaphore sema;  
  5.       
  6.     public SemaphoreTest(int bound){  
  7.         this.set = Collections.synchronizedSet(new HashSet<T>());  
  8.         this.sema = new Semaphore(bound);  
  9.     }  
  10.       
  11.     public boolean add(T o) throws InterruptedException{  
  12.         sema.acquire();  
  13.         boolean wasAdded = false;  
  14.         try{  
  15.             wasAdded = set.add(o);  
  16.             return wasAdded;  
  17.         }finally{  
  18.             if(!wasAdded){  
  19.                 sema.release();  
  20.             }  
  21.         }  
  22.     }  
  23.       
  24.     public boolean remove(T o){  
  25.         boolean wasRemoved = set.remove(o);  
  26.         if(wasRemoved){  
  27.             sema.release();  
  28.         }  
  29.         return wasRemoved;  
  30.     }  
  31.       
  32.     public static void main(String[] args) throws InterruptedException{  
  33.         int permits = 5;  
  34.         int elements = permits + 1;  
  35.         SemaphoreTest<Integer> test = new SemaphoreTest<Integer>(permits);  
  36.         for(int i = 0;i < elements; i++){  
  37.             test.add(i);  
  38.         }  
  39.     }  
  40. }  

 输出结果:由于实际待添加的元素个数大于信号量所允许的数量,因此最后一次添加时,会一直阻塞。

 

4、巧用FutureTask缓存计算过程

当一个计算的代价比较高,譬如比较耗时,或者耗资源,为了避免重复计算带来的浪费,当第一次计算后,通常会将结果缓存起来。比较常见的方式就是使用synchronized进行同步,但该方式带来的代价是被同步的代码只能被串行执行,如果有多个线程在排队对待计算结果,那么针对最后一个线程的计算时间可能比没有使用缓存的时间会更长。

第二种方式是采用ConcurrentHashMap,但对于耗时比较长的计算过程来说,该方式也存在一个漏洞。如果在第一个线程正在计算的过程中,第二个线程开始获取结果,会发现缓存里没有缓存结果,因此第二个线程又启动了同样的计算,这样就导致重复计算,违背了缓存的初衷。计算过程越长,则出现这种重复计算的几率就会越大。

 

通过第二种方式的缺点分析,得知真正要缓存的应该是计算是否已被启动,而不是等待漫长的计算过程结束后,再缓存结果。一旦从缓存中得知某个计算过程已被其他线程启动,则当前线程不需要再重新启动计算,只需要阻塞等待计算结果的返回。FutureTask就是实现该功能的最佳选择。

 

Java代码   收藏代码
  1. public class Memoizer<A, V> implements Computable<A, V> {  
  2.     private ConcurrentMap<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();  
  3.     private final Computable<A, V> c;  
  4.   
  5.     public Memoizer(Computable<A, V> c) {  
  6.         this.c = c;  
  7.     }  
  8.   
  9.     @Override  
  10.     public V compute(final A a) {  
  11.         while (true) {  
  12.             Future<V> f = cache.get(a);  
  13.             if (null == f) {  
  14.                 Callable<V> eval = new Callable<V>() {  
  15.   
  16.                     @Override  
  17.                     public V call() throws Exception {  
  18.                         return c.compute(a);  
  19.                     }  
  20.   
  21.                 };  
  22.                 FutureTask<V> ft = new FutureTask<V>(eval);  
  23.                 f = cache.putIfAbsent(a, ft);  
  24.                 if (null == f) {  
  25.                     f = ft;  
  26.                     ft.run();  
  27.                 }  
  28.             }  
  29.             try {  
  30.                 return f.get();  
  31.             } catch (InterruptedException e) {  
  32.                 e.printStackTrace();  
  33.                 cache.remove(a, f);  
  34.             } catch (ExecutionException e) {  
  35.                 e.printStackTrace();  
  36.             }  
  37.         }  
  38.     }  
  39.   
  40.     public static void main(String[] args) throws InterruptedException,  
  41.             ExecutionException {  
  42.         Computable<Integer, String> c = new Computable<Integer, String>() {  
  43.   
  44.             @Override  
  45.             public String compute(Integer a) {  
  46.                 try {  
  47.                     Thread.sleep(1000);  
  48.                 } catch (InterruptedException e) {  
  49.                     e.printStackTrace();  
  50.                 }  
  51.                 String result = "由线程:" + Thread.currentThread().getName()  
  52.                         + "计算得到" + a + "的结果";  
  53.                 return result;  
  54.             }  
  55.         };  
  56.   
  57.         Memoizer<Integer, String> memoizer = new Memoizer<Integer, String>(c);  
  58.   
  59.         ExecutorService executorService = Executors.newFixedThreadPool(3);  
  60.   
  61.         for (int i = 0; i < 3; i++) {  
  62.             Task<Integer, String> task = new Task<Integer, String>(memoizer, 10);  
  63.             String result = executorService.submit(task).get();  
  64.             System.out.println(result);  
  65.         }  
  66.         executorService.shutdown();  
  67.     }  
  68. }  
Java代码   收藏代码
  1. 结果如下:  
  2. 由线程:pool-1-thread-1计算得到10的结果  
  3. 由线程:pool-1-thread-1计算得到10的结果  
  4. 由线程:pool-1-thread-1计算得到10的结果  
  5. 当然如果仅仅只是采用FutureTask,仅仅只是减小了启动同一个计算过程的概率。当两个线程同时经过compute方法时,还是会出现重复启动同一个计算的情况,上面的例子通过结合ConcurrentHashMap 的putIfAbsent方法解决了这个问题  
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当在使用Java中的FutureTask类时,可能会遇到java.util.concurrent.FutureTask.report内存溢出的问题。 FutureTaskJava中Executor框架的一部分,用于表示一个异步计算任务。在执行计算任务时,可能会出现内存溢出的情况,这通常是由于以下原因引起的: 1. 任务计算量过大:如果计算任务需要处理大量的数据或执行复杂的操作,可能会导致内存使用过多,进而导致内存溢出。 2. 内存泄漏:如果在任务执行过程中有未正确释放的资源或对象引用,那么这些未释放的资源可能会一直保存在内存中,最终导致内存溢出。 解决这个问题的方法如下: 1. 优化任务计算:检查计算任务的实现代码,尝试减少计算量或改进计算逻辑,以降低内存使用量。可以通过合理使用数据结构、循环和递归等技巧来减少内存占用。 2. 调整内存配置:如果计算任务确实需要较大的内存空间,可以调整Java虚拟机的内存配置参数。可以增加-Xmx和-Xms等参数,以提供足够的内存空间给计算任务使用。 3. 检查内存泄漏:通过使用Java内存分析工具,如Eclipse Memory Analyzer等,来检查任务执行期间是否存在内存泄漏。如果发现泄漏问题,需要修复代码,确保资源和对象在不再使用时能够正确释放。 综上所述,当出现java.util.concurrent.FutureTask.report内存溢出的情况时,我们可以通过优化任务计算、调整内存配置和检查内存泄漏等方法来解决该问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值