java 和c 并发性能_并发与高并发(十三)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)我们看一下底层的数据结构

d8707bbba0d28dad6ae3a71eb677b827.png

解释一下:

底层使用的是双向链表,是队列的一种实现,因此我们可以把它当做一个队列。其中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;

@Slf4jpublic classCountDownLatchExample1 {private final static int threadCount= 200;//线程数

private final static CountDownLatch countDownLatch= newCountDownLatch(threadCount);public static voidmain(String[] args) {

ExecutorService executorService=Executors.newCachedThreadPool();for(int i=0;i

finalint 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(intthreadNum) 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;

@Slf4jpublic classCountDownLatchExample2 {private final static int threadCount= 200;//线程数

private final static CountDownLatch countDownLatch= newCountDownLatch(threadCount);public static voidmain(String[] args) {

ExecutorService executorService=Executors.newCachedThreadPool();for(int i=0;i

finalint 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(intthreadNum) throws Exception{

Thread.sleep(100);

log.info("test-{}",threadNum);

}

}

结果:

00:38:59.331 [main] INFO com.practice.aqs.CountDownLatchExample2 -finish00: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;

@Slf4jpublic classSemaphoreExample1 {private final static int threadCount = 200;private final static Semaphore semaphore = new Semaphore(20);//同时允许20个线程执行

public static voidmain(String[] args) {

ExecutorService executorService=Executors.newCachedThreadPool();for(int i=0;i

finalint 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(intthreadNum){

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 -finish22: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);

结果如下:

d022e7d1e3e742f14b173f206122cefe.gif

它会像这样等待3个许可全部被释放,才会执行下一组,所以结果看起来像是一块一块的执行。

6.像这样,还有一种更复杂点的场景,如果现在我想超过信号量5的结果丢弃,如何完成呢?且看以下的例子。

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.Semaphore;

@Slf4jpublic classSemaphoreExample1 {private final static int threadCount = 200;private final static Semaphore semaphore = new Semaphore(3);//同时允许3个线程执行

public static voidmain(String[] args) {

ExecutorService executorService=Executors.newCachedThreadPool();for(int i=0;i

finalint 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(intthreadNum) throws Exception{

log.info("test-{}",threadNum);

Thread.sleep(1000);

}

}

结果:

22:30:26.356 [main] INFO com.practice.aqs.SemaphoreExample1 -finish22: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-2Process finished with exit code0

可见只有三个线程获得了许可。

我们详细看一下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个一组,成块状执行:

c60d56529bcd2aa4d2d0bb931cce9bfc.gif

以上就是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;

@Slf4jpublic classCyclicBarrierExample1 {private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);//当屏障内的线程突破5个时才允许其继续执行

public static voidmain(String[] args) throws Exception{

ExecutorService executorService=Executors.newCachedThreadPool();for(int i=0;i<10;i++){

finalint threadNum =i;

Thread.sleep(1000);

executorService.execute(()->{try{

race();

}catch(Exception e) {

log.error("exection",e);

}

});

}

executorService.shutdown();

}public static voidrace() throws Exception{

Thread.sleep(1000);

log.info("i'm ready");

cyclicBarrier.await();

log.info("i'm finished");

}

}

结果每5个线程ready了,才会执行紧接着的finish:

de967a65f32924c0f022876a431d8b2a.gif

5.CyclicBarrier.await();方法其实是可以放入时间参数的,也就是等待多久。但是直接加上时间往往会出现异常,我们需要将其异常捕捉,才能保证下面的任务会继续执行。

例如,我将以上的cyclicBarrier.await();修改为:cyclicBarrier.await(2000,TimeUnit.MILLISECONDS);也就是让它等2000毫秒,如果超过就不等了,直接让屏障内未超时的线程继续向下执行。

最后的结果图如下:

b04bbf779312e51c4cbdb4d7ecead7aa.gif

6.最后,关于CyclicBarrier还有个例子,可以在线程达到屏障数的时候,可以指定一个线程,让它优先执行指定的线程。

只需要改下CyclicBarrier的定义。

private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5,()->{

log.info("callback is running!!!");

});//当屏障内的线程突破5个时才允许其继续执行

结果会如下所示:

e3a5e4188dddf5da75f6d0612791e326.gif

五、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;

@Slf4jpublic classReentrantLockExample1 {private final static int clientCount=5000;private final static int threadCount = 50;private static int count =0;private final static Lock lock = newReentrantLock();public static voidmain(String[] args) {

CountDownLatch countDownLatch= newCountDownLatch(clientCount);

Semaphore semaphore= newSemaphore(threadCount);

ExecutorService executorService=Executors.newCachedThreadPool();for(int i=0;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 voidtest(){lock.lock();try{

count++;

}finally{lock.unlock();

}

}

}

结果:

22:55:05.999 [main] INFO com.practice.aqs.ReentrantLockExample1 - count:5000Process finished with exit code0

3.像我们刚刚讲的,ReentrantLock拥有许多特性,实际上它提供了很多方法来实现这些特性。如

lockInterruptibly():如果当前线程没有被中断的话,那么就获取锁,如果已经被中断,就抛出异常。

isLocked():查询此锁定是否有任意线程保持。

isHeldByCurrentThread():查询当前线程是否保持锁定状态。

isFair():判断是不是公平锁。

getHoldCount():查询当前线程保持锁定的个数。

...

4.接下来,我们要说一下另外一个锁叫做ReentrantReadWriteLock,在没有任何读锁的时候,才可以取得写入锁,这个要求这个类的核心。

@since 1.5

*@author Doug Lea*/

public classReentrantReadWriteLock

implements ReadWriteLock, java.io.Serializable {private static final long serialVersionUID = -6992448646407690164L;/** Inner class providing readlock*/

privatefinal ReentrantReadWriteLock.ReadLock readerLock;/** Inner class providing writelock*/

privatefinal 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;

@Slf4jpublic classReetrantReadWriteLockExample1 {private final Map 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 SetgetAllKeys(){

readLock.lock();try{returnmap.keySet();

}finally{

readLock.unlock();

}

}/**

* 写入map

* @param key

* @param value

* @return*/

publicData put(String key,Data value){

wirteLock.lock();try{returnmap.put(key,value);

}finally{

wirteLock.unlock();

}

}class Data{//这里声明一个内部类

}

}

为什么说写线程容易遭遇饥饿呢?原因就是有线程不停的读,导致写永远难以执行,以上就是ReentrantReadWriteLock的简单介绍,后续有待补充。这个类其实应用场景很少,只要知道了解就可以了,不过有兴趣的话可以详细搜索一下这个类的用法。

5.这里再介绍一个锁,叫做StampedLock。它控制锁有三种模式:读、写、乐观读,重点在于这个乐观读上。一个StampedLock是由版本和模式两个部分组成。锁获取方法返回的是一个数字作为票据,它由相关的锁状态来控制并发线程的访问。在读锁上分为悲观锁和乐观锁。所谓乐观读,其实就是如果读的操作很多,写的操作很少的情况下,我们可以乐观的认为读的操作和写的操作同时发生的几率很小。

以下是StampedLock内部注释中提供的一个例子。已表明中文注释,可以试着理解一下。

classPoint {private doublex, y;private final StampedLock sl = newStampedLock();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;

@Slf4jpublic classStampedLockExample1 {private final static int clientCount = 5000;private final static int threadCount = 50;private static int count =0;private final static CountDownLatch countDownLatch = newCountDownLatch(clientCount);private final static Semaphore semaphore = newSemaphore(threadCount);private final static StampedLock stampedLock = new StampedLock();//定义stampedLock

public static voidmain(String[] args) {

ExecutorService executorService=Executors.newCachedThreadPool();for(int i=0;i

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 voidtest(){long stamped = stampedLock.writeLock();//定义写锁,这时候他会返回一个stamped值

try{

count++;

}finally{

stampedLock.unlock(stamped);//释放的时候带上这个票据stamped

}

}

}

结果自然是没有问题的,其实看中的是它在这种读线程多写线程少的场景下性能好的特点:

22:00:33.261 [main] INFO com.practice.aqs.StampedLockExample1 - count:5000Process finished with exit code0

6.接下来看一下Condition这个类,直接看个例子。

mport lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.ReentrantLock;

@Slf4jpublic classConditionExample {public static voidmain(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 signal22:21:43.777 [Thread-1] INFO com.practice.aqs.ConditionExample - get lock

22:21:46.777 [Thread-1] INFO com.practice.aqs.ConditionExample -send signal22:21:46.777 [Thread-0] INFO com.practice.aqs.ConditionExample - getsignal

Process finished with exit code0

例子中已经标明了程序的输出顺序。

平时的时候大家可能Condition用的地方会很少,有兴趣的话可以继续研究一下。

总结

我们来总结一下这一章涉及的锁的类,第一个是synchronized,发生异常的时候,JVM会自动释放锁。而ReentrantLock,ReentrantReadWriteLock,StampedLock它们三都是对象层面的锁定,要保证锁一定会被释放,就必须把unlock放到finally里面执行。StampedLock对吞吐量有巨大的改进,特别是读线程很多写较少的场景下。

这里涉及的锁还是比较多的,那么何时该应用什么锁呢?

1.当只有少量竞争者的时候(少量竞争线程的时候),synchronized是一个很好的通用锁实现。

2.竞争者比较多的时候,但是线程数增长趋势我们是可以预估的时候,ReentrantLock是一个很好的通用锁实现。

在这里要注意一点,除了synchronized是会被JVM自动释放的,而其他锁一旦没有被手动释放,是会发生死锁的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值