并发与高并发(十三)J.U.C之AQS
前言
什么是AQS,是AbstractQueuedSynchronizer类的简称。J.U.C大大提高了并发的性能,而AQS又是J.U.S的核心。
主体概要
- J.U.C之AQS介绍
- J.U.C之AQS-CountDownLatch
- J.U.C之AQS-Semaphore
- J.U.C之AQS-CyclicBarrier
- J.U.C之AQS-ReentrantLock与锁
主体内容
一、J.U.C之AQS介绍
1.AbstractQueuedSynchronizer简称AQS
AbstractQueuedSynchronizer是J.U.C(java.util.concurrent)中的重中之重。
(1)我们看一下底层的数据结构
解释一下:
底层使用的是双向链表,是队列的一种实现,因此我们可以把它当做一个队列。其中Sync queue同步队列是双向链表,包括(head、tail)节点,head节点主要用于后期的调度。而Condition queue不是必须的,它是一个单向链表,只有当使用到Condition queue的时候才会存在这个单向链表,并且可能会有多个Condition queue。
(2)接下来我们看一下AQS的设计
- 使用Node实现FIFO队列,可以用于构建锁或者其他同步装置的基础框架。
- 利用了一个int类型表示状态
- 使用方法是继承(使用的时候需要继承AQS,并复写其中的方法)
- 子类通过继承并通过实现它的方法管理其状态(aquire和release)的方法操纵状态
- 可以同时实现排它锁和共享锁模式(独占、共享)
这里介绍一下AQS大致实现的思路:
首先,AQS内部维护了一个CLH(Craig,Landin,and Hagersten)队列来管理锁,线程会首先尝试获取锁,如果失败,就向当前线程以等待状态为信息包成一个lock节点加入到同步队列Sync Queue队列,接下来会不断循环尝试获取锁,它的条件是当前节点为head的直接后继才会尝试,如果失败就会阻塞自己,直到自己被唤醒,而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。
(3)JDK为我们提供了许多AQS子类的同步组件。
- CountDownLatch(通过计数来保证线程是否一直需要阻塞)
- Semaphore(控制同一时间并发的线程数)
- CyclicBarrier
- ReetrantLock
- Condition
- FutureTask
本章以下将详细介绍这几个类。
二、J.U.C之AQS-CountDownLatch
1.CountDownLatch是一个同步辅助类,通过它可以完成类似线程阻塞的功能,简单来说,就是让一个线程等待其他线程执行完成。CountDownLatch使用一个给定的计数器进行初始化,该计数器的操作是原子性的操作,就是同时只有一个线程可以执行该计数器。用该类的await()方法就可以让调用它的线程一直处于阻塞状态,当其他线程调用countDown()方法,每次计数减一,当计数值等于0的时候,因调用await()方法的阻塞线程就会继续往下执行。
2.CountDownLatch使用场景(敬豪大帅哥看这里)
(1)并行计算
当某个处理运算量很大时,可以将该运算拆分成多个子任务,等待所有的子任务完成之后,父任务拿到所有子任务的运算结果进行汇总
(2)举个例子,循环创建200个线程,分别执行每次循环的次数输出值,吗,每次线程执行完调用countDown(),计数值减一,最后主线程调用await()方法,意思是,主线程需要等待线程池创建的200个线程全部执行完毕,才可以继续执行。
import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @Slf4j public class CountDownLatchExample1 { private final static int threadCount= 200;//线程数 private final static CountDownLatch countDownLatch= new CountDownLatch(threadCount); public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for(int i=0;i<threadCount;i++){ final int threadNum=i; executorService.execute(()->{ try { test(threadNum); } catch (Exception e) { e.printStackTrace(); }finally { countDownLatch.countDown(); } }); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } log.info("finish");
executorService.shutdown();
}
public static void test(int threadNum) throws Exception{
Thread.sleep(100); log.info("test-{}",threadNum); Thread.sleep(100);
}
}
所以,结果是
省略其他结果值... ... 00:25:24.082 [pool-1-thread-153] INFO com.practice.aqs.CountDownLatchExample1 - test-152 00:25:24.082 [pool-1-thread-162] INFO com.practice.aqs.CountDownLatchExample1 - test-161 00:25:24.069 [pool-1-thread-25] INFO com.practice.aqs.CountDownLatchExample1 - test-24 00:25:24.088 [pool-1-thread-185] INFO com.practice.aqs.CountDownLatchExample1 - test-184 00:25:24.069 [pool-1-thread-21] INFO com.practice.aqs.CountDownLatchExample1 - test-20 00:25:24.068 [pool-1-thread-31] INFO com.practice.aqs.CountDownLatchExample1 - test-30 00:25:24.073 [pool-1-thread-78] INFO com.practice.aqs.CountDownLatchExample1 - test-77 00:25:24.073 [pool-1-thread-52] INFO com.practice.aqs.CountDownLatchExample1 - test-51 00:25:24.077 [pool-1-thread-112] INFO com.practice.aqs.CountDownLatchExample1 - test-111 00:25:24.225 [main] INFO com.practice.aqs.CountDownLatchExample1 - finish
(3)上面是一个比较简单的例子,那我们来看一个复杂一点的例子,我现在想要给每个线程执行各自任务的时间有限,超过这个时间的结果不要了。假设我给所有线程10毫秒的时间执行,让每个线程睡上100毫秒,那么所有线程都赶不上这个时间,主线程睡10毫秒后继续执行。例子如下:
import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @Slf4j public class CountDownLatchExample2 { private final static int threadCount= 200;//线程数 private final static CountDownLatch countDownLatch= new CountDownLatch(threadCount); public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for(int i=0;i<threadCount;i++){ final int threadNum=i; executorService.execute(()->{ try { test(threadNum); } catch (Exception e) { e.printStackTrace(); }finally { countDownLatch.countDown(); } }); } try { countDownLatch.await(10, TimeUnit.MILLISECONDS);//10毫秒结束等待 } catch (InterruptedException e) { e.printStackTrace(); } log.info("finish"); } public static void test(int threadNum) throws Exception{ Thread.sleep(100); log.info("test-{}",threadNum); } }
结果:
00:38:59.331 [main] INFO com.practice.aqs.CountDownLatchExample2 - finish 00:38:59.397 [pool-1-thread-28] INFO com.practice.aqs.CountDownLatchExample2 - test-27 00:38:59.395 [pool-1-thread-7] INFO com.practice.aqs.CountDownLatchExample2 - test-6 00:38:59.397 [pool-1-thread-23] INFO com.practice.aqs.CountDownLatchExample2 - test-22 00:38:59.397 [pool-1-thread-25] INFO com.practice.aqs.CountDownLatchExample2 - test-24 00:38:59.396 [pool-1-thread-13] INFO com.practice.aqs.CountDownLatchExample2 - test-12 00:38:59.397 [pool-1-thread-27] INFO com.practice.aqs.CountDownLatchExample2 - test-26 00:38:59.395 [pool-1-thread-4] INFO com.practice.aqs.CountDownLatchExample2 - test-3
...
以下省略...
发现finish先输出了,那为什么线程会紧随其后继续输出呢?原因是线程池的shutdown方法调用后,并不是所有线程都被销毁了,而是允许他们执行完。
以上就是CountDownLatch的两个小例子。
三、J.U.C之AQS-Semaphore
1.Semaphore(信号量),可以控制某个资源可被同时访问的线程个数。Semaphore也提供了两个方法,分别是aquire()和release()方法。aquire()是获取一个许可,如果没有,则等待。而release()方法则是在操作之后是否一个许可。Semaphore通过同步机制来控制同时访问的个数。
2.使用场景:比如数据库连接池允许的最大连接数为10,如果同时超过10个线程访问数据库资源,则会导致异常,此时就需要信号量来控制。
3.接下来,举一个小栗子来演示一下Semaphore的用法。
mport lombok.extern.slf4j.Slf4j; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; @Slf4j public class SemaphoreExample1 { private final static int threadCount = 200; private final static Semaphore semaphore = new Semaphore(20);//同时允许20个线程执行 public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for(int i=0;i<threadCount;i++){ final int threadNum =i; executorService.execute(()->{ try { semaphore.acquire();//获取一个许可 test(threadNum); semaphore.release();//释放一个许可 } catch (InterruptedException e) { log.info("exception",e); } }); } executorService.shutdown(); log.info("finish"); } public static void test(int threadNum){ log.info("test-{}",threadNum); } }
结果:
省略...
22:07:14.467 [pool-1-thread-47] INFO com.practice.aqs.SemaphoreExample1 - test-46 22:07:14.467 [pool-1-thread-48] INFO com.practice.aqs.SemaphoreExample1 - test-47 22:07:14.467 [pool-1-thread-105] INFO com.practice.aqs.SemaphoreExample1 - test-172 22:07:14.467 [pool-1-thread-49] INFO com.practice.aqs.SemaphoreExample1 - test-48 22:07:14.468 [pool-1-thread-111] INFO com.practice.aqs.SemaphoreExample1 - test-182 22:07:14.467 [pool-1-thread-35] INFO com.practice.aqs.SemaphoreExample1 - test-170 22:07:14.469 [main] INFO com.practice.aqs.SemaphoreExample1 - finish 22:07:14.469 [pool-1-thread-65] INFO com.practice.aqs.SemaphoreExample1 - test-64 22:07:14.469 [pool-1-thread-62] INFO com.practice.aqs.SemaphoreExample1 - test-61 22:07:14.467 [pool-1-thread-37] INFO com.practice.aqs.SemaphoreExample1 - test-176 22:07:14.467 [pool-1-thread-51] INFO com.practice.aqs.SemaphoreExample1 - test-50 22:07:14.467 [pool-1-thread-52] INFO com.practice.aqs.SemaphoreExample1 - test-51
省略...
4.有时候我们需要多个许可,那么又该如何写呢?其实aquire()也提供了参数。
直接在这两方法中加入参数3,这样就是获取3个许可,然后我让执行test()的每个线程睡个1秒。
semaphore.acquire(3); test(threadNum); semaphore.release(3);
结果如下:
它会像这样等待3个许可全部被释放,才会执行下一组,所以结果看起来像是一块一块的执行。
6.像这样,还有一种更复杂点的场景,如果现在我想超过信号量5的结果丢弃,如何完成呢?且看以下的例子。
import lombok.extern.slf4j.Slf4j; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; @Slf4j public class SemaphoreExample1 { private final static int threadCount = 200; private final static Semaphore semaphore = new Semaphore(3);//同时允许3个线程执行 public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for(int i=0;i<threadCount;i++){ final int threadNum =i; executorService.execute(()->{ try { if(semaphore.tryAcquire()) { test(threadNum); semaphore.release(); } } catch (Exception e) { log.info("exception",e); } }); } executorService.shutdown(); log.info("finish"); } public static void test(int threadNum) throws Exception{ log.info("test-{}",threadNum); Thread.sleep(1000); } }
结果:
22:30:26.356 [main] INFO com.practice.aqs.SemaphoreExample1 - finish 22:30:26.356 [pool-1-thread-6] INFO com.practice.aqs.SemaphoreExample1 - test-5 22:30:26.356 [pool-1-thread-1] INFO com.practice.aqs.SemaphoreExample1 - test-0 22:30:26.356 [pool-1-thread-3] INFO com.practice.aqs.SemaphoreExample1 - test-2 Process finished with exit code 0
可见只有三个线程获得了许可。
我们详细看一下tryAquire()的几个方法。
(1)tryAquire() --boolean
(2)tryAquire(int) --boolean 一次性获取多少个许可,如果获取不到即丢弃
(3)tryAquire(long,TimeUnit) --boolean long:超时时间 TimeUnit:时间单位 意思是如果我获取许可可以最大在long时间内,如果超过long,则放弃获取许可。
(4)tryAquire(int,long,TimeUnit) --boolean 相当于以上两个函数的结合。
我还是举一个小例子吧!仅需要将以上的代码修改一处即可。
if(semaphore.tryAcquire()) { test(threadNum); semaphore.release(); }
修改为
if(semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS)) { test(threadNum); semaphore.release(); }
结果只有在5秒内获取到许可的线程们执行了,而且是3个一组,成块状执行:
以上就是Semaphore的讲解,后续有待补充。
四、J.U.C之AQS-CyclicBarrier
1.CyclicBarrirer也是一个同步辅助类,它允许一组线程持续等待,直到到达某个工作屏障点。通过它可以完成多个线程相互等待,只有当每个线程都准备就绪后,才能各自继续往下执行后面的操作。它和CountDownLatch有相类似的地方,都是通过计数器来实现的,当某个线程调用了await()方法后,该线程就进入等待状态,注意,这里的计数器是执行+1操作,当计数器值达到我们设置的初始值时候,因为调用await()方法的线程会被唤醒,继续执行他们自己后续的操作。由于CyclicBarrier在释放等待线程后可以重用,我们又称之为循环屏障。
2.CyclicBarrier和CountDownLatch的使用场景十分相似,它可以用于多线程合并最终计算结果。
3.简单讲一下CyclicBarrier和CountDownLatch的区别
(1)CountDownLatch的计数器只能使用一次,而CyclicBarrier可以使用reset()方法重置,可以循环使用。
(2)CountDownLatch主要是实现一个或多个线程需要等待其他线程完成某项操作之后,才能继续往下执行,它描述的是一个或N个线程等待其他线程的关系。而CyclicBarrier主要是实现多个线程之间相互等待,直到所有线程满足条件后,才能执行后续的操作,它描述的是多个线程之间相互等待的关系。
4.先列出一个小例子,展示先它的用法。
import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @Slf4j public class CyclicBarrierExample1 { private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);//当屏障内的线程突破5个时才允许其继续执行 public static void main(String[] args) throws Exception{ ExecutorService executorService = Executors.newCachedThreadPool(); for(int i=0;i<10;i++){ final int threadNum = i; Thread.sleep(1000); executorService.execute(()->{ try { race(); } catch (Exception e) { log.error("exection",e); } }); }
executorService.shutdown(); } public static void race() throws Exception{ Thread.sleep(1000); log.info("i'm ready "); cyclicBarrier.await(); log.info("i'm finished"); } }
结果每5个线程ready了,才会执行紧接着的finish:
5.CyclicBarrier.await();方法其实是可以放入时间参数的,也就是等待多久。但是直接加上时间往往会出现异常,我们需要将其异常捕捉,才能保证下面的任务会继续执行。
例如,我将以上的cyclicBarrier.await();修改为:cyclicBarrier.await(2000,TimeUnit.MILLISECONDS);也就是让它等2000毫秒,如果超过就不等了,直接让屏障内未超时的线程继续向下执行。
最后的结果图如下:
6.最后,关于CyclicBarrier还有个例子,可以在线程达到屏障数的时候,可以指定一个线程,让它优先执行指定的线程。
只需要改下CyclicBarrier的定义。
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5,()->{ log.info("callback is running!!!"); });//当屏障内的线程突破5个时才允许其继续执行
结果会如下所示:
五、J.U.C之AQS-ReentrantLock与锁
1.这里重新复习一下Java里的锁,一种是之前提到的synchronized锁,一种是J.U.C里面提供的锁,即ReentrantLock。
简要讲一下ReentrantLock(可重入锁)与synchronized的区别
(1)可重入性
(2)锁的实现
synchronized是依赖于JVM实现的,而ReentrantLock是基于JDK实现的,具体区别相当于操作系统控制锁和用户自己控制锁的区别。
(3)性能的区别
在之前,synchronized锁性能相对于ReentrantLock是很差的,但后期synchronized引入了偏向锁,轻量锁后,他们两个的性能就差不多了。官方目前建议使用synchronized锁,因为它的写法比较容易。
(4)功能区别
synchronized可以自动加锁,释放锁。而ReentrantLock是需要手动加锁,释放锁,为了避免忘记释放锁而造成死锁,这里建议写在finally中释放。
锁的细粒度和灵活度方面,ReentrantLock是优于synchronized的。
ReentrantLock拥有自己独立的功能:
- 可指定是公平锁还是非公平锁,而synchronized只能是非公平锁(所谓公平锁就是先等待的线程先获得锁)
- 提供了一个Condition类,可以分组唤醒需要唤醒的线程
- 提供能够中断等待锁的线程的机制,lock.lockInterruptibly()
那么既然ReentrantLock比synchronized更全面,是不是应该舍弃synchronized呢?非也,java.util.concurrent是面向高级用户的,当有明确的需求或证据需要用到ReentrantLock特性的时候,才会建议使用ReentrantLock。主要建议synchronized是因为如果因为使用了ReentrantLock一旦忘记释放锁,后期项目测试无问题上线了出现了问题,很难定位到是没有释放锁的原因,所以建议初学者使用synchronized。
2.下面利用最初的例子,来演示一下ReentrantLock的基本用法。
import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Slf4j public class ReentrantLockExample1 { private final static int clientCount=5000; private final static int threadCount = 50; private static int count =0; private final static Lock lock = new ReentrantLock(); public static void main(String[] args) { CountDownLatch countDownLatch = new CountDownLatch(clientCount); Semaphore semaphore = new Semaphore(threadCount); ExecutorService executorService = Executors.newCachedThreadPool(); for(int i=0;i<clientCount;i++){ executorService.execute(()->{ try { semaphore.acquire(); test(); semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } countDownLatch.countDown(); }); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } executorService.shutdown(); log.info("count:{}",count); } public static void test(){ lock.lock(); try { count++; } finally { lock.unlock(); } } }
结果:
22:55:05.999 [main] INFO com.practice.aqs.ReentrantLockExample1 - count:5000 Process finished with exit code 0
3.像我们刚刚讲的,ReentrantLock拥有许多特性,实际上它提供了很多方法来实现这些特性。如
lockInterruptibly():如果当前线程没有被中断的话,那么就获取锁,如果已经被中断,就抛出异常。
isLocked():查询此锁定是否有任意线程保持。
isHeldByCurrentThread():查询当前线程是否保持锁定状态。
isFair():判断是不是公平锁。
getHoldCount():查询当前线程保持锁定的个数。
...
4.接下来,我们要说一下另外一个锁叫做ReentrantReadWriteLock,在没有任何读锁的时候,才可以取得写入锁,这个要求这个类的核心。
@since 1.5 * @author Doug Lea */ public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { private static final long serialVersionUID = -6992448646407690164L; /** Inner class providing readlock */ private final ReentrantReadWriteLock.ReadLock readerLock; /** Inner class providing writelock */ private final ReentrantReadWriteLock.WriteLock writerLock; /** Performs all synchronization mechanics */ final Sync sync;
这个ReentrantReadWriteLock实现了悲观读取,即如果我们执行中进行读取时,经常可能有另一个执行要写入的需求。为了保持同步ReentrantReadWriteLock的读取锁定就可以派上用场了。然而读取机会很多,写入很少的情况下,使用ReentrantReadWriteLock可能会造成写入线程遭遇饥饿,即写入线程一直处于等待状态。文字可能有点难懂,还是写一段代码演示一波。
import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; @Slf4j public class ReetrantReadWriteLockExample1 { private final Map<String,Data> map = new TreeMap<>();//定义一个map用于读写 private final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();//定义ReentrantReadWriteLock private final Lock readLock = reentrantReadWriteLock.readLock();//定义读锁 private final Lock wirteLock = reentrantReadWriteLock.writeLock();//定义写锁 /** * 读取map * @param key * @return */ public Data get(String key){ readLock.lock(); try { return map.get(key); } finally { readLock.unlock(); } } /** * 获取所有的key * @return */ public Set<String> getAllKeys(){ readLock.lock(); try { return map.keySet(); } finally { readLock.unlock(); } } /** * 写入map * @param key * @param value * @return */ public Data put(String key,Data value){ wirteLock.lock(); try { return map.put(key,value); } finally { wirteLock.unlock(); } } class Data{//这里声明一个内部类 } }
为什么说写线程容易遭遇饥饿呢?原因就是有线程不停的读,导致写永远难以执行,以上就是ReentrantReadWriteLock的简单介绍,后续有待补充。这个类其实应用场景很少,只要知道了解就可以了,不过有兴趣的话可以详细搜索一下这个类的用法。
5.这里再介绍一个锁,叫做StampedLock。它控制锁有三种模式:读、写、乐观读,重点在于这个乐观读上。一个StampedLock是由版本和模式两个部分组成。锁获取方法返回的是一个数字作为票据,它由相关的锁状态来控制并发线程的访问。在读锁上分为悲观锁和乐观锁。所谓乐观读,其实就是如果读的操作很多,写的操作很少的情况下,我们可以乐观的认为读的操作和写的操作同时发生的几率很小。
以下是StampedLock内部注释中提供的一个例子。已表明中文注释,可以试着理解一下。
class Point { private double x, y; private final StampedLock sl = new StampedLock(); void move(double deltaX, double deltaY) { // an exclusively locked method long stamp = sl.writeLock(); try { x += deltaX; y += deltaY; } finally { sl.unlockWrite(stamp); } } //下面看看乐观锁案例 double distanceFromOrigin() { // A read-only method long stamp = sl.tryOptimisticRead();//获得一个乐观读锁 double currentX = x, currentY = y;//将两个字段读入本地局部变量 if (!sl.validate(stamp)) {//检查发出乐观读锁后同时是否有其他锁发生? stamp = sl.readLock();//如果没有,我们再次获得一个读悲观锁 try { currentX = x;//将两个字段读入本地局部变量 currentY = y;//将两个字段读入本地局部变量 } finally { sl.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); } //以下是悲观读锁案例 void moveIfAtOrigin(double newX, double newY) { // upgrade // Could instead start with optimistic, not read mode long stamp = sl.readLock(); try { while (x == 0.0 && y == 0.0) {//循环,检查当前状态是否符合 long ws = sl.tryConvertToWriteLock(stamp);//将读锁转为写锁 if (ws != 0L) {//这是确认写锁是否成功 stamp = ws;//如果成功,替换票据 x = newX;//进行状态改变 y = newY;//进行状态改变 break; } else {//如果不能成功,转为写锁 sl.unlockRead(stamp);//我们显式的释放读锁 stamp = sl.writeLock();//显式直接进行写锁 然后通过循环再试 } } } finally { sl.unlock(stamp);//释放读锁或写锁 } } }
这里来个栗子演示一波StampedLock的用法。
import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.locks.StampedLock; @Slf4j public class StampedLockExample1 { private final static int clientCount = 5000; private final static int threadCount = 50; private static int count =0; private final static CountDownLatch countDownLatch = new CountDownLatch(clientCount); private final static Semaphore semaphore = new Semaphore(threadCount); private final static StampedLock stampedLock = new StampedLock();//定义stampedLock public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for(int i=0;i<clientCount;i++){ try { semaphore.acquire(); test(); semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } countDownLatch.countDown(); } try { countDownLatch.await(); log.info("count:{}",count); } catch (InterruptedException e) { e.printStackTrace(); }finally { executorService.shutdown(); } } public static void test(){ long stamped = stampedLock.writeLock();//定义写锁,这时候他会返回一个stamped值 try { count++; } finally { stampedLock.unlock(stamped);//释放的时候带上这个票据stamped } } }
结果自然是没有问题的,其实看中的是它在这种读线程多写线程少的场景下性能好的特点:
22:00:33.261 [main] INFO com.practice.aqs.StampedLockExample1 - count:5000 Process finished with exit code 0
6.接下来看一下Condition这个类,直接看个例子。
mport lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; @Slf4j public class ConditionExample { public static void main(String[] args) { ReentrantLock reentrantLock = new ReentrantLock();//首先我们定义了一个reentrantLock Condition condition = reentrantLock.newCondition();//其次从reentrantLock里取出了condition new Thread(()->{//线程1 try { reentrantLock.lock();//线程1调用了reentrantLock的lock()方法,这个线程就加入到了AQS的等待队列里面去了 log.info("wait signal");//1 condition.await();//当线程1调用condition.await()方法时就从AQS等待队列里移除了,对应的操作其实就是锁的释放,接着它马上又加入了condition的等待队列里面去 } catch (InterruptedException e) { e.printStackTrace(); } log.info("get signal");//4 reentrantLock.unlock(); }).start(); new Thread(()->{//线程2因为判断线程1释放后被唤醒 reentrantLock.lock();//同样的,线程2加入到了AQS的等待队列中 log.info("get lock");//2 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } condition.signalAll();//发送信号,这时候condition的等待队列里面有我们线程1的节点,于是它(线程1)就被取出来加入到AQS的等待队列中,注意,此时线程1还没被唤醒 log.info("send signal");//3 reentrantLock.unlock();//当执行到这一步时,线程2释放锁,线程1被唤醒,于是上面的线程1继续开始执行,输出get signal }).start(); } }
结果:
22:21:43.774 [Thread-0] INFO com.practice.aqs.ConditionExample - wait signal 22:21:43.777 [Thread-1] INFO com.practice.aqs.ConditionExample - get lock 22:21:46.777 [Thread-1] INFO com.practice.aqs.ConditionExample - send signal 22:21:46.777 [Thread-0] INFO com.practice.aqs.ConditionExample - get signal Process finished with exit code 0
例子中已经标明了程序的输出顺序。
平时的时候大家可能Condition用的地方会很少,有兴趣的话可以继续研究一下。
总结
我们来总结一下这一章涉及的锁的类,第一个是synchronized,发生异常的时候,JVM会自动释放锁。而ReentrantLock,ReentrantReadWriteLock,StampedLock它们三都是对象层面的锁定,要保证锁一定会被释放,就必须把unlock放到finally里面执行。StampedLock对吞吐量有巨大的改进,特别是读线程很多写较少的场景下。
这里涉及的锁还是比较多的,那么何时该应用什么锁呢?
1.当只有少量竞争者的时候(少量竞争线程的时候),synchronized是一个很好的通用锁实现。
2.竞争者比较多的时候,但是线程数增长趋势我们是可以预估的时候,ReentrantLock是一个很好的通用锁实现。
在这里要注意一点,除了synchronized是会被JVM自动释放的,而其他锁一旦没有被手动释放,是会发生死锁的。