大家好!我是未来村村长,就是那个“请你跟我这样做,我就跟你这样做!”的村长!
未来村村长正推出一系列【To Up】文章,该系列文章重要是对Java开发知识体系的梳理,关注底层原理和知识重点。”天下苦八股文久矣?吾甚哀,若学而作苦,此门无缘,望去之。“该系列与八股文不同,重点在于对知识体系的构建和原理的探究。
一个进程是一个程序的一次启动和执行,一个进程一般由程序段、数据段、进程控制块组成:
每当使用Java命令启动一个Java应用程序时,就会启动一个JVM进程,所有的Java程序代码都是以线程运行,JVM找到程序的入口main()方法,然后运行main方法产生一个线程,同时还会启动另外一个GC线程用于垃圾回收。
线程是指“进程代码段”的一次顺序执行流程,线程是CPU调度的最小单位,而进程是操作系统资源分配的最小单位,线程之间共享进程的内存空间、系统资源。
线程的组成如下:
线程是“进程代码段”的一次顺序执行流程,一个进程由多个线程组成,一个进程至少有一个线程。线程是CPU调度的最小单位,进程是操作系统分配资源的最小单位。进程之间相互独立,但进程内部的各个线程之间并不完全相互独立。各个线程之间共享进程的方法区内存、堆内存以及系统资源。
Thread类是Java多线程编程的基础,通过继承Thread类创建线程类可以实现线程的创建:
Thread类的源码量较大,不作展示分析。我们只需要知道Thread定义了线程的状态以及操作线程的相关方法即可。
Thread也实现了Runnable接口,且Thread中有以下构造方法可通过传入Runnable接口实现对象参数来实现线程的创建。
//系统定义名称
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
//自定义名称创建
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
则通过实现Runnable接口来创建线程的步骤如下:
这里Thread的run()方法先判断target是否为null,这里的target类型就是Runnable,即我们传入的参数。
public void run() {
if (target != null) {
target.run();
}
}
我们也可以看看Runnable的源码,可以看到Runnable是一个函数式接口,即只有一个方法的接口,其与Thread都来自java.lang包。
package java.lang;
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
在使用Callable和FutureTask之前我们先来看看它们的源码,来认识一下他们。首先是Callable,同样是一个函数式接口,其中的call与run类似,但是其具有返回值,可通过泛型来定义。
package java.util.concurrent;
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
但是我们知道要创建线程离不开Thread类,所以这里使用了FutureTask进行牵线搭桥。我们可以看到FutureTask类的声明和构造器。
public class FutureTask<V> implements RunnableFuture<V> {
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
}
FutureTask继承了RunnableFuture,其源码如下。所以我们可以想到,通过FutureTask构造器可构造一个Runnable实例,这样就可以传入Thread代理执行。
package java.util.concurrent;
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
使用Callable和FutureTask创建线程的步骤如下:
可以通过Executors工厂类构建线程池,然后通过其execute()【没有返回值,只接收Runnable实例和Thread实例】方法和submit()【可接收有返回值的Callable实例,或Runnable实例和Thread实例】方法实现线程的创建和执行。但生产环境不允许通过Executors创建线程池,需要通过调用ThreadPoolExecutor的构造方法完成。
线程池具体原理与操作后续会进行说明。
在Thread源码中使用enum枚举了Thread的六种状态:
我们可以通过getState()方法获取线程的执行状态,或者通过isAlice()方法判断一个线程是否还存活。
JVM进程中的GC线程就是一个守护线程,守护线程的使用有以下要点:
一组线程或线程组的集合,在多线程情况下,对线程进行分组管理。直接在main方法中运行的线程或线程组,都属于main线程组,在main方法中运行的代码上一级为System线程组,其中线程的上一级为main线程组。
ThreadGroup threadGroup01 = new ThreadGroup()
Thread thread01 = new Thread(threadGroup01,new ThreadImplentsRunnable(),"thread-01");
Thread thread02 = new Thread(threadGroup01,new ThreadImplentsRunnable(),"thread-02")
Thread[] threadList = new Thread[10];
threadGroup.enumerate(threadList);
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup()
线程的通信可以定义为:当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,避免无效的资源争夺。通信方式有:等待-通知、共享内存、管道流。其中[等待-通知]是使用较普遍的通信方式。
Java内置锁可以使用wait()和notify()来实现”等待-通知“的通信方式。使用wait()方法以后,JVM会将当前线程加入该锁监视器的等待集合(WaitSet)。使用notify()后,JVM会唤醒该锁监视器等待集合中的第一条线程,若使用notifyAll会唤醒监视器等待集合的所有线程。
Java线程的创建和销毁代价都比较高,频繁的创建和销毁线程非常低效,所以出现了线程池。线程池的好处:
① Executor:Executor是Java异步目标任务”执行者“接口,其只包含一个方法execute(Runnable command)。
② ExecutorService:ExecutorService继承Executor,其新增了submit和invoke系列方法,对外提供了异步任务的接收服务。
③ AbstractExecutorService:AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。
④ ThreadPoolExecutor:JUC线程池的核心实现类,线程池预先提供了指定数量的可重用线程,并对每个线程池都维护了一些基础数据统计,方便线程的管理和监控。
⑤ ScheduledExecutorService:继承于ExecutorService,用于完成”延时“和周期性任务的调度线程接口。
⑥ ScheduledThreadPoolExecutor:它提供了ScheduledExecutorService中的”延时执行“和”周期执行“等抽象调度方法的具体实现。
⑦ Executors是一个静态工厂类,提供了快速创建线程池的方法。
Java通过Executors工厂类提供了4中快捷创建线程池的方法。
方法名 | 功能简介 |
---|---|
newSingleThreadExecutor() | 创建只有一个线程的线程池 |
newFixedThreadPool(int nThreads) | 创建固定大小的线程池 |
newCachedThreadPool() | 创建一个不限制线程数量的线程池,任何提交的任务都立即执行,但空闲线程会得到及时回收 |
newScheduledThreadPool() | 创建一个可定期或延时执行任务的线程池 |
使用Executors工厂类创建线程池有以下潜在问题:
企业开发规范会要求使用标准的ThreadPoolExecutor构造工作线程池,其中会使用到其较重要的构造器如下:
public ThreadPoolExecutor(
int corePoolSize,//核心线程数,空闲也不会回收
int maximumPoolSize,//最大线程数
long keepAliveTime,TimeUnit unit,//线程最大空闲时长
BlockingQueue<Runnable> workQueue,//任务的排队队列
ThreadFactory threadFactory,//新线程的产生方式
RejectedExecutionHandler handler//拒绝策略
)
线程池执行器根据corePoolSize和maximumPoolSize来自动维护线程池的工作线程,当maximumPoolSize被设置为Integer.MAX_VALUE时,线程池可以接收任意数量的并发任务。corePoolSize和maximumPoolSize可以在运行过程中通过set方法动态更改。
使用线程池可以降低资源消耗,提高相应速度和线程的管理性。但是线程数配置不合理会适得其反。对于不同的任务类型将配置不同的线程数:
阻塞队列,用于展示接收异步任务,当工作线程多于corePoolSize时,就会将异步任务放到阻塞队列中
空闲线程存活时间,若超过这个时间,非核心线程会被回收
方式一:execute()
void execute(Runnable command);
方式二:submit()
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task,T result);
Future<?> submit(Runnable task);
两者区别如下:
ThreadFactory是Java线程工厂接口,其只包含一个newThread方法。
package java.util.concurrent;
public interface threadFactory{
Thread newThread(Runnable target);
}
BlockingQueue是JUC的接口,其有常见实现类:ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue(优先队列),DelayQueue(无界阻塞延迟队列),SynchronousQueue(同步队列)。
当线程池关闭或阻塞队列和maximumPoolSize已满时会执行任务拒绝策略。RejectedExecutionHandler是JUC中的拒绝策略接口,其有以下实现:
线程池一共有5种状态:
关闭线程池主要涉及三个方法:shutdown、shutdownNow、awaitTermination,一般的关闭流程如下:
为了保证线程对变量的安全访问,可以将变量放到ThreadLocal类型的对象中,使变量子啊每个线程都有独立值,不会出现一个线程读取变量时被另一个线程修改。
TheadLocal类也被称为线程局部变量类。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本,即实现“线程隔离”或“数据隔离”。
ThreadLocal能实现每个线程都拥有一份变量的本地值,其原理是每个线程都拥有自己独立的ThreadLocalMap空间,采用了空间换时间的方式实现“无锁编程”。
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
方法 | 说明 |
---|---|
set(T value) | 设置绑定本地值 |
T get() | 获取绑定的本地值 |
remove() | 移除绑定的本地值 |
内存泄露:不再用到的内存没有及时释放。使用ThreadLocal时要在使用完本地值后执行remove操作,将对应ThreadLocalMap中的Key置为null,以便GC回收。
在进入锁之前我们需要了解以下几个知识点:
线程同进程一样,也可以设置临界区资源,当线程申请资源时可以视作线程进入进入区代码段,当线程释放资源时可视作线程进入退出区代码段。
临界区代码段是每个线程中访问临界区资源的那段代码,多个线程必须互斥地对临界区资源进行访问。
线程的阻塞和唤醒操作需要进程在内核态【使用到原语,即操作系统底层操作】和用户态之间来回切换,这会导致性能降低。
线程同步使用较多的是synchronized,这是一个重量级锁。使用synchronized(Object)调用相当于获取Java对象的内置锁,所以使用sychronized的本质是利用Java对象的内置锁对临界区代码段进行排他性保护。
非静态同步方法如下,对实例方法进行synchronized同步,实际上是当前对象this的监视锁,依旧使用的是Java内置锁。
public synchronized void xxxMethod(){
xxx;//临界区代码段
}
静态同步方法如下,当sychronized修饰静态方法时,当类加载时还未创建相应对象,此时synchronized获取到的是该类对应的class对象的内置锁,我们称其为类锁。使用类锁会导致所有线程都要互斥的进入临界区代码段,粒度较粗。
public static synchronized void xxxMethod(){
xxx;//临界区代码段
}
使用synchronized修饰方法进行同步,实际上是将该对象的资源视作临界区资源,这会导致临界区资源限制等待,所以可以使用synchronized同步块减小临界区范围。
synchronized(Object){
xxx;//临界区代码段
}
Java对象结构包含三个部分:对象头、对象体和对齐字节。
我们重点看Mark Word,其中包含了GC标志位、分代年龄、哈希码、锁状态等信息。Java内置锁的信息就放在该字段中,其中锁状态分为:无锁、偏向锁、轻量级锁、重量级锁。JDK1.6以前Java内置锁是重量级锁,但是JDK1.6后将锁的状态分为4种,四种状态不可逆。
JVM中每个对象会有一个监视器,监视器会同对象一起创建和销毁。监视器的义务是保证同一时间只有一个线程可以访问被保护的临界区代码块。
Java对象刚创建时还没有任何线程来竞争,对象处于无锁状态。
偏向锁是指同一段同步代码一直被同一个线程所访问,该线程会自动获取锁,降低获取锁的代价。偏向锁状态的Mark Word会记录内置锁偏爱的线程的线程ID。
锁记录:存在线程的栈帧中,每一个线程都有一份自己的锁记录,锁记录存放的是内置锁的Mark Word内容,供释放锁时使用。
偏向锁的加锁过程:新线程判断内置锁对象的Mark Word种的线程ID是不是自己的ID,若不是或内置锁的线程ID为空,则需要使用CAS交换,将自己的线程ID交换到内置锁对象的Mark Word中。
偏向锁的撤销:当锁升级时需要撤销偏向锁,这需要停止拥有锁的线程,然后遍历线程的栈帧,检查是否存在锁记录进行撤销,并消除内置锁中的偏向线程ID,将锁升级为轻量级锁。
当锁处于偏向锁,又被另一个线程企图抢占时,锁会升级为轻量级锁,哪个线程先占有锁对象,锁对象就指向哪个线程栈帧的锁记录。企图抢占的线程会通过自旋尝试获取锁,该线程不会进入阻塞。
自旋:企图抢占锁的线程会进行不断尝试获取锁,当另一个线程释放锁后,即可获取该锁。这样线程就不需要进行内核态与用户态的切换。但是自旋操作消耗CPU,若处于自旋状态的线程过多,或处于自旋状态的时间过长也较为低效。JDK1.6后引入了适应性自旋锁,设定自旋最大时长,在该时间内线程自旋成功,则下次自旋的允许次数就会增多,若线程自旋失败,则下次会减少自旋次数。
原理:
重量级锁会让其他申请的线程进入阻塞,重量级锁也称为同步锁。
重量级锁的开销:轻量级锁使用CAS进行自旋抢锁,CAS操作是在用户态下的,但重量级锁对于进程的阻塞和唤醒需要使用到内核操作,即进程会在用户态和内核态之间切换,则重量级锁开销就较大。
① 线程枪锁时,JVM先检测内置锁对象Mark Word中的偏向锁标识是否为1,锁标志位是否位01,若满足则确认锁对象为可偏向状态。
② JVM检查Mark Word中的线程ID是否为抢锁线程ID,如果是就表示抢锁线程处于偏向锁状态。如果不是则会通过CAS操作竞争锁,竞争成功,则将内置锁的Mark Word中的线程ID设置为抢锁线程,即获取锁。
③ 若CAS操作失败,说明发生了竞争。就会撤销偏向锁,进而升级为轻量级锁。JVM使用CAS操作将锁对象的Mark Word替换为抢锁线程的记录指针,若成功则获取锁。若失败,JVM会使用CAS自旋替换抢锁线程的所记录指针,若自旋成功,则锁依旧处于轻量级锁状态。
④ 若JVM的CAS替换锁记录指针自旋失败,轻量级锁就升级为重量级锁,后面等待的线程不进行自旋,进入阻塞状态。
Java中的CAS操作是sun包中的Unsafe类的native方法,基于C++。其主要提供了三个“比较并交换”的原子方法,Compare And Set即再Set之前先Compare该值有没有变化,只有在没变的情况下才对其赋值。
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
CAS指令需要三个操作数,分别是:
内存位置(Java变量的内存地址V)
旧的期望值(A)
准备设置的新值(B)
当前仅当V符合A时,处理器才会用B更新V的值,否则就不执行更新。
CAS在Java.util.concurrent.atomic包中的原子类、JavaAQS以及显式锁中广泛使用,在JUC原子类中使用了CAS和volatile来实现操作的原子性。内置锁的升级,锁的获取和释放也是通过CAS来完成。
1.ABA问题
两个线程同时拿到对象V,V的值为A。一个线程将A改成了B,然后又改成了A。另一个对象想修改V,对其进行CAS操作,期望原值为A,然后确实是A,修改成功,但是实际上改对象发生了改变。
规避方式:为变量增加版本号,每次修改都会导致版本号的改变,CAS操作需要检查版本号的值变化。例如Java中的AtomicStampedReference和AtomicMarkableReference。
2.只能保证一个共享变量之间的原子性操作
当对一个共享变量执行操作时,我们可以使用循环CAS方式保证原子操作,但对多个共享变量时,CAS就无法保证原子性。
规避方式:将多个共享变量合并成一个共享变量,例如Java中的AtomicReference,将多个变量放到AtomicReference中再进行CAS操作。
3.开销问题
CAS自旋会给CPU带来较大的开销,可以采用队列,将发生CAS争用的线程放到一个队列中,降低CAS的争用激烈程度,例如JUC中的AQS。
Java内置锁使用简单,但功能单一,不具备限时抢锁、可中断抢锁、多个等待队列等高级锁功能。除此外重量级锁的线程状态切换开销较大,性能较低。于是需要使用到Java显示锁Lock。
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
/*
* @since 1.5
* @author Doug Lea//Java并发大佬
*/
public interface Lock {
void lock();//抢锁
void lockInterruptibly() throws InterruptedException;//可中断抢锁
boolean tryLock();//尝试抢锁,不会进入阻塞
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//限时抢锁
void unlock();//释放锁
Condition newCondition();//获取与显式锁绑定的Condition对象,用于“等待-通知”方式通信
}
从Lock接口可看出,Java显式锁具有可中断获取锁、可非阻塞获取锁、可限时抢锁。
ReentrantLock是JUC包提供的显式锁的一个基础实现类,ReentrantLock类实现了Lock接口,是一个可重入的独占锁。该锁与synchronized具有同样的并发性和内存语义,但在该基础上增加了限时抢占和可中断抢占等功能。
其部分源码如下
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
//abstract static class Sync extends AbstractQueuedSynchronizer
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isLocked();
}
}
Lock lock = new SomeLock();//SomeLock代指Lock的实现类
lock.lock();//抢占锁
try{
//执行临界区代码
}finally{
lock.unlock();//释放锁
}
释放锁操作一定在finally中执行,这时防止临界区发生异常导致锁无法释放。lock抢占锁操作需要在try之外,要先获取锁才能执行临界区代码,才需要进行锁的释放。
Lock lock = new SomeLock();
if(lock.tryLock()){
try{
//执行临界区代码
}finally{
lock.unlock();//释放锁
}
}
else{
//抢锁失败,执行后续操作
}
不常用,一般使用其重载方法tryLock(long time,TomeUnit unit)。
Lock lock = new SomeLock();
if(lock.tryLock(xxx,TimeUnit.SECONDS)){
try{
//执行临界区代码
}finally{
lock.unlock();//释放锁
}
}
else{
//抢锁失败,执行后续操作
}
该方法在抢不到锁时会阻塞一段时间,在阻塞期间获取倒锁则会返回true。
LockSupport是JUC提供的一个线程阻塞和唤醒的工具类,该工具类可以让线程在任意位置阻塞和唤醒,其所有方法都是静态方法。其方法主要分为两类:park和unpark。
LockSupport.park()与Thread.sleep()区别:
LockSupport.park()与Object.wait()区别:
可重入锁指一个线程可以多次抢占同一个锁。例如线程A在进入外层函数时抢占了一个Lock显示锁后,当线程A继续进入内层函数时,遇到有抢占同一个Lock显式锁的代码,线程A依然可以抢到该Lock显式锁。例如synchronized、ReentrantLock。
不可重入锁指一个线程只能抢占一次同一个锁,例如当线程A在进入外层函数时抢占了一个Lock显示锁后,当线程A继续进入内层函数时,遇到有抢占同一个Lock显式锁的代码。线程A不可抢占该锁,除非线程A释放之前占有的Lock显式锁。
悲观锁:每次进入临界区操作数据时都认为别的线程会进行修改,所以线程每次在读写数据时都会上锁。例如synchronized重量级锁。
乐观锁:每次拿数据时都认为别的线程不会进行修改,只有在提交数据更新时才会检查数据是否被修改(一般采用版本号的方式),才会进行加锁执行写操作。例如synchronized轻量级锁、JUC中基于抽象队列同步器实现的显式锁(如ReentrantLock)。
Java乐观锁都是通过CAS自旋操作实现,即比较当前值和传入值是否相同,是则更新,不是则失败。
公平锁:指不同的线程抢占锁的机会是公平的,抢锁成功次序为FIFO。例如默认情况下ReentrantLock是非公平锁,可设置为公平锁,synchronized也是非公平的。
非公平锁:指不同线程抢占锁的机会不同,抢锁次序不一定为FIFO。
可中断锁:这里是中断阻塞的意思,即若A线程正占有锁,B阻塞式抢占锁,B可以中断自己的阻塞继续执行。例如JUC显式锁。
不可中断锁:若线程在抢占锁失败进入阻塞时,不能中断阻塞,只能继续等待直到拿到锁。例如synchronized。
独占锁:一个锁一次只能由一个线程占有。例如ReentrantLock、synchronized
共享锁:允许多个线程同时获取锁。例如ReentrantReadWriteLock是一个共享锁实现类,读操作可以有很多线程一起读,但写操作只能有一个线程写。
通过ReentrantReadWriteLock类能获取读锁和写锁,它的读锁是可以多线程共享的共享锁,而它的写锁是排他锁。同一时刻不允许读锁和写锁同时被抢占,二者互斥,具体使用范例如下。
final static ReetrantReadWriteLock RWLock = new ReentrantReadWriteLock();
final static Lock readLock = RWLock.readLock();
final static Lock writeLock = RWLock.writeLock();
final static Map<String,String> map = new ReentrantReadWriteLock();
public static String put(String key,String value){
writeLock.lock();
try{
String put = map.put(key,value);
return put;
}finally{
writeLock.unLock();
}
return null;
}
public static String get(String key){
readLock.lock();
try{
String value = map.get(key);
return value;
}finally{
readLock.unLock();
}
return null;
}
CAS自旋会导致浪费大量CPU资源,可以使用队列削峰来解决。JUC并法包中的许多类比如ReentrantLock都是基于AQS构建的。AQS是一个双向队列的抽象基础类AbstractQueuedSynchronizer,称为抽象同步队列类,简称AQS。显式锁的获取与释放都基于AQS实现
AQS的核心组成有四个模块:状态标志位、队列节点类、FIFO双向同步队列、钩子方法
private volatile int state;
protected final int getState() return state;
protected final boolean compareAndSetState(int expect,int update){
return unsafe.compareAndSwapInt(this,stateOffset,expect,update);
}
以ReentrantLock为例,state初始化状态为0,表示未锁定。当A线程执行lock操作时,会调用tryAcquire()独占该锁并将state加1.此后,其他线程再tryAcquire()时就会失败,直到线程执行unlock时,将state减1为0,其他线程才能获取该锁。
Reentrant的可重入:若A线程在释放锁之前,重复获取该锁,state会进行累加。但获取多少次就得unlock多少次,才能释放锁。
AQS是一个虚拟队列,不存在队列实例,仅存在节点间的前后关系。
static final class Node {
//状态
static final int CANCELLED = 1;//取消状态
static final int SIGNAL = -1;//等待状态
static final int CONDITION = -2;//条件等待
static final int PROPAGATE = -3;//下一次共享锁的acquireShared无条件传播
volatile int waitStatus;//普通同步节点的初始状态为0,条件等待为-2
//节点连接
volatile Node prev;//前驱节点
volatile Node next;//后继节点
Node nextWaiter;//下一个条件等待节点【相当于另外的条件等待队列】
//成员变量
volatile Thread thread;//存放AQS队列中的线程引用
//获取前驱节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
}
AQS节点与等待线程相关联,每个节点维护一个状态watiStatus。
值为0:表示当前节点处于初始状态。
值为-1:表示其后继节点处于等待状态,若当前节点释放锁或被取消,会通知后继节点,使后继节点开始运行。
值为1:表示当前节点已释放(中断或等待超时),取消等待。
值为-2:表示该线程在条件队列中阻塞。
值为-3:表示当前锁为共享锁,该节点线程获取共享锁后通知后续的共享线程赶快来享用。
AQS通过内置的FIFO双向队列来完成线程的排队工作,内部通过节点head和tail记录队首和队尾元素,元素节点类型为Node类型。每当线程通过AQS获取锁失败时,线程将被封装成一个Node节点,通过CAS操作插入队列尾部。当有线程释放锁时,AQS会尝试让队头的后继节点占用锁。
private transient volatile Node head;
private transient volatile Node tail;
AQS定义了两种资源共享方式:独占锁和共享锁。其为不同的资源共享方式提供了不同的模板流程,自定义的同步器只需要实现共享资源state的获取和释放即可,这些逻辑都编写在钩子方法中,需要重写的钩子方法如下:
public class SimpleLock implements Lock{
private final Sync sync = new Sync();//同步器-自定义内部类
//基于AQS构建同步器Sync
private static class Sync extends AbstractQueuedSynchronizer{
//重写钩子方法
//尝试获取锁
protected boolean tryAcquire(int arg){
if(compareAndSetState(0,1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//尝试释放锁
protect boolean tryRelease(int arg){
if(Thread.currentThread()!=getExclusiveOwnerThread){
throw new IllegalMonitorStateException();
}
if(getState()==0){
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
//显示锁抢占方法
public void lock(){
sync.acquire(1);//AQS中的acquire会调用tryAcquire
}
//显示锁释放方法
public void unlock(){
sync.release(1);//AQS中的release会调用tryRelease
}
}
依上图,首先Lock实现类执行lock(),该方法会调用AQS的模板方法acquire(),该模板方法会调用我们实现的同步器Sync的tryAcquire()方法,如果tryAcquire()返回true,则表示获取锁成功。若失败,会将该线程加入等待队列。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();
}
我们可以看到acquire只使用了一行代码,但是其执行逻辑顺序却不简单。
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
这里补充Thread.interrupt()的相关使用:
若线程正在运行,使用interrupt()会将线程的中断标志设置为true,需要用户程序区监视线程的isInterrupted状态(),并进行相应处理
若线程被wait()、sleep()、join()方法阻塞,此时使用interrupt()会抛出中断异常,提前终结被阻塞状态
private Node addWaiter(Node mode) {
//新增节点,传入当前线程和节点类型参数
Node node = new Node(Thread.currentThread(), mode);
//加入队尾
Node pred = tail;
if (pred != null) {
node.prev = pred;
//通过CAS修改队尾节点为最新节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果尝试添加队尾失败,则进入自旋
enq(node);
return node;
}
若addWaiter第一次在尾部添加节点失败,则意味着有抢锁情况发生,需要进行自旋,自旋的方法为enq(node)。
在节点入队后,启动自旋抢锁操作acquireQueued():当前Node节点线程在死循环中不断获取同步状态,并且不断在前驱节点上自旋,只有当前驱节点是头节点时才能尝试获取锁。
处于自旋的节点对应的线程将会被挂起进入阻塞状态这样就不会执行无效的空循环,如果头节点获取了锁,那么该节点绑定的线程会终止acquireQueued()自旋,线程获得锁执行临界区代码,并唤醒后继节点,后继节点继续检查自己的前驱节点是否为头节点。
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);
}
}
依上图,当Lock实现类执行unLock()方法时,会先调用AQS的模板方法relaese()方法,该方法会调用我们Sync实例的tryRelease(int)方法,tryRelease(int)方法执行成功后会调用unparkSuccessor()来唤醒后继节点。
release()方法会先执行tryRelaes()方法,如果返回true,则说明释放锁成功。若头节点不位null,说明后面有后继节点在等待获取锁,这就需要唤醒后继节点,具体唤醒方法为unparkSuccessor。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protect boolean tryRelease(int arg){
if(Thread.currentThread()!=getExclusiveOwnerThread){
throw new IllegalMonitorStateException();
}
if(getState()==0){
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
tryRelease()的核心逻辑是将同步状态设置为0,即释放锁。tryAcquire()的核心逻辑是通过CAS将同步状态从0置换为1。
节点在获得锁时就已经出队,则AQS队列的头节点就是下一个获得锁的节点。我们只需要判断头节点是否为null,不为null则使用unparkSuccessor()将其唤醒。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
这里唤醒方法使用的是LockSupport支持类的unpark方法将其唤醒,唤醒后该节点继续执行acquireQueued()方法。
Condition是JUC用来替代传统Object的wait()/notify()线程间通信与协作机制的新组件。
Condition中的await和signal与Object中的wait()/notify()类似,前者通过Java的基础类库实现,而后者是JVM底层的native方法。
public interface Condition {
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
在AQS中有一个内部类ConditionObject是实现条件队列的关键,每个ConditionObject对象都维护一个单独的条件等待队列。
public class ConditionObject implements Condition, java.io.Serializable{
private transient Node firstWaiter;
private transient Node lastWaiter;
}
在一个显式锁上我们可以创建多个等待任务队列。Condition条件队列是单向的,AQS同步队列是双向的。一个AQS实例可以有多个条件队列,但只有一个同步队列。
private Lock lock = new ReentrantLock();
private Condition waitQueue1 = lock.newCondition();
private Condition waitQueue2 = lock.newCondition();
当线程调用await()时,说明当前线程的节点为当前AQS同步队列的头节点,正好处于锁状态。await()方法会将线程从AQS同步队列移动到Condition条件队列,释放锁并唤醒AQS同步队列中头节点的下一个节点。
线程在某个ConditionObject对象上调用siganl()后,Condition条件队列中的firstWaiter会被加入到AQS同步队列中,等待节点被唤醒。整体流程如下:
Atomic操作指不可中断的操作,多个线程一起执行Atomic类型操作时,一个操作一旦开始就不会被其他线程中断。JUC并发包中的原子类都存放在java.util.concurrent.atomic类路径下,可分为4类。
在多线程环境下,若涉及基本数据类型的并发操作,不建议采用synchronized重量级锁进行线程同步,建议优先使用基础原子类。
基础原子类基于CAS自旋+volatile的方案实现。
对于对象变量也可以通过JUC原子类实现其原子操作,可以使用原子类型原子类和属性更新原子类。
使用AtomicReference对对象引用进行原子性修改,首先需要对对象进行包装,然后再需要更新值的引用时采用CAS交换。
//person类
class Person{
int no;
public Person(int no){
this.no = no;
}
}
//包装
class Main{
public static void main(String args[]){
AtomicReference<Person> atomicPerson = new AtomicReference<>();
//包装对象person1
Person person1 = new Person(1);
//包装
atomicPerson.set(person);
//待替换对象person2
Person person1 = new Person(2);
//CAS替换
boolean success = atomicPerson.compareAndSet(person1,person2);
}
}
原子操作,即不可中断的一个或一系列操作。我们之前的内置锁、显示锁以及原子类都是解决原子性问题的方案。
一个线程对共享变量的修改,另一个线程立刻可见。
可见性问题原因:工作私有内存与主存同步延迟
使用Java提供的关键字volatile修改共享变量可解决可见性问题。
有序性指的是程序代码执行的先后顺序,编译器编译器和CPU处理器会根据自己的决策,对代码的执行顺序进行重新排序。优化指令的执行顺序,使语句执行顺序发生改变。
有序性问题原因:在多线程环境下(多核),由于执行语句重排序后,重排序的这一部分没有一起执行完,就切换到了其它线程。
如下伪代码,有线程A和线程B,其通过拥有共享变量flag,我们想让线程A输出"hello"后,线程B再输出"world",如果flag=true提前执行了,则可能导致"world"被先输出。
boolean flag = false;
//线程A执行
System.out.println("hello");
flag = true;
//线程B执行
while(!flag){
sleep();
}
System.out.println("world");
使用Java提供的关键字volatile修改共享变量也可以解决有序性问题。
使用volatile修饰的变量,在编译后增加一个以lock为前缀的操作,该lock前缀操作具体有三个功能:
——————————————————————
作者:未来村村长
参考:
[1]《Java高并发核心编程》尼恩
[2]《深入理解Java虚拟机》周志明
个人网站:www.76pl.com
可以收藏加关注下吗
——————————————————————