个人博客网:https://wushaopei.github.io/ (你想要这里多有)
一、 J.U.C之AQS-介绍
1、定义:
AbstractQueuedSynchronizer简称AQS,AQS是JUC的核心,AQS是并发类的重中之重,可以用来构建锁的同步框架。
2、AQS底层的数据结构:
3、AQS的特点:
- 使用Node实现FIFO队列,可以用于构建锁或者其它同步装置的基础框架
- 利用了一个int类型表示状态
- 使用方法是继承;
- 子类通过继承并通过实现它的方法管理其状态{ acquire 和 release } 的方法操纵状态;
- 可以同时实现排它锁和共享锁模式(独占、共享)
4、AQS具体实现的大致思路:
AQS内部维护了一个CAS队列来管理锁,线程会首先尝试获取锁,如果失败就会将当前线程以及等待状态的信息包装成一个节点加入到之前的同步队列Sync queue里。接着会不断循环尝试获取锁,他的条件是当前节点为head的直接后进才会尝试。如果失败就会阻塞自己,知道自己被唤醒,而它持有锁的线程在释放锁的时候会唤醒队列中的后进线程,基于这些基础的设计和思路,jdk提供了许多基于AQS的子类。
5、AQS 同步组件
- CountDownLatch
CountDownLatch 是一个闭锁,通过线程计数来保证线程是否需要一直阻塞;
- Semaphore
Semaphore能控制同一时间并发线程的数目
- CyclicBarrier
- ReentrantLock
- Condition
- FutureTask
二 、J.U.C之AQS-CountDownLatch
1、 CountDownLatch执行原理图:
说明:
CountDownLatch是一个同步辅助类,通过它可以完成类似于阻塞当前线程的功能,换句话说一个线程或多个线程一直等待,直到其他线程执行的操作完成。
Latch它使用一个给定的计数器来初始化,该计数器的操作是原子操作,就是同一时间只能有一个线程去操作计数器。调用该类的await()方法的线程会一直处于阻塞状态,直到线程调用countDown()方法使当前的cnt值变成0,每次countDown时,计数器的值会减一。当计数器的值减到0的时候,所有因调用await()方法而处于等待状态的线程就会往下执行。这个操作只会出现一次,因为cnt值是不能被重置的。
2、CountDownLatch线程安全测试代码实例:
执行打印结果:
由打印结果可知,finish是在所有线程执行完后才执行的。
分析:这是因为CountDownLatch.countDown()计数器已经降到0了,所以在最后的CountDownLatch.await()校验通过,接着就打印最后面的finish字段
3、实现CountDownLatch在指定时间内完成:
作用 :可以给定时间让线程有时间反应过来执行。
三、J.U.C之AQS-Semaphore
1、定义
Semaphore维护了当前访问的个数,通过同步机制来控制同时访问个数,Semaphore可以保存有限个数的链表。
2、Semaphore的使用场景:
常用于仅能提供有限访问的资源,比如项目中使用的数据库,数据库的链接数最大只有20,而项目的并发数可能远远大于20;如果同时对数据库进行操作,就可能出现因为无法取数据库连接数而导致的异常,这个时候就可以通过Semaphore来进行并发访问控制。当Semaphore把并发数控制到一定数量时,就和单线程很相似了。
3、Semaphore线程安全代码演示:
执行打印线程结果:
由图中结果可以知道,代码中Semaphore限定了当前同时执行的线程数为3,而在统一时间只有3个线程被打印,一秒后又有3个线程被打印,由此可以说明Semaphore是可以实现线程安全操作的。
4、测试多个许可条件下的Semaphore业务场景:
执行打印线程结果:
由执行结果可知,每一秒Semaphore只执行一次线程操作,这是因为同一时间内获取3个许可,又同一时间内释放3个许可,就相当于是在同一时间内只执行一次test(threadNum)线程操作。而同一秒钟内拿到了3个许可,而与原本Semaphore定义的3个可执行线程数一校验,发现在同一秒钟内没有多余的许可可以释放了。这就很接近单线程的执行了。
5、案例:
1) 场景需求:当前的并发数是3个,超过部分则丢弃:
当尝试获取一个许可,如果获取不到,就丢弃;获取到,就执行。又由于线程业务处理消耗了一秒,如:
所以,最终可能执行的线程数只有Semaphore最初定义时所并发执行的3个线程。
2) 场景需求:超时时间内,执行许可:
当尝试在5秒内获取一个许可,如果超时后依旧获取不到,就丢弃;获取到,就执行。
最终可能执行的超过约定的初始3个线程,但不一定全部线程数都能够被执行。
四、J.U.C之AQS-CyclicBarrier
1、图解 CyclicBarrier
CyclicBarrier是一个同步辅助类,它允许一组线程同步等待,直到某一个公共的屏障点,通过它可以实现线程之间的相互等待,每个线程都各自就绪后,才能够继续执行后续的操作。
它和CountDownLatch一样都是通过计数器来实现的。CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。
比如:
我们用excel保存用户的银行流水,excel的每一页保存用户每一年的银行流水,现在我们要统计用户的日均流水,我们就可以用多线程处理每个页里面的银行流水,都执行完以后得到每一个页里面的日均银行流水,之后CyclicBarrier.action利用多线程计算结果再计算用户的日均银行流水。
2、CyclicBarrier和CountDownLatch的区别:
第一点:CountDownLatch的计数器只能使用一次,CyclicBarrier的计数器可以使用resume方法重置,重复使用;
第二点:CountDownLatch主要实现一个或N个线程需要等待其他线程完成某项操作之后才能去往下执行,它描述的是一个或N个线程等待其他线程的关系;
而CyclicBarrier是实现多个线程之间相互等待,在所有线程都满足条件之后才能去执行后续的操作,它描述的是各个线程内部相互等待的关系。
3、CyclicBarrier线程安全代码演示:
4、CyclicBarrier支持传入等待时间,代码演示如下:
5、CyclicBarrier支持线程到达屏障时,优先支持running:
执行结果:
线程中的屏障是 4 is ready ,此时所有入队线程已准备就绪,即到达了屏障,在进入执行序列时,会优先执行 running ,即匿名指定的线程对象。
五、J.U.C之AQS-ReentrantLock与锁-1
1、 ReentrantLock (可重入锁) 和 synchronized 区别
可重入性:ReentrantLock的字面意思就是可重入的,而synchronized也是属于可重入的,两者的却别不大;都是进入一次线程,锁的计数器就自增1,所以等到锁的计数器下降为0时才会释放锁。
锁的实现:synchronized是依赖于JVM实现的,而ReentrantLock基于JDK实现的;两者的区别就类似于用户控制操作系统来实现和自己敲代码实现的区别;
性能的区别:在synchronized的关键字优化以前,synchronized的性能比ReentrantLock的性能差很多;从synchronized引入了偏向锁、轻量级锁也就是自旋锁后,它们两者的性能就差不多了,在两者都可用的情况下,官方更建议使用synchronized的,因为它的写法更容易。
功能区别:
从便利性来说:synchronized比较方便、简洁,并且它是有编译器去保证锁的加锁和释放的;而ReentrantLock需要我们来手动声明加锁和释放锁,为了避免忘记手动释放锁而导致死锁,最好是在finally中声明释放锁;
从细腻度和灵活度来说:ReentrantLock要优胜于synchronized的。
2、ReentrantLock独有的功能
- 可指定是公平锁还是非公平锁
- 提供了一个Condition类,可以分组唤醒需要唤醒的线程
- 提供能够中断等待锁的线程的机制 , lock.lockInterruptibly()
所谓公平锁,就是先等待的线程先获得锁,这一点ReentrantLock是独有的,可以自己选择公平还是不公平
3、ReentranLock的定义:
ReentrantLock实际上是一种自旋锁,通过循环调用CAS操作来实现加锁,它的性能比较好也是因为避免了进入内核态的阻塞状态,想尽办法避免进入内核的阻塞状态是去分析和理解锁的设计的关键要素。
4、什么情况下适合使用ReentrantLock呢?
当你需要用到ReentrantLock这三个独立功能的时候你就必须使用ReentrantLock,而且你可以根据业务场景来选择使用ReentrantLock或者是Synchronized的。
5、ReentrantLock 和 Synchronized
Synchronized 能做的事情,ReentrantLock都能做,而ReentrantLock能做的Synchronized却有许多都做不了,在性能方面ReentrantLock不比Synchronized差!
那么,是否可以抛弃Synchronized呢?
不可以,Javautil.current.lock包中的锁定类是应用于高级用户和高级情况的工具,一般来说,除非你对lock的某个高级的包邮明确的需要,或者有明确的证据。这里不仅仅是怀疑,而是表名特定的情况下,同步已经成为可伸缩性的瓶颈的时候,建议还是使用synchronized。
并且即使是相对于高级锁定类而言,synchronized也有它的优势,比如,使用synchronized 的时候,你不会忘记释放锁,退出synchronized块的时候,jvm会为你做这件事情。你会很容易忘记使用finally去释放锁,这对程序非常有害。你的程序会通过测试,但在实际工作中会出现死锁,当时会很指出原因。这也是很不建议初级开发人员使用ReentrantLock的好理由。
另外一个原因是,当JVM使用synchronized管理线程的锁定请求和释放时,JVM在生成线程转储时,能够包括锁定信息。这些信息对调试程序很有帮助。有利于分析死锁以及其它异常问题的来源。而lock类只是普通的,jvm不知道具体哪个线程拥有lock对象,而且几乎每个开发人员都熟悉synchronized的,它可以在JVM的所有版本中工作,在jdk5.0成为标准之前,使用lock类将意味着要利用lock的特性而不是每个JVM都有的,而且也不是每个开发人员都熟悉的。
所以,程序员在开发中遇到的大部分的加锁的情景都可以使用synchronized,那种特别高级的特殊情况还是很少的。
6、ReentrantLock相关类的代码演示;
执行并打印结果:
7、重要底层实现原理分析:
ReentrantLock的底层代码实现中:
默认构造方法中生成给定的是一个不公平的锁;
在带参构造中,根据传入的true或者false来决定是使用公平锁或不公平锁
仅在调用时锁定未被保持的情况下才获取锁定
如果锁定在给定的等待时间内没有被线程保持,且当前线程没有被中断,则获取这个锁定
如果当前线程没有被中断,就获取锁定;如果被中断,就抛出异常。
查询此锁定,是否有任意线程保持
查询当前线程是否处于锁定状态
isFair()作用是判断是不是公平锁
8、扩展:ReentrantReadWriteLock
作用:在没有任何读写锁的时候才可以取得写入锁
ReentrantReadWriteLock可以用于实现悲观读取,即读取时经常有另一个可能要写入的需求,为了保持同步,ReentrantReadWriteLock的读取锁定就可以派上用场了。如果读取情况很多,写入情况很少的情况下,使用ReentrantReadWriteLock可能会使写入线程遭遇饥饿,也就是说写入线程迟迟不能竞争到锁定,而一直等待。
六、 J.U.C之AQS-ReentrantLock与锁-2
1、ReentrantReadWriteLock实例代码演示:
说明:一个类里面封装了一个内部的map,这个map不需要把所有的方法都暴露给别人,它们要做的事情完全都通过我提供的方法来使用。而我提供一些单独的方法来给外部使用,使用的时候为了避免发生并发的问题,可以加上ReentrantReadWriteLock来实现在读和写的时候分别加锁。只有在没有读写锁的时候才能进行相应的插入操作或读操作。
这里实现的是悲观读取。
在进行新的插入操作时,必须要当前执行的插入或读操作完成后才能继续执行。
如果发生写入很多,而读取很少的时候,使用ReentrantReadWriteLock类就可能会遭遇饥饿。所谓饥饿,就是代码中的
写锁一直想执行,但是大量的读操作就会导致写操作永远都无法执行,一直在等待,而不知道什么时候能真正的去执行这个写操作。
2、另一种锁:StampedLock
StampedLock控制锁有3中模式,分别是:写、读、乐观读(重点)
一个StampedLock的状态是有版本和模式两个部分组成,锁获取的方法是返回一个数字作为票据(stamped)。它用相应的锁状态来表示并控制相关锁的访问。数字0表示没有写锁被访问;读锁被分为悲观锁和乐观锁;所谓乐观读是写的操作很多,读的操作很少的情况下,我们可以认为写入和读取同时发生的几率很少,因此不悲观的使用完全读取锁定,程序可以采取查看
读取资料之后,是否遭到写入执行的变更,再采取后续的措施。这一个相应的改进可以大幅度提高程序的吞吐量。
3、StampedLock实例代码演示:
4、StampedLock实例代码演示:
5、代码演示Condition的作用:
执行打印结果:
结果分析:
首先,我们定义了一个ReentrantLock的实例,从实例中取出了一个Condition,也就是reentrantLock.newCondition();操作;
在main方法中,我们声明了两个线程Thread方法,按顺序命名为T1和T2.
在T1中,使用reentrantLock.lock()方法将线程T1加入到了AQS的等待队列里面,接着线程T1输出了“wait signal”日志信息,紧接着T1中调用了condition.await()方法,这时就将T1从AQS的等待队列中移除了,该操作对应了锁的释放。
接着,它就加入到了condition的等待队列中去了,等待这该线程需要的序号。这里对应了AQS原理图中的Condidtion queue,如上图。
线程T2因为线程T1释放锁的关系被唤醒,并判断它是否可以取到锁,于是获取锁,也加入到了,AQS的Sync queue等待队列中,当执行取锁任务后,就继续执行后续的日志打印“get lock”操作,线程T2在执行打印完成后,接着执行condition.signalAll()方法,紧接着就输出了后续的日志打印操作“send signal ”发送信号操作,
而此时condition线程队列中有线程T1的一个节点,于是它就被取出来加入到了AQS的等待队列里面去。注意:此时线程T1并没有被唤醒,只是加入到了AQS服务里面的Sync queue队列里面。
当线程T2的signaAll()发送信号的方法执行完成以后,调用了reentrantLock.unlock()方法,就释放了锁。此时在AQS的Sync queue队列中只有线程T1的线程,而AQS按照线程从头到尾的顺序唤醒了T1线程。于是线程T1继续开始执行,继续执行的时候就得到了最后的线程输出,即“get signal”日志的打印。
通过以上代码执行的结果以及对执行过程的分析,可以看得出来,Condition也是多线程间协调通讯的工具类。使得某个或某个线程一直等待条件(unlock()),只有当该条件具备,这些等待的线程才会被唤醒。以上案例中,唤醒所具备的条件就是等待的信号,包含signal和signalAll这两个方法,分别用于唤醒一个或多个等待的线程。这些被唤醒的线程会重新按顺序获得锁,并执行相应的业务操作。