0 摘要
JUC包大大提高了Java程序的并发性能,AQS作为JUC的核心,是Java并发编程学习的重点。本文介绍了AQS这个类,以及AQS中的同步组件,如CountDownLatch,Semaphore 等组件的讲解和简单示例。
1 AbstractQueuedSynchronizer--AQS
- 使用Node实现FIFO队列,是构建锁和其他同步装置的基础框架
- 有一个int类型成员变量表示状态(int型的volatile变量waitStatus来维护同步状态)
- 使用方法是继承,AQS的设计是基于模板方法设计的
- 子类通过继承并通过实现它的方法管理其状态{acquire和release}方法操纵状态
- 可以同时实现排它锁和共享锁模式(独占、共享)
大致实现:其内部维护一个队列管理锁,线程会尝试获取锁,如果失败会将当前线程和等待信息包装成一个Node节点,加入到同步队列,会不断循环尝试获取锁,前提是当前节点为Head节点的直接后继,如果失败会阻塞自己,直到自己被唤醒。而持有锁的线程在释放线程会唤醒其后继节点中的线程。
2 AQS同步组件介绍
2.1 CountDownLatch
会阻塞当前线程,等到其他线程操作完成,调用await()方法的线程会一直处于阻塞状态,直到其他线程调用countDown()(原子性的,每调用一次计数器会减一)方法,将计数器的值减为0时,所有因为调用await()方法而阻塞的线程会继续往下执行。例如在并行计算场景中,父任务只有拿到所有子任务的计算结果后汇总才能继续执行。典型用法:
1 @Slf4j 2 public class CountDownLatchExample1 { 3 private final static int threadCount = 200; 4 public static void main(String[] args) throws InterruptedException { 5 ExecutorService executor = Executors.newCachedThreadPool();//线程池 6 final CountDownLatch countDownLatch = new CountDownLatch(threadCount); 7 for (int i = 0; i < threadCount; i++) {//模拟两百个请求 8 final int threadCount = i; 9 executor.execute(() -> { 10 try { 11 todo(threadCount);//执行的函数 12 } catch (Exception e) { 13 log.error("exception",e ); 14 } finally {//根据情况来countDown 15 countDownLatch.countDown();//确保countDown 16 } 17 }); 18 } 19 //countDownLatch.await(10, TimeUnit.MILLISECONDS);//表示阻塞10毫秒 20 countDownLatch.await();//阻塞 21 executor.shutdown();//关闭线程池 22 log.info("finish" ); 23 } 24 private static void todo(int threadCount) throws Exception { 25 Thread.sleep(100); 26 log.info("{}", threadCount+1); 27 } 28 }
2.2 Semaphore
能控制并发访问的线程个数,使用场景:仅能提供有限访问的场景,
例子1:semaphore.acquire()
1 @Slf4j 2 public class SemaphoreExample1 { 3 private final static int threadCount = 20; 4 public static void main(String[] args) throws InterruptedException { 5 ExecutorService executor = Executors.newCachedThreadPool(); 6 final Semaphore semaphore = new Semaphore(4);//实例,同时允许4个线程并发访问 7 for (int i = 0; i < threadCount; i++) { 8 final int threadCount = i; 9 executor.execute(() -> { 10 try { 11 semaphore.acquire();//获得许可 12 todo(threadCount); 13 semaphore.release();//释放许可 14 } catch (Exception e) { 15 log.error("exception",e ); 16 } 17 ; 18 }); 19 } 20 executor.shutdown(); 21 } 22 private static void todo(int threadCount) throws Exception { 23 Thread.sleep(1000); 24 log.info("{}", threadCount + 1); 25 } 26 }
结果:一开始线程池里面初始化20个线程,但每次并发访问的只有4个线程。同时,semaphore.acquire(permits)支持每次要获得permints个许可后才能放行。总之,Semaphore的使用简单,效果明显。
例子2:semaphore.tryAcquire
考虑这样的情况,当并发数实在太高了,即使同时并发处理也处理不过来,这时候会考虑丢弃其他的请求,使用TryAcquire()可以尝试获取许可,拿到许可就可以做,拿不到就丢弃,同时这个方法提供四个构造函数,满足不同的需求。
1 //尝试获取一个许可 2 //semaphore.tryAcquire() 3 4 //尝试获取permints个许可 5 //semaphore.tryAcquire(permits) 6 7 //等待多少时间 8 //semaphore.tryAcquire(timeout,unit) 9 10 //等待多少时间,尝试获取获取permints个许可 11 //semaphore.tryAcquire(permits,timeout,unit)
2.3 CyclicBarrier
同步工具类,和CountDownLatch有相似的地方,同样通过计数器来实现,与前者不同的是:由字面意思可以理解为循环屏障,其在释放等待的线程后可以重用。它使得一组线程相互等待(线程调用awaiting()方法,进入等待状态,计数器加一操作),直到到达某个公共的屏障点(之前设置的计数器值,这时候等待状态的线程会被唤醒),其同样能确保多个线程都准备好后,然后继续执行后面的操作。使用场景与CountDownLatch相似,如并行计算,需要合并计算数据等。
区别:
1)CountDownLatch的计数器只能用一次,CyclicBarrier计数器可以重用。
2) CountDownLatch描述的是一个或者多个线程等待其他线程完成某些操作后才能继续往下执行,CyclicBarrier实现的是多个线程之间相互等待,直到满足条件(计数器值满),其描述的是各个线程之间 相互等待的关系,所以其能处理更复杂的业务场景。
1 @Slf4j 2 public class CyclicBarrierExample1 { 3 private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> { 4 log.info("callback is running");//Runnable参数表示:到达屏障之前会优先执行 5 });//1.实例,给定初始值,表示有5个线程会等待 6 7 public static void main(String[] args) throws InterruptedException { 8 ExecutorService executorService = Executors.newCachedThreadPool();//线程池实例 9 10 for (int i = 0; i < 10; i++) {//模拟10次请求 11 final int threadNum = i; 12 Thread.sleep(1000);//睡眠1秒 13 executorService.execute(() -> { 14 try { 15 rase(threadNum);//todo 16 } catch (Exception e) { 17 log.error("exception", e); 18 } 19 20 }); 21 } 22 executorService.shutdown();//关闭线程池 23 } 24 25 private static void rase(int threadNum) throws InterruptedException, BrokenBarrierException { 26 Thread.sleep(1000); 27 log.info("{} is ready", threadNum); 28 cyclicBarrier.await();//2.调用await()方法,计数器加一 29 log.info("{} continue", threadNum);//3.达到定义的计数器数量后,await()后面的操作就可以执行了 30 31 } 32 }
结果:会等待五个线程ready后开始执行后续操作
1 08:20:24.214 [pool-1-thread-1] INFO com.truekai.concurrency.example.AQS.CyclicBarrierExample1 - 0 is ready 2 08:20:25.213 [pool-1-thread-2] INFO com.truekai.concurrency.example.AQS.CyclicBarrierExample1 - 1 is ready 3 08:20:26.214 [pool-1-thread-3] INFO com.truekai.concurrency.example.AQS.CyclicBarrierExample1 - 2 is ready 4 08:20:27.214 [pool-1-thread-4] INFO com.truekai.concurrency.example.AQS.CyclicBarrierExample1 - 3 is ready 5 08:20:28.216 [pool-1-thread-5] INFO com.truekai.concurrency.example.AQS.CyclicBarrierExample1 - 4 is ready 6 08:20:28.216 [pool-1-thread-5] INFO com.truekai.concurrency.example.AQS.CyclicBarrierExample1 - callback is running 7 08:20:28.216 [pool-1-thread-5] INFO com.truekai.concurrency.example.AQS.CyclicBarrierExample1 - 4 continue 8 08:20:28.216 [pool-1-thread-1] INFO com.truekai.concurrency.example.AQS.CyclicBarrierExample1 - 0 continue 9 08:20:28.216 [pool-1-thread-2] INFO com.truekai.concurrency.example.AQS.CyclicBarrierExample1 - 1 continue 10 08:20:28.216 [pool-1-thread-3] INFO com.truekai.concurrency.example.AQS.CyclicBarrierExample1 - 2 continue 11 08:20:28.216 [pool-1-thread-4] INFO com.truekai.concurrency.example.AQS.CyclicBarrierExample1 - 3 continue
3 小结
总结了JUC包中的AQS同步组件的使用,关于并发编程中的锁(JDK的锁和JVM的锁)后面会单独总结一篇文章。
2019-05-26 16:12:21