目录
概述
并发在技术层面上的问题
volatile&synchronized&cas&final
volatile
synchronized
cas
final
JMM
线程基础
进程与线程
定义线程任务
线程的生命周期
线程的优先级
守护线程
线程中断
等待通知机制
线程的Join/Yield
AQS
独占模式
共享模式
响应中断
支持超时
Lock
ReentrantLock
ReentrantReadWriteLock
并发组件
线程池
一晃的时间,做Java开发已经有七个年头了,过去的几年多数时候其实是混混度日,没有对自己的工作和生活有太多的深入思考,当我意识到的时候,犹如晴天霹雳,这段时间思考了很多但依然没有什么头绪,我想或许应该换一种策略,找出几个有代表性的问题去深入思考一下,而技术的意义就是其中一个。
而之所以再次来写Java并发,原因在于对这块知识的理解其实一直的片面的,稍微回想一些相关的知识点,好像都有所了解,但对于并发的意义以及Java并发模块的整体设计思路和相关的思想并没有一个比较深入的理解,所以借此机会,再来探索一下Java并发。这次和以前相比,会更侧重于实现原理和设计思想,而不是范范的罗列一些人尽皆知的知识点。好了,那就开始吧。
并发这个词,从广义上理解应该是多件事情同时发生,比如经常听到的并发症指的是患一种病的同时又患了由它引起的另一种疾病,那延伸到计算机的世界也是一样,但由于在现代计算机中的最小执行单元是线程,所以一说到并发大多数人就会想到多线程,但这样理解还是有点狭隘,比如对于操作系统用户来说,多个应用程序同时执行这本身也是一种并发,所以,我对于并发的广义理解是,多件同时发生的事或多件同时正在做的事,这里的”事“可以是生病,可以是旅行,可以是线程,可以是进程,可以是任何事。
那么,在Java中的并发指的是多线程,也就是多个线程同时执行。这里需要明确两个概念,并发和并行,并发指的是做多件事情,但不是在同一时间做,这就好比一个人他同一时刻只能做一件事情,但在一天内可以做多件事情,而并行指的是在同一时刻同时做多件事情,这种事情人是做不到的,计算机之所以能做到并行,是因为多cpu/cpu多核的出现,如果是单cpu,计算机也只能通过cpu时间片轮转的方式实现并发,但没办法实现并行。
凡事都有利弊两面,作为一个面向对象设计语言,Java在引入多线程时,解决了什么问题?有带来的什么问题?对于这些多线程的”并发症“又是如何解决的?这三个基本问题必须贯穿整个学习Java并发的过程。
多线程主要解决了三个方面的问题:
在计算机中,一个线程就相当于一个干活的人,当一个工作不能在给定的时间内完成时,增加人手是一个有效的选择,春节期间武汉修建火神山雷神山就是一个很好的利器,另外,引入多线程可以优化原有的顺序编程模型,对于一些常场景来说极大的简化了实现,最有代表性的是生产者-消费者模式和java图形化界面,如果用顺序编程模式就比较难实现,第三,尽最大努力压榨计算机资源,别让它占着茅坑不拉屎,比较计算资源还是比较贵的,尤其是一些大型的服务器。
多线程再带来好处的同时,也带来了一些并发症:
当线程的数量超过cpu的数量时,并行就转换成了并行+并发,而并发本身其实是通过cpu的时间片轮转实现的,如果线程的数量特别多,会造成线程上下文切换的开销大于它带来的性能提升,所以当线程数量达到一个临界值时,程序的整体性能就开始下降了,第二个问题是死锁,死锁问题源于多线程操作共享资源时的锁的开启和释放,第三是计算机资源本身的限制,造成了不能一直通过增加线程的方式来提高程序性能。
从技术层面讲,学习Java并发其实就是学习Java是如何解决并发编程中的问题的的,那么并发问题的根源都可以归结为以下几个方面:
可见性问题是由现代cpu硬件架构带来的,现在cpu架构多采用多cpu/cpu多核 + 多级缓存的架构,cpu在运行时会首先从主内存中将数据加载到cpu缓存中进行计算,计算完成后再写入主内存,另外,java线程模型在cpu多级缓存架构的基础上,也具备这样的特性,每个线程都有自己的工作内存,线程在计算数据时,会先把数据从主内存加载到自己的工作内存,再写回到主内存,这种架构模式在多线程环境下,如何保证每个线程工作内存中的主数据的副本的一致性就成了一个巨大的挑战,这就是内存可见性问题。
原子性问题本质上其实是由cpu时间片轮转引起的,如果一个操作在cpu级别不是一个原子指令,那么它在执行的时候很可能会被打断,这样多个线程交叉在一起就可能造成计算结果错误。
有序性问题其实是由两个因素引起的,一是cpu为了提高计算速度,会对待执行的指令集进行乱序优化,二是java编译器在编译代码的时候,为了最大化性能,也会对代码指令进行重新编排以达到性能的最大化(java编译器在重排序指令时只能保证单线程环境下结果的正确性)。
Java作为一个支持并发编程的面向对象语言,必须解决这三个问题,才能为程序员提供稳定、好用的并发编程环境。从总体上看,Java通过一系列的关键字、并发组件等来解决上述问题,比如volatile、synchronized、Atomic、AQS、BlockingQueue等等,而学习解决并发编程中出现的问题的关键字和组件其实就是我们学习java并发编程的重点。
从问题入手总比一上来就学有效的多。
volatile在一些并发框架和本地缓存框架的源码中经常能看到,它的主要作用有两个
volatile是在编译时插入一个#lock信号来保证内存可见性的,这个#lock信号的作用就是,当前线程对volatile变量进行修改时,会强迫其他线程其他线程工作内存中该变量的副本失效,从而强迫其他线程从主内存中重新拉取最新的数据。而对于禁用指令重排序这一点,其实并不是完全禁用,知识在对被volatile修饰的变量进行指令重排序时,需要遵循一个happens-before规则:
java是通过插入内存屏障的方式实现这个happens-before规则的,正因为volatile有这个特性,有时也把它称为轻量级的锁,通过使用volatile就可以避免通过同步锁的方式实现对单个变量操作的线程安全。一个比较经典的场景是:实现一个双检锁模式的单例
public class Singleton {
private static volatile Object instance;
public Object getInstance() {
if (Objects.isNull(instance)) {
synchronized (Singleton.class) {
if (Objects.isNull(instance)) {
instance = new Object();
}
}
}
return instance;
}
}
另外,volatile一般用来修饰状态变量,比如AQS中的同步状态
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
synchronized是解决并发安全问题最常用的工具,它可以修饰成员方法、静态方法和构造同步代码块
从jdk1.6开始,对synchronized进行了优化,引入了偏向锁和轻量级锁,极大的提高了synchronized的性能。使用synchronized进行同步处理时能够自动进行加锁和解锁操作,底层采用一对monitorenter和monitorexit指令实现。在语言层面,synchronized锁的状态存储在锁对象的对象头中,对象头中包含了锁状态、偏向线程ID、hashcode、gc年龄等信息。
当只有一个线程进行加锁和解锁时,synchronized采用的是偏向锁,偏向锁在底层实际上并没有真正的执行加锁和解锁,而是将锁对象头中的偏向线程ID执行当前线程,当再次获取锁时,实际上知识判断偏向线程的ID是否为当前线程,极大的提高了synchronized的性能;如果锁的竞争程度加剧,有多个线程获取锁状态时,偏向锁就会升级为轻量级锁,轻量级锁的实现原理是,每个线程在自己的线程栈中开辟一块内存空间,然后把锁对象头中的信息复制过来,然后利用cas的方式,尝试将锁对象头执行当前线程栈中的锁对象头的副本,如果执行成功,说明锁获取成功,轻量级锁适合线程任务比较小、能够快速释放锁的场景;但如果锁的竞争程度进一步加剧,轻量级锁就会升级为重量级锁。
cas是compare and set的缩写,意思就是先比较后替换,是一种线程安全的数据修方式,一般和loop一起使用,实现线程安全的数据修改,这种方式由于不会阻塞线程,所以执行效率会高很多,但cas也存在一些问题:
对于ABA问题一般可以通过为数据增加版本号来解决,活锁问题可以通过在逻辑中增加超时即使开解决。cas在java中的juc中被广泛的应用。
final可以修饰类、方法和变量,被final修饰的类不能被集成,被final修饰的方法不能被重写,被final修饰的变量不能被修改,通过final可以定义不可变对象,定义不可变的共享消息,是解决线程安全问题的一个很重要的手段。
JMM是Java Memory Model的缩写,即Java内存模型。
提到JMM,首先要了解一下现代计算机的多cpu/cpu多核+多缓存架构,由于cpu在计算数据时,是先从主内存中将数据和指令加载到多级缓存中,然后再从缓存中取出来执行,那么当存在多个cpu时,如何保证多cpu之间数据的一致性就成了一个棘手问题,为了解决这个问题,牛逼的科学家们提出了MESI协议,MESI的全程是Modify-Exclusive-Shared-Invalid的缩写,举一个简单的例子,假如有两个线程对主内存中的x执行x+=1的操作,当线程A从主内存中加载x的值时,总线上x的状态是E,当线程B也从主内存中读取x的值时,总线上x的状态边成S,在线程A将结果写回主内存的过程中,总线上x的状态为M,写完后,x的状态变为I,这个状态I会使得除了A中的其他cpu缓存中的x失效,当B在对x进行计算时,就会重新从主内存中加载新的数据。
另外一个需要说明的是cpu乱序执行优化和Java编译器指令重排序,这两种机制都是为了提高程序的执行速度,但这给多线程下的线程安全带来了更大的挑战。
由于cpu的内存结构、Java线程内存模型以及cpu乱序优化和编译器指令重排序的存在,这就引起了线程安全需要面对的三个核心问题:
那JMM实际上可以理解为在Java语言层面对MESI协议的一种抽象,,它的作用是解决由Java线程模型引起的在多线程环境下的线程安全问题,以及解决cpu乱序优化和编译器指令重排序带来的线程安全问题,具体的,JMM可以简单的理解为一系列为了解决线程安全问题而提出的happens-before规则:
但JMM本身还是一个规范,它本身并不能解决任何安全问题,JMM需要一些载体,而这些载体就是synchronize/volatile/final/AQS等一系列的Java并发安全组件
进程是操作系统分配资源的基本单位,线程是cpu运行的基本单位,一个进程内可以包含多个线程。
java本身知识线程优先级的设置,通过Thread.setPriority(int priority),线程优先级越高,分配的cpu时间片的概率就越大,但现在很多操作系统直接忽略了线程优先级,因为它可能会影响到系统的稳定性,所以设置线程优先级一般不会起到什么作用。
守护线程指的是与主线程相伴相生的后台线程,通过Thread.setDaemon(true)来设置一个线程为守护线程,当主线程运行完成并退出后,守护线程会立即终止。
每个线程都有一个中标识,Thread提供了三个方法来让程序能够对线程中断进行相应
其中,interrupt()方法的作用是修改线程中断表示为true,interrupted()方法判断线程中断状态,同时会重置中断状态,比如当前中断状态为false,那么调用interrupted()方法返回true,但在此调用时就会返回false,isInterrupted()方法也是判断当前线程的中断状态,但它不会重置线程的中断信号。
Thread级别的等待/通知机制是通过synchronized + wait/notify来实现的
/**
* @author Echo
* @date 2020/8/8 12:31 下午
*/
public class WaitNotify {
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) throws Exception {
Thread waitThread = new Thread(new Wait());
waitThread.start();
TimeUnit.SECONDS.sleep(1);
Thread notifyThread = new Thread();
notifyThread.start();
}
static class Wait implements Runnable {
@Override
public void run() {
synchronized (lock) {
//如果条件不满足,一直等待
while (flag) {
try {
System.out.println(Thread.currentThread() + " flag is true. wait @ " +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.wait();
}catch (InterruptedException ex) {}
}
//满足条件时,完成工作
System.out.println(Thread.currentThread() + " flag is false. running @ " +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
}
}
}
static class Notify implements Runnable {
@Override
public void run() {
synchronized (lock) {
try {
System.out.println(Thread.currentThread() + " hold lock. notify at @" +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.notifyAll();
flag = false;
TimeUnit.SECONDS.sleep(5);
}catch (InterruptedException ex) {}
}
}
}
}
join的作用是将当前线程插入到执行序列中,主线程会一直等待当前线程执行完成后才继续执行
/**
* @author Echo
* @date 2020/8/8 12:41 下午
*/
public class JoinTest {
public static void main(String[] args) throws Exception {
Thread main = new Thread(() -> {
try {
System.out.println("start");
TimeUnit.SECONDS.sleep(5);
System.out.println("end");
}catch (Exception ex) {}
});
main.start();
main.join();
System.out.println("join complete");
}
}
yield操作会让当前线程让出cpu的占优权,但它并不能保证当前线程从运行状态编程就绪状态,具体如何执行还是由cpu决定,所以yield在实际应用中非常少见,因为它是一种不可控的操作。
AQS是AbstractQueuedSynchronizer的缩写,是JUC中一个基础组件,他是一个基于同步状态的抽象同步框架,很多更高级的同步工具类都是基于AQS实现的,比如最常用的Lock、CountdownLatch、CyclicBarrier、Semaphore等。
AQS的核心包括同步状态、同步队列、等待队列以及一些列操作同步状态的方法,使用AQS实现同步功能时能够支持更高级的功能,比如超时、可中断、独占模式以及共享模式等,下面一一说明。
/**
* The synchronization state.
*/
private volatile int state;
AQS的同步状态是一个呗volatile修饰的int变量,这个变量根据不同的上层实现会有不同的取值,比如在ReentrantLock中同步变量的取值就是1代表锁被获取,而在Semaphore或CountdownLatch中,初始值往往会设置一个比1大的值。
同步队列是AQS内部维护的一个双向链表,用来存储获取同步状态失败的线程,等待队列用在Condition的等待/通知场景中,存在那些正在等待的线程。
以独占模式同步状态的获取和释放为例来说明AQS的核心原理。独占模式下获取和释放同步状态的入口是acquire()和release()这两个方法。
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看到这个方法的注释写的非常清楚:这个方法被用在排他模式下,不会对中断进行响应,在实际应用时上层组件需要重新tryAcquire方法,如果tryAcquire方法返回false,当前线程就会被添加到同步队列中并阻塞,注释中还专门提到了,这个方法被用在了Lock的实现中,下面详细看下这个方法的实现:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
tryAcquire方法由上层组件实现
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这个方法的作用是将Node添加到同步队列(双向链表),可以看到,一开始它进行了一个快尝试,通过cas的方式将node节点添加到同步队列的末尾,如果设置失败了,就会进入到enq方法中,通过死循环加cas的方式把node节点添加到同步队列的末尾,而不是直接进入到一个死循环中,从这里可以看出Doug Lea对性能的追求。
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
前面把Node节点添加到了同步队列的节点,acquireQueued这个方法的作用是让Node节点对应的线程进入阻塞状态,每一个节点阻塞以后,由前一个节点唤醒,具体的,当前节点的前驱节点执行完成 释放掉同步状态,它会通过LockSupport唤醒他的后继节点,后继节点获取同步状态,然后将自己设置为头节点。
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release方法用于释放同步状态,用在排他模式下,上传组件需要实现tryRelease方法,这个方法成功返回时说明同步状态被成功释放了,紧接着就会调用unparkSucessor方法,这个方法通过LockSupport.unpark(s.thread);唤醒当前节点的后继节点。
共享模式的运行原理与独占模式基本一致,只是调用入口不同,共享模式是通acquireShare和tryAcquireShare实现的,区别在于共享模式下的同步状态值一般是大于1的,比如CyclicBarrie和Semaphore等同步组件都是通过AQS共享模式实现的。
对于synchronized来说,是无法对相应中断的,为了提供更高级的同步能力,AQS支持了对中断的响应。实现的原理其实很简单,就是在现存加入等待队列并进入自旋的过程中,通过Thread.interrupted()方法来判断当前线程的中断状态,如果当前线程被中断了,那么就抛出InterruptedException异常。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
AQS实现超时阻塞的原理时使用了一个标准的超时编码范式
private boolean doAcquireNanos(int arg, long nanosTimeout) {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
for (;;) {
..
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (nanosTimeout > 1000L) {
LockSupport.parkNanos(this, nanosTimeout);
}
if (Thread.interrupted())
throw new InterruptedException();
}
}
1. 根据设置的超时时间计算出deadline
2. 在自旋的过程中用deadline-当前时间,计算剩余时间
3.如果剩余时间小于0,则直接返回false
4.如果生于时间大于1秒,则通过LockSupport挂起当前线程
5.如果当前现存被中断了,则抛出InterruptedException异常;
逻辑其实很清晰,这里有一个特别的地方需要注意,如果剩下的时间小于1秒,就不会被阻塞而是直接死讯还,因为这样可以保证超时时间的准确性。
Lock是java 1.5在juc包中对锁的抽象,与synchronized不同的是,Lock完全是用java代码实现的,而synchronized是由jvm底层来支持的。Lock是一个接口,定义了基本的加锁和解锁方法
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
常用的Lock实现主要有两个,一个是ReentrantLock和ReentrantReadWriteLock,前者是可重入的互斥锁,后者是读写索,下面先来说说ReentrantLock。
ReentrantLock地层是基于AQS实现的,他的内部提供了一个AQS的自类,并重写了tryAcquire和tryRelease方法,定义了对同步变量的操作逻辑。在这个实现类中,把同比状态=0定义为无锁状态,把同比状态=1定义为锁被获取的状态,当一个现存通过lock方法尝试获取锁时的处理逻辑如下:
1. 通过getStatus方法获取当前同步状态
2. 判断当前状态值,如果=0,说明锁没有被获取,尝试通过cas的方式将同比状态设在为1,如果获取成功,则加锁成功
3. 判断当前状态值,如果!=0,判断上次拿到同比状态的线程是否为当前现存,如果是,则当前的lock操作需要进行重入,就是直接把同比状态+1
4. 如果以上都不满足,说明同比状态正在被其他现存持有,那么当前线程就会通过AQS的逻辑,加入到同步队列中进行等待;
通过Lock的unlock方法释放锁,unlock里面调用了AQS的release方法,这个方法,这个方法内部会调用tryRelease方法,ReentrantLock的tryRelease方法中,会将当前AQS中的同比状态-1,如果减完之后的结果为0,则返回true,release方法就唤醒等待队列中的后继节点,如果减完以后大于0,说明重入加锁还没有释放完,返回false,等待队列中的后继节点就不会被唤醒。
另外,ReentrantLock还支持锁的公平性和非公平性两种模式,两种模式的区别在于,调用lock方法时,公平性锁会将当前线程直接添加到等待队列(非重入调用),而非公平摸索是直接利用cas对同比状态进行抢占,而不管现存等待队列中是否存在正在等待的线程,如果抢占失败了,再加入到等待队列中。
Lock的另一个实现类是ReentrantReadWriteLock,他是一个读写锁,能够很方便的实现读读不互斥,读写互斥和写写互斥,利用读写锁很容易实现一个简易缓存
public class Cache {
private static Map map = new HashMap();
private static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private static Lock rl = rwl.readLock();
private static Lock wl = rwl.writeLock();
public static final Object get(String key) {
rl.lock();
try {
return map.get(key);
}finally {
rl.unlock();
}
}
public static final Object put(String key, Object value) {
wl.lock();
try {
return map.put(key, value);
} finally {
wl.unlock();
}
}
public static final void clear() {
wl.lock();
try {
map.clear();
}finally {
wl.unlock();
}
}
}
ReentrantReadWriteLock的底层也是通过AQS实现的,内部包含了一个AQS的子类,一个ReadLock和一个WriteLock,Readock和WriteLock内部调用了AQS的子类。
那么一个AQS是如何实现读写两种锁的呢,这一点是ReentrantReadWriteLock的核心,它是通过切割整形的同比状态为高16位和低16位来实现的,高16位表示读锁,低16位表示写锁。
先来说一下写锁的获取和释放,写锁是一个可重入的排他锁,内部通过AQS的tryAcquire进入到同步流程,
1.首先,判断当前同步状态是否为0,如果不为0,说明存在读锁或写锁
2.判断当前锁状态的拥有者是否为当前线程,如果是,则同步状态+1,否则返回false(因为写写互斥/写读互斥)
3.如果同步状态为0,则先用cas的方式设置同比状态,如果设在成功返回true,如果设置失败,说明同比状态已经被抢占了,返回false,后面走AQS的同步流程。
写锁的释放与ReentrantLock基本一致,就是将同比状态-1,直到减为0表示锁被完全释放。
读锁的获取与写锁有所不同,读锁是通过tryAcquireShare方法实现的,因为读读不互斥,所以它属于一个共享锁
1.首先判断读锁是否被其他线程获取(这里的其他很重要,因为ReentrantReadWriteLock的锁降级就是在这里实现的)
2.如果是,则直接返回false,当前现存进入同步队列等待
3.如果不是,首先用cas快速尝试设置同步状态,如果设在成功了,直接返回true,如果设置失败了,进入到一个fullLoop方法中通过死循环+cas的方式设置同步状态,指导成功(AQS共享模式的标准做法)
读锁是释放同样是通过cas的方式将同步状态-1。
CountdownLatch能够实现主任务等待多个子任务完成后再继续执行的功能,比如下面的示例代码
static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws Exception {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
countDownLatch.countDown();
System.out.println(1);
}catch (Exception ex){}
}).start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
countDownLatch.countDown();
System.out.println(2);
}catch (Exception ex){}
}).start();
countDownLatch.await();
System.out.println("complete");
}
输出内容:
2
1
complete
使用很简单,这里主要关注CountdownLatch的实现原理,主要来看await和countDown这两个方法。内部使用了AQS来实现同步
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
//同步状态值=0时,返回1 = 在AQS中表示成功获取同步状态
//同步状态值!=时,返回-1,在AQS中表示获取同比状态失败
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
可以看到,这是一个非常简单的AQS子类,重写了tryAcquireShared和tryReleaseShared方法
当调用countDown时,内部会调用AQS的releaseShared方法释放同步状态,这个很好理解,当调用await方法时,会调用AQS的acquireSchared方法,而在被重写的tryAcquire方法中的逻辑是,只有当同步状态为0时,才表示成功获取了同比状态,否则当先现存就会被AQS阻塞。
用上面的例子来解释以下,初始化时CountownLatch的信号量为2,当主线程调用await方法时,发现同步状体不为0,返回给AQS的值为-1,那么主线程就会被加入到等待队列中,然后两个子线程调用countDown时,释放同步状态,直到同步状态被释放到0时,才返回true,进入到AQS的releaseShared中的doReleaseShared,唤醒在等待队列中等待的主线程。
不得不说,设计的确实挺巧妙的。
CyclicBarrier实现的功能与CountdownLatch类似,他们的主要区别在于CountdownLatch只能使用一次,而CyclicBarrier能循环使用。
public class JucTest {
static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
System.out.println("-----------------------------");
}
});
static class TaskThread extends Thread {
CyclicBarrier cyclicBarrier;
public TaskThread(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("A");
cyclicBarrier.await();
System.out.println("Break A");
TimeUnit.SECONDS.sleep(2);
System.out.println("B");
cyclicBarrier.await();
System.out.println("Break B");
}catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static void main(String[] args) {
new TaskThread(cyclicBarrier).start();
new TaskThread(cyclicBarrier).start();
}
}
输出如下:
A
A
-----------------------------
Break A
Break A
B
B
-----------------------------
Break B
Break B
虽然说从效果上与CountdownLatch类型,但所底层实现原理完全不同,CountdownLatch底层是通过AQS实现的,而CyclicBarrier是通过Lock+Condition实现的 ,下面来看以下具体的实现原理。
CyclicBarrier的核心方法时await
//信号量数量
private final int parties;
//Lock
private final ReentrantLock lock = new ReentrantLock();
//等待队列
private final Condition trip = lock.newCondition();
//栏栅任务
private final Runnable barrierCommand;
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
/**
这个方法是实现CyclicBarrier的核心逻辑
*/
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
int index = --count;
//判断信号i量是否为0了,如果为0,说明可以执行下一个循环了
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
//如果设置了栏栅任务,就执行他的run方法
if (command != null)
command.run();
ranAction = true;
//这里会唤醒所有在Conditon上等待的线程
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
//如果在调用await方法时,没有设置超时时间,则直接加入等待队列
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
下面描述一个CyclicBarrier的核心流程:
在内部,有一个ReentrantLock和与之关联的Condition,一个信号量数量和一个栏栅任务,当子线程调用await方法时:
1. 在ReentrantLock上加锁,以便关联Condition对象
2.判断信号量的数量是否到0了,如果到0了,就会执行栏栅任务,然后在condition对象上调用signalAll方法唤醒正在等待的线程
3.如果信号量的数量大于0,判断在调用await时是否设在了超时时间,决定调用condition时是否需要制定超时时间。
Semaphore与前面两个同步组件的使用场景都不同,它通常被比作一个限流路口的信号灯,也就是说它规定了同时能够通过路口的最大车辆数。Semaphore一般被用在类似限流的场景中,比如对某个接口作访问限流控制。
Semaphore的实现原理比较简单,就是通过AQS的共享模式实现的,调用acqire方法时,通过tryAcquireShared获取同比状态,如果同步状态都获取完了,那么当前线程就进入AQS等待队列,当调用release方法时,通过tryReleaseShared释放同步状态,然后通过AQS唤醒在等待队列上等待的线程。
阻塞队列的基本逻辑是,当队列为空时,获取数据的操作被阻塞,当队列为满时,添加数据的操作被阻塞,juc中提供的阻塞队列的基本原理时通过Lock + Condition实现的等待/通知机制实现的,下面用一个简单的示例来说明一下
public class BoundedQueue {
private T[] items;
private int addIndex, removeIndex, count;
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public BoundedQueue(int capacity) {
items = (T[])new Object[capacity];
}
public void add (T t) throws InterruptedException {
lock.lock();
try {
//quue is full , wait in notFull condition
while (count == items.length) {
notFull.await();
}
items[addIndex] = t;
if (++ addIndex == items.length) {
addIndex = 0;
}
//notify all thread waiting in notEmpty
notEmpty.notifyAll();
}finally {
lock.unlock();
}
}
public T remove() throws InterruptedException {
lock.lock();
try {
//queue is empty, waiting new data
while (count == 0) {
notEmpty.await();
}
T x = items[removeIndex];
if (++removeIndex == items.length) {
removeIndex = 0;
}
--count;
//get new data success, notify all thread waiting in notFull to put data
notFull.notifyAll();
return x;
} finally {
lock.unlock();
}
}
}
在java中提供了一些常用的阻塞队列: