目录
***:newScheduledThreadPool和newSingleThreadPool创建的线程池,当池中线程运行抛出异常时,各自的执行状态?
***:为什么使用interrupt方法并不能如期中断执行呢?
***:wait()、notify()和notifyAll()为什么必须在同步方法或者同步代码块中执行?
***:synchronized和ReentrantLock的区别?
使用CyclicBarrier、CountDownLatch和Semaphore等满足特定场景的线程同步
***:CyclicBarrier和CountDownLatch的区别?
***:多线程并发操作,保证线程同步(ConcurrentHashMap)
***:编程题1--要求3线程并发,实现顺序输出abcabcabc......
前言
带着问题学java系列博文之java基础篇。从问题出发,学习java知识。
什么是线程?
在开始java多线程之前,我们要先了解一下基本概念:进程和线程。一个java程序可以直接运行,运行之后,在jvm中就新建了一个进程。可以这样理解:一个程序/服务就是一个进程,也只能是一个进程,一个服务无法建立多个进程。然而程序或者服务往往逻辑没有那么简单,会存在很多并发任务,需要同时处理多个事情,因此就需要启动多个线程,一个线程单独处理某个任务。总结:一个程序就是一个进程,一个程序可以启动多个线程;一个进程必定含有最少一个线程(主线程,main方法入口)。
***:进程和线程对比?
进程:一个在内存中运行的应用程序,有自己独有的一块内存空间;
线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。
- 进程是操作系统进行资源分配的最小单位;线程是任务处理器进行任务调度和执行的最小单位
- 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
- 一个进程至少包含一个线程
- 同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
- 一个进程的崩溃不会影响其他进程;但是一个线程的崩溃,往往会引发整个进程的执行异常,导致进程崩溃
- 进程可以独立运行,而线程的运行必须依赖进程,并受进程的管理控制
并发知识库
从上面进程和线程的对比,我们知道多线程是可以共享所在进程的地址空间和资源。当多线程并发操作共享内存时,如果不加以控制,则会引发各种异常现象。为了避免多线程并发执行时引发的异常,需要用到并发知识,来控制并发操作。下图就是java中并发知识库
认识Thread类
java对线程做了封装,有一个线程类Thread:
内部类主要有:
属性主要有:
private volatile String name: 线程名称;
private int priority:线程的优先级;
private Runnable target:线程的目标任务;
主要方法有:
Thread():无参构造方法;
Thread(Runnable target):一个接口参数的构造方法;
Thread(String name)和 Thread(Runnable target,String name);
start():启动线程执行任务;
interrupt():中断线程执行;
join():等待线程执行完毕;
join(long millis):等待线程执行完毕,最多等待mills毫秒,超时不再等待;
isAlive():线程是否存活,存活返回true,否则返回false;
isInterrupted():线程是否被中断,中断返回true,否则返回false;
isDaemon():线程是否是守护线程,是返回true,否则返回false;
setUncaughtExceptionHandler(UncaughtExceptionHandler eh):设置当前线程的异常捕获执行handler,当前线程执行过程中如果抛出未捕获的异常,则调用该handler方法;
static currentThread():返回当前正在执行的线程对象的引用;
static interrupted():当前线程是否被中断,中断返回true,否则返回false;
static sleep(long millis):暂停当前线程millis毫秒,倒计时结束继续执行;
static yield():当前线程让步,释放cpu,重新进入竞争cpu状态;
static setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh):设置Thread类默认的异常捕获执行handler,当所有的Thread对象执行时抛出未捕获的异常时,调用该handler;
创建线程的几种方式
- 调用Thread的构造方法创建线程(注意重写run方法,实现具体的线程逻辑)
new Thread(){
@Override
public void run() {
//todo 实现具体逻辑
}
};
- 调用Thread的带参构造方法创建线程——Runnable参数
new Thread(new Runnable() {
@Override
public void run() {
//todo 实现具体逻辑
}
});
这种方式通过传参匿名内部类Runnable,设置了Thread的成员变量target,thread对象执行时将直接执行target的具体任务;
- 调用Thread的带参构造方法创建线程——FutureTask参数(Runnable)
new Thread(new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
//todo 实现具体逻辑
return null;
}
}));
这种方式通过传参匿名内部类FutureTask(FutureTask实现了Runnable接口);使用FutureTask有个好处是,可以通过其成员方法cancel()来终止线程执行,也可以通过get()方法阻塞等待task执行返回结果。
- 线程池方式创建线程(4种线程池)
jdk提供了一个工厂类Executors,有一系列静态方法,帮我们直接创建合适的线程池;
Executors.newCachedThreadPool():创建一个可根据任务数量自动扩容或者缩容的线程池;当任务数大于线程池中线程数,则创建新的线程,扩容线程池;当池中存在线程空闲超60s,则自动回收该线程,缩容线程池;
Executors.newFixedThreadPool(int nThreads):创建指定线程数的可重用线程池,默认任务队列是共享无界队列,当任务数超过线程数时,任务进入队列中等待执行;
Executors.newScheduledThreadPool(int corePoolSize):创建指定核心线程数量的可重用线程池,特点是可周期性执行任务;
Executors.newSingleThreadExecutor():创建单线程的线程池,池中仅有一个线程;
***:newScheduledThreadPool和newSingleThreadPool创建的线程池,当池中线程运行抛出异常时,各自的执行状态?
newScheduledThreadPool创建的线程池可以周期性执行任务,但是当某一次执行任务时抛出了异常,则该周期性执行的所有后续任务都不再执行(解决办法:使用try-catch包裹代码块,捕获异常);newSingleThreadPool创建的线程池仅有一个线程,这个线程池可以在线程死后或者发生异常时重新启动一个线程来代替原来的线程执行下去。
终止线程的几种方式
- 执行完毕,正常结束
Thread或者Runnable的run方法执行完毕,或者Callable的call方法执行完毕,则线程自动进入完成-死亡状态,生命周期结束,等待系统回收;
- 使用标志控制线程终止
boolean flag = true;
new Thread(new Runnable() {
@Override
public void run() {
while (flag){
//todo 实现具体逻辑
}
}
}).start();
如范例,通过boolean变量flag来控制线程的终止,当我们想终止循环执行任务的线程时,只需要将flag置为false,则run方法执行完毕即可终止线程。
- 调用线程的成员方法interrupt()中断执行
boolean flag = true;
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (flag) {
System.out.println("run");
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
try {
Thread.sleep(3500);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
如上代码,虽然执行了interrupt()方法试图中断执行。但是其实线程并没有被终止,而是在3.5s后,抛出一个InterruptedException异常,但由于该异常被线程自己捕获处理了,因此while循环还是继续,线程依然在运行。
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println(System.currentTimeMillis());
//Thread.sleep(1 * 1000);
}
}
});
thread.start();
try {
Thread.sleep(500);
thread.interrupt();
System.out.println(thread.isInterrupted());
} catch (InterruptedException e) {
e.printStackTrace();
}
如上代码,在500ms后由于主线程执行了interrupt()方法,线程也如期望的一样,中断了执行。
***:为什么使用interrupt方法并不能如期中断执行呢?
说到这个问题,我们先得看一下interrupt方法到底做了啥。贴上源码:
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
public static native void sleep(long millis) throws InterruptedException;
private native void interrupt0();
从源码可以看到,interrupt方法在检查完权限之后,就会首先尝试获取blockerLock对象锁,获取到锁之后,修改一下interrupt标志位。当线程由于执行sleep方法进入阻塞状态后,此时调用interrupt方法尝试获取对象锁,会导致sleep方法抛出InterruptedException异常(注意此时interrupt方法还是无法获取到对象锁,线程的interrupt标志位并没有被改变)。所以上面的范例代码1,在3.5s时由于主线程执行了子线程的interrupt方法,导致正在阻塞状态的Thread.sleep方法抛出了异常,子线程在捕获异常后继续执行后续的循环。所以结果如下图:
而范例代码2,由于run方法体并没有Thread.sleep,子线程也从未进入过阻塞状态,所以当0.5s主线程执行了子线程的interrupt方法,interrupt方法可以获取到对象锁,进入sync代码块,修改了子线程的interrupt的标志位为true。while循环的条件是! isInterrupted(),所以循环被终止,run方法体执行完毕,线程进入死亡状态。结果如下图:
综上:总结出interrupt方法本质是尝试获取对象锁,获取锁之后改变线程的中断标志。要想通过线程的interrupt()方法直接中断线程的执行,必须满足几个条件:
1.执行interrupt()方法时,线程不是阻塞状态;否则将会导致抛出InterruptedException异常,如果在run方法中捕获了异常,则线程不会被中断,处理完异常后继续执行;
2.执行interrupt()方法时,线程不是阻塞状态,并且循环条件必须是 Thread.currentThread().isInterrupted()。这样才可以在interrupt方法执行修改完线程的中断标志为true,线程退出循环,执行终止。
- 执行stop方法终止
首先注意Thread的stop方法已被弃用,不再建议使用此方法。此方法的执行会强制中断线程,类似于电脑强制断电关机。这个方法不能很好的处理线程状态,无法保证线程安全。
- 执行cancel方法取消任务
通过FutureTask或者Callable方法创建的线程,我们可以直接调用FutureTask或者Future的cancel方法,来取消任务,终止线程。
***:为什么cancel方法失效?
首先我们要理解cancel(true)方法的本质其实是和Thread的interrupt方法完全相同的,当线程阻塞时,也是获取不到对象锁,从而可能抛出InterruptedException异常;当线程不阻塞时,则是改变线程中断标志位interrupt。区别于interrupt方法,cancel(true)当线程阻塞时,不管线程是否捕获了异常,只要抛出InterruptedException异常,都会自动终止线程的执行,而interrupt方法在异常被线程捕获处理后就不会中断执行。需要注意的是cancel方法有个参数boolean mayInterruptIfRunning,当该参数设置为false时,即使线程阻塞,也不会中断线程。另外,如果线程不是阻塞状态,要想cancel(true)可以取消任务,终止线程的运行,则也必须和interrupt方法一样,设置循环条件为Thread.currentThread().isInterrupted()。以上都是在指定线程运行时,才有效,如果使用线程池执行任务,则cancel设置中断标志无效,因为线程池中的线程可以重复利用,cancel无法设置线程池中的线程interrupt中断标志。
线程的状态和生命周期
如上图,线程从创建启动到执行完毕死亡,整个生命周期共有五大状态:
- 初始状态
当程序使用new关键字创建一个线程后,该线程就处于初始状态。此时由jvm为其分配内存,初始化成员变量;
- 就绪状态(可运行状态)
当线程对象调用了start()方法后,该线程处于就绪状态。jvm会为其创建方法调用栈和程序计数器,等待调度运行;
- 运行状态
如果处于就绪状态的线程获得了cpu时间片,开始执行run()方法的线程执行体,则该线程处于运行状态;
- 阻塞状态
阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
- 死亡状态
线程会以三种方法结束,结束之后就是死亡状态。
- run()或者call()方法执行完毕,线程正常结束
- 线程执行过程抛出一个未捕获的Exception或Error,中断执行
- 调用该线程对象的stop()方法,强制结束线程(注意该方法容易引发死锁,已被新版jdk弃用)
***:sleep()和wait()的区别?
- sleep()方法是Thread类的静态方法,wait()方法是Object的成员方法;
- sleep()方法会导致当前线程暂停执行,让出cpu;同时监控状态依然保持,获取的锁也不会释放,倒计时结束会自动恢复运行状态;
- wait()方法会导致线程放弃所获得的对象锁,进入等待此对象锁的等待锁池;只有当针对此对象调用了notify()或者notifyAll()方法后,线程才进入竞争此对象锁的锁池队列,和其他线程一起竞争对象锁。
***:wait()、notify()和notifyAll()为什么必须在同步方法或者同步代码块中执行?
wait()、notify()和notifyAll()方法都是针对多线程场景下,解决线程同步问题而设计的。三个方法都是对该对象的对象锁进行系列操作,wait()方法的调用会导致调用线程会释放自身获取到的对象锁;notify()方法的调用会随机唤醒该对象锁等待锁池中的某一个线程,被唤醒的线程从等待锁池进入竞争锁池,与其他线程一起竞争对象锁;notifyAll()方法的调用会唤醒该对象锁等待锁池中的所有线程,全部进入竞争锁池,大家一起竞争对象锁。很明显,三个方法的调用前提都是调用线程自身已经获取到对象锁,再通过这三个方法来控制并发和线程同步。所以这三个方法必须在synchronized修饰的同步方法或者同步代码块中执行,当进入到同步方法或者同步代码块时,意味着调用线程已经获取了对象锁;否则将会抛出IllegalMonitorStateException异常。
线程池
说到线程池,大家首先想到的应该是jdk给我们提供的工具类Executors,Excutors工具类给我们提供了一系列静态方法,可以非常方便的创建各种线程池,供我们使用,其中主要是这四类线程池:
Executors.newCachedThreadPool():创建一个可根据任务数量自动扩容或者缩容的线程池;当任务数大于线程池中线程数,则创建新的线程,扩容线程池;当池中存在线程空闲超60s,则自动回收该线程,缩容线程池;
Executors.newFixedThreadPool(int nThreads):创建指定线程数的可重用线程池,默认任务队列是共享无界队列,当任务数超过线程数时,任务进入队列中等待执行;
Executors.newScheduledThreadPool(int corePoolSize):创建指定核心线程数量的可重用线程池,特点是可周期性执行任务;
Executors.newSingleThreadExecutor():创建单线程的线程池,池中仅有一个线程;
我们直接使用工具类静态方法创建线程池使用的时候,有没有考虑过类似问题:核心线程数该设置多少?提交的任务过多,超出池中线程数时,这些任务存放在哪里?如果一直提交很多任务会导致内存溢出吗?来不及执行的任务会被丢弃吗,它们有机会得到线程执行吗,是以什么规则得到线程执行呢?要解答以上问题,需要我们好好了解下线程池,不能仅仅停留在一堆可复用线程的集合上面。
线程池的状态
- running:接收新任务,处理已添加的任务;(线程池创建后自动进入此状态)
- shutdown:不再接收新任务,继续处理已添加的任务;(线程池调用shutdown(),由running转变为shutdown)
- stop: 不再接收新任务,也停止处理任务;(线程池调用stop(),转变为stop)
- tidying:池中所有任务已终止(shutdown状态下处理完所有已添加任务,stop状态下所有任务被终止),此时会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
- terminated:线程池完全终止状态,执行完terminated()后进入此状态。
线程池的创建
跟踪Executors的源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
从上面源码可以看到,原来Executors的静态方法创建线程池,也无非就是使用new关键字,通过设置不同的参数来创建不同的线程池,虽然是四类线程池,但是底层其实就两种:ThreadPoolExecutor和ScheduledThreadPoolExecutor。再继续看下这两个对象的构造方法各个参数的含义:
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
如上源码ThreadPoolExecutor构造方法有7个参数:
- int corePoolSize:指定核心线程数
- int maximumPoolSize:指定池中允许的最大线程数
- long keepAliveTime:指定当池中超出核心线程数的线程空闲时,该线程的存活时长,配合第4个参数联合设定
- TimeUnit unit:指定存活时长的单位,配合第3个参数联合作用
- BlockingQueue<Runnable> workQueue:当提交的任务超出池中线程时,存放任务的缓冲队列(阻塞队列)
- ThreadFactory threadFactory:指定创建线程的线程工厂对象
- RejectedExecutionHandler handler:指定任务拒绝策略
/**
* Creates a new ScheduledThreadPoolExecutor with the given
* initial parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if {@code corePoolSize < 0}
* @throws NullPointerException if {@code threadFactory} or
* {@code handler} is null
*/
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
如上源码ScheduledThreadPoolExecutor构造方法有3个参数:
- int corePoolSize:指定核心线程数
- ThreadFactory threadFactory:指定创建线程的线程工厂对象
- RejectedExecutionHandler handler:指定任务拒绝策略
了解了Executors静态方法的实际实现,以及ThreadPoolExecutor和ScheduledThreadPoolExecutor两大线程池对象的构造函数,在项目中创建适合需求的线程池才是最优的实现方案。我们自己创建线程池,根据具体需求和对任务的处理方式,指定核心线程数的大小,最大线程数量,任务队列,任务拒绝策略等等。
添加任务到线程池时的几种情形
基于ThreadPoolExecutor通用线程池分析:
- 池中线程数小于corePoolSize,即使池中有线程处于空闲状态,也会创建新的线程执行本次添加的任务;
- 池中线程数等于corePoolSize,缓存队列未满,本次添加的任务放入缓存队列;
- 池中线程数大于corePoolSize小于maximumPoolSize,并且缓存队列满,创建新的线程执行本次添加的任务;
- 如果池中线程数大于corePoolSize,等于maximumPoolSize,并且缓存队列满,本次添加的任务将被拒绝(根据配置的拒绝策略执行拒绝)
****小贴士:池中线程数大于corePoolSize时,如果某个线程空闲时间超过keepAliveTime,则该线程将被终止。
四种任务拒绝策略
- AbortPolicy: 抛出java.util.concurrent.RejectedExecutionException异常
- CallerRunsPolicy: 用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务
- DiscardOldestPolicy: 丢弃被拒绝的任务(优先丢弃老旧的任务)
- DiscardPolicy: 丢弃被拒绝的任务
通过上面的系列分析和理解,建议在项目中要用到线程池时,根据实际需求合理估算线程池相关参数,自己创建线程池,而不是简单地直接使用Executors静态方法创建一个。这里也提供一个自己编写的线程池工具类,支持各种参数自定义。
public class GlobalThreadPool {
private volatile static GlobalThreadPool instanceHolder;
private GlobalThreadPool(){}
public static GlobalThreadPool getInstance(){
if (instanceHolder == null){
synchronized (GlobalThreadPool.class){
if (instanceHolder == null){
instanceHolder = new GlobalThreadPool();
}
}
}
return instanceHolder;
}
//核心线程数量
private static final int CORE_POOL_SIZE = 10;
//最大线程数量
private static final int MAX_POOL_SIZE = 20;
//空闲线程最长存活时长
private static final Long KEEP_ALIVE_TIME = 60l;
private static class MyThreadFactory implements ThreadFactory{
AtomicInteger num;
String threadNamePrefix;
public MyThreadFactory(String threadNamePrefix) {
this.threadNamePrefix = threadNamePrefix;
this.num = new AtomicInteger(0);
}
@Override
public Thread newThread(Runnable r) {
return new Thread(r,threadNamePrefix+num.incrementAndGet());
}
}
public static ExecutorService createThreadPool(String threadPrefix,int core,int max,long timeOut){
if (core > 0 && max > core && timeOut > 0) {
return new ThreadPoolExecutor(core, max, timeOut, TimeUnit.SECONDS,
new LinkedBlockingQueue<>((max - core))
, new MyThreadFactory(threadPrefix)
, new ThreadPoolExecutor.CallerRunsPolicy());
} else {
return new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_TIME,TimeUnit.SECONDS
,new LinkedBlockingQueue<>(10)
,new MyThreadFactory(threadPrefix)
,new ThreadPoolExecutor.CallerRunsPolicy());
}
}
public static ScheduledExecutorService createScheduledThreadPool(String threadPrefix,int core){
if (core > 0){
return new ScheduledThreadPoolExecutor(core,new MyThreadFactory(threadPrefix),
new ThreadPoolExecutor.CallerRunsPolicy());
} else {
return new ScheduledThreadPoolExecutor(CORE_POOL_SIZE,new MyThreadFactory(threadPrefix),
new ThreadPoolExecutor.CallerRunsPolicy());
}
}
}
线程同步
当我们创建多线程并发执行任务,如果存在并发操作共享内存,此时就会发生线程同步问题。拿经典的银行存取款举例,起两个线程,并发操作银行存款,线程a不停的存款,线程b不停的取款。此时如果不加以线程同步控制,其结果往往不符合我们的预期,最后账户中的存款数额也很大可能不正确。因此,说到多线程并发,我们不得不了解一下线程同步知识。
synchronized关键字实现线程同步
public class Test {
private static int account = 300;
public static synchronized boolean deposit(int money){
account += money;
System.out.println("存入"+money+"元,账户余额:"+account);
return true;
}
public static synchronized boolean withdrawal(int money){
if (account >= money) {
account -= money;
System.out.println("取出"+money+"元,账户余额:"+account);
return true;
} else {
System.out.println("余额不足,取款失败");
return false;
}
}
static class Deposit implements Runnable{
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
deposit(100);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Withdrawal implements Runnable{
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
withdrawal(200);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new Thread(new Deposit()).start();
new Thread(new Withdrawal()).start();
}
}
上面的程序是模拟并发银行存取款,账户初始金额300元,线程1每隔50ms向账户中存入100元,线程2每隔50ms从账户中取出200元。由于在存款和取款方法上都添加了synchronized关键字控制线程同步,所以执行结果完全符合预期:
如果想看看不做线程同步控制的情况,不妨去掉synchronized关键字,会发现打印的余额完全对不上。
我们看看synchronized关键词到底做了啥,为什么添加之后就可以做到线程同步了?
synchronized关键字是java用来对多线程场景进行线程同步控制的,是一种同步锁。synchronized可以作用于方法、代码块,当synchronized作用于方法时,如果方法是成员方法,则关键字就是对当前调用方法的对象进行加锁,进入方法意味着调用线程获取到了该对象的锁,方法执行完毕自动释放对象锁;如果方法是静态方法(类方法),则关键字就是对当前类对象进行加锁,进入方法意味着调用线程获取到了该类锁,方法执行完毕自动释放类锁。当synchronized作用于代码块时,此时需要显式指定加锁的对象,在括号中显式指定对象(synchronized(Object)),进入代码块意味着调用线程获取到了该对象的锁,代码块执行完毕自动释放对象锁。
所以上例是synchronized直接作用于类方法,每次执行deposit()或者withdrawal()方法时,都要先获取类锁,然后才能具体执行存款或者取款逻辑。由于同一时间仅允许一个线程获得锁,所以存款线程和取款线程同一时刻仅允许运行一个,这样也就保证了同一时刻仅有一个线程在修改accout,保证了线程同步。
ReentrantLock可重入锁实现线程同步
public class Test {
private static int account = 300;
private static ReentrantLock lock = new ReentrantLock();
public static boolean deposit(int money){
lock.lock();
account += money;
System.out.println("存入"+money+"元,账户余额:"+account);
lock.unlock();
return true;
}
public static boolean withdrawal(int money){
boolean result = false;
lock.lock();
if (account >= money) {
account -= money;
System.out.println("取出"+money+"元,账户余额:"+account);
result = true;
} else {
System.out.println("余额不足,取款失败");
}
lock.unlock();
return result;
}
static class Deposit implements Runnable{
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
deposit(100);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Withdrawal implements Runnable{
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
withdrawal(200);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new Thread(new Deposit()).start();
new Thread(new Withdrawal()).start();
}
}
上例和synchronized范例类似,只是去掉了存款和取款方法的synchronized关键字,创建了一个ReentrantLock可重入锁对象,在存款和取款方法中,执行前都加上lock.lock()获取重入锁,然后执行具体逻辑,执行完毕,再调用lock.unlock()释放重入锁。使用ReentrantLock其实和synchronized非常相似,都是先获取对象锁,然后执行逻辑,执行完毕再释放锁。
***:synchronized和ReentrantLock的区别?
两者都是加锁式同步,会阻塞线程,都是可重入锁;不同的是synchronized是java关键字,而ReentrantLock是jdk1.5之后提供的互斥锁对象;
synchronized作用域很广泛,可以作用于方法和代码块,而ReentrantLock仅可以用于一段代码前后;
synchronized会自动释放锁,使用更加便捷,而ReentrantLock需要手动调用unlock()释放,且如果忘记释放,会导致死锁(所以一般都是使用try-catch包裹代码块,在finally中释放锁,确保unlock()一定会被执行,避免死锁);
synchronized是非公平锁,已经获得过锁的线程,在下一次竞争锁的过程中,和已经等待过多次的线程获取锁的概率是相同;ReentrantLock默认初始化也是非公平锁,但是可以通过设置参数 boolean fair为true,来创建公平锁,公平锁就是已经获得过锁的线程,在下一次竞争锁的过程中,它排在已经等待过多次的线程后面,排在前面的线程优先获得锁(注意公平锁的效率低于非公平锁)。
***:什么是公平锁,什么是非公平锁?
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再次进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必去唤醒所有线程,会减少唤起线程的数量。
- 缺点:可能导致队列中间某个线程一直获取不到锁或者长时间获取不到锁,导致饿死。
使用CyclicBarrier、CountDownLatch和Semaphore等满足特定场景的线程同步
- 使用CycicBarrier实现:等待一组任务全部执行完毕之后,再继续执行后续任务的场景
/**
* CyclicBarrier:循环栏珊
* 可重复使用的屏障,拦截子任务,所有子任务都完成之后,由最后达到的线程执行最终任务,然后都放行;
* 比如组队去餐厅用餐,大家到达的时间不一致,餐厅要求全员到齐后由最后一人签到才可以进场,这样先到的就都得等待,全员到齐后,最后一位到达签名然后大家一起进场。
* 适用场景:多线程执行组合运算任务,利用栏珊,保证所有线程执行完毕,最终任务中统一处理结果。
*/
public class TestCyclicBarrier {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"大家吃完了,按照约定由最后到达的人付款");
}
});
static class Task implements Runnable{
CyclicBarrier cyclicBarrier;
public Task(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+"到达了餐厅");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName()+"离开了餐厅");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new Task(cyclicBarrier)).start();
}
}
}
上面的范例是模拟3个人约好去餐厅吃饭,吃完由最后到达的人付款。使用了cyclicbarrier循环栏珊,先到达的调用cyclicbarrier.await()然后当前线程就阻塞等待放行;每调用一次,cyclicbarrier就进行一次自减计数(初始化时设置的参数parties自减),当计数为0时,则由最后一个调用await()的线程执行设定的最后任务(初始化时设置的参数barrierAction);等最后任务执行完毕,cyclicbarrier重置计数parties为初始值,并且全部放行所有await()的线程,因此cyclicbarrier是可以循环使用的。
上例执行结果如下:
- 使用CountDownLatch实现:一个线程或者多个线程等待一组任务执行完毕,再执行自身任务
public class TestCountDownLatch {
private static CountDownLatch taskCount = new CountDownLatch(10);
static class Task implements Runnable{
private String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(3)*1000);
System.out.println(name+"任务执行完毕");
taskCount.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
service.submit(new Task("线程"+i));
}
System.out.println("主线程等待所有任务执行完毕,最多等待30s,超时不再等待");
try {
taskCount.await(30, TimeUnit.SECONDS);
System.out.println("所有任务执行完毕,继续执行主线程");
System.exit(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上例实现主线程等待10个子线程全部执行完毕(最多等待30s,超时不再等待),再执行自身任务的情景。使用了CountDownLatch程序计数器来实现,主线程调用await()方法(可设置最长阻塞等待的时长,不设置则默认一直等待),等待所有子线程执行完成;子线程完成自身任务后,则调用countDown()方法,将程序计数器计数减1;当程序计数器减至0时(初始化时设定初值),意味着所有子任务全部执行完毕,所以放行阻塞等待的主线程。
上例执行结果如下:
***:CyclicBarrier和CountDownLatch的区别?
两者适用场景很类似,都是等待一组任务执行完毕再执行后续任务,只是在一些细节方面有些区别。
- CyclicBarrier是可以循环使用的,在一轮计数结束会重置计数为初始值;CountDownLatch是一次性的程序计数器,自减为0后不会重置为初始值,所以只能使用一次;
- CyclicBarrier的最终任务只能是由最后到达栏珊的线程去执行;CountDownLatch则是支持谁调用await(),则谁阻塞等待,最终任务也是由它执行。
- 使用Semaphore实现:控制并发数量(资源有限,同一时刻仅允许有限个线程享有资源,执行任务)
public class TestSemaphore {
static Semaphore semaphore = new Semaphore(3);
static class task extends Thread{
public task(String name) {
this.setName(name);
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(this.getName()+"获取了信号量");
Thread.sleep(new Random().nextInt(3)*1000);
System.out.println(this.getName()+"执行完毕,释放信号量");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
new task("线程"+i).start();
}
}
}
上例实现并发启动6个线程,但是受资源限制,同一时刻仅允许3个线程并发执行,其他线程必须等已获得资源的线程释放了资源才有机会得到空闲资源然后执行自身任务。使用并发信号量Semaphore控制并发数,子线程执行前,必须先调用acquire(),阻塞竞争信号量,如果竞争到了则可以执行自身任务,同时信号量自身减1(初始化时设定初值),执行完毕调用release()释放信号量,同时信号量加1。本例中semaphore初始化时设定的初值为3,所以限制同一时刻允许并发3个线程,其他线程阻塞等待。
上例执行结果如下:
使用线程安全的容器保证线程同步
***:实现消费者-生产者模式范例(阻塞队列)
public class Test {
private static BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);
private static class Consumer implements Runnable{
private boolean flag = true;
public void stop(){
flag = false;
}
@Override
public void run() {
while (flag){
try {
Thread.sleep(100);
String message = queue.take();
System.out.println("消费:"+message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private static class Producer implements Runnable{
private boolean flag = true;
public void stop(){
flag = false;
}
@Override
public void run() {
while (flag){
try {
String message = new Random().nextInt(10)+"元";
queue.put(message);
System.out.println("生产:"+message);
Object[] datas = queue.toArray();
StringBuilder str = new StringBuilder();
for (Object data : datas) {
str.append(data+",");
}
System.out.println("当前队列中有:"+str.toString());
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new Thread(new Producer()).start();
new Thread(new Consumer()).start();
}
}
上面范例,生产者每隔20ms生成一条消息,存入队列中;消费者每隔100ms从队列中取出一条消息消费;使用有界阻塞队列作为共享存储区。范例运行结果如下:
一开始队列为空,生产者可以不断生产消息,存入队列;由于生产者生产速度远高于消费者速度,所以很快队列就被装满了,之后生产者就被阻塞,等到队列有空间才继续生产,也就出现了第二张图现象(消费者消费一条,生产者马上就生成一条填满队列,然后又被阻塞)。
***:多线程并发操作,保证线程同步(ConcurrentHashMap)
public class Test {
private static ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>(11);
private static class Task implements Runnable{
private boolean flag = true;
public void stop(){
flag = false;
}
@Override
public void run() {
while (flag){
try {
Thread.sleep(new Random().nextInt(3)*100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String key = new Random().nextInt(10)+"";
String value = new Random().nextInt(10)+"";
map.put(key,value);
System.out.println("插入键值对:"+key+"-"+value);
System.out.println(map.toString());
}
}
}
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
for (int i = 0; i < 4; i++) {
service.submit(new Task());
}
}
}
上面范例并发执行4个线程,间隔随机时长后向同一个hashmap存储随机键值对;hashmap有个特性,put内容的时候如果key相同,则覆盖原来的键值对;如果无相同key,则新增。本例是4个线程并发,除了并发插入操作需要线程安全之外,还需要检测键值对,防止同时插入相同key引发的线程安全问题。所以借助concurrenthashmap来作为容器,在《Java基础篇-容器》中有分析,concurrenthashmap是对桶数组进行分段式加锁,在提高并发效率的同时,又保证了线程同步。
执行结果如下图:
实战多线程同步编程题(持续更新中)
***:编程题1--要求3线程并发,实现顺序输出abcabcabc......
看题面,重点在保证顺序执行上面。这里我们一定要清楚,加锁是为了控制并发,但控制并发不等于保证顺序执行。所以千万不要陷入加锁的误区里面,本题仅从加锁并不好实现。
方案一:
public class Test {
private static volatile int cyclic = 1;
static class TaskA implements Runnable{
@Override
public void run() {
while (true){
if (cyclic == 1) {
System.out.print("a");
cyclic = 2;
}
}
}
}
static class TaskB implements Runnable{
@Override
public void run() {
while (true){
if (cyclic == 2) {
System.out.print("b");
cyclic = 3;
}
}
}
}
static class TaskC implements Runnable{
@Override
public void run() {
while (true){
if (cyclic == 3) {
System.out.print("c");
cyclic = 1;
}
}
}
}
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(3);
service.submit(new TaskA());
service.submit(new TaskB());
service.submit(new TaskC());
}
}
如分析,我们的主要任务是保障顺序执行,所以跳出加锁的误区;使用一个共享内存变量cyclic,volatile修饰符保障了该变量的可见性和有序性(volatile修饰符知识详见《Java基础篇--修饰符》),通过控制共享变量的值,来控制下一步轮到哪个线程执行。如上方案一,我们初始设定共享变量cyclic为1,此时TaskA的if判断条件为true,进入代码块执行taskA的具体任务,A执行完成,修改cyclic变量值为2;然后taskB的if判断条件为true,进入到B的代码块执行B任务,B执行完成,修改cylic变量值为3;然后taskC的if判断条件为true,进入到C的代码块执行C任务,C执行完成,修改cyclic变量值为1.然后又轮到A执行,依次下去。
方案优缺点分析:
该方案思路清晰,实现简单,且扩展或者减少顺序执行的环节也很简单,只需要修改一下cyclic的链路即可。比如像增加一个,顺序输出abcdabcd,只需要修改一个TaskC的cyclic的赋值为4,在TaskD里面判断条件是cyclic == 4,执行完毕赋值cyclic为1.
缺点就是,该方案不限制线程的并发执行,程序起了多少个线程,就有多少个线程一直在运行,持续消耗cpu资源。不过这个方案只是一直读取cyclic,然后判断一下,对服务器资源消耗较小。
方案二:
public class Test {
private static volatile int cyclic = 1;
private static ReentrantLock lock = new ReentrantLock(true);
static class TaskA implements Runnable{
@Override
public void run() {
while (true){
try {
lock.lock();
if (cyclic == 1) {
System.out.print("a");
cyclic = 2;
}
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
static class TaskB implements Runnable{
@Override
public void run() {
while (true){
try {
lock.lock();
if (cyclic == 2) {
System.out.print("b");
cyclic = 3;
}
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
static class TaskC implements Runnable{
@Override
public void run() {
while (true){
try {
lock.lock();
if (cyclic == 3) {
System.out.print("c");
cyclic = 1;
}
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(3);
service.submit(new TaskA());
service.submit(new TaskB());
service.submit(new TaskC());
}
}
区别于方案一,添加了ReentrantLock用来控制并发数量,同一时刻仅有一个线程在执行,减少了对cpu等资源的消耗;但是加锁本身也有资源消耗,所以到底是方案一消耗资源多还是方案二消耗资源多,也并不能确定。另外,这里要注意,如代码中使用的是new ReentrantLock(true)公平锁,为的是避免线程饥饿的情况出现,但是公平锁的性能是低于非公平锁的。
方案三:
public class Test {
private static ReentrantLock lock = new ReentrantLock();
private static Condition conditionA = lock.newCondition();
private static Condition conditionB = lock.newCondition();
private static Condition conditionC = lock.newCondition();
static class TaskA implements Runnable{
@Override
public void run() {
try {
lock.lock();
while (true) {
System.out.print("a");
conditionB.signal();
conditionA.await();
}
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static class TaskB implements Runnable{
@Override
public void run() {
try {
lock.lock();
while (true) {
System.out.print("b");
conditionC.signal();
conditionB.await();
}
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static class TaskC implements Runnable{
@Override
public void run() {
try {
lock.lock();
while (true) {
System.out.print("c");
conditionA.signal();
conditionC.await();
}
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(3);
service.submit(new TaskA());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.submit(new TaskB());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.submit(new TaskC());
}
}
方案三采用ReentrantLock和Condition联合控制,既保证同一时刻仅一个线程在运行,又保证了执行的顺序,且优于方案二,因为采用了ReentrantLock默认的非公平锁,效率更高。不过这里由于不再使用共享内存变量来控制执行顺序,所以需要在启动线程的时候略微间隔一点时间,保证线程的初始执行顺序是正确的。
***:Condition是什么,如果工作的?
Condition是在java.util.concurrent.locks包下,是一个接口;condition通常和ReentrantLock配合实现线程同步。
具体工作原理:1.拿到Condition对象:lock.newCondition();
final ConditionObject newCondition() {
return new ConditionObject();
}
public class ConditionObject implements Condition, java.io.Serializable
从源码可以看到,其实就是new了一个ConditionObject对象,ConditionObject实现了Condition接口。
2.唤醒他人,沉睡自己:conditionB.signal(); conditionA.await();
AQS:Abstract Queued Synchronizer 抽象队列同步器;我们这里简单的把它当做一个FIFO先入先出的双向队列即可,一个ReentrantLock对应一个AQS的队列。
如上图,每通过lock.newCondition()创建一个condition,相当于创建了一个FIFO的单向队列,该队列中存放的是调用了await()的线程,这些线程都是等待状态,等待移入AQS队列,去竞争锁;每当使用该condition对象调用signal(),则从该condition对象对应的队列中取出一个线程,添加到AQS的队列中,该线程得竞争锁的机会,将和AQS队列中的其它线程一起竞争锁(非公平锁),如果是公平锁,则按照进入AQS队列的顺序调度,先入队的线程优先获得竞争锁的机会(所以一般都是按照入队顺序获得锁,不排除竞争锁失败,队列中排在后面的线程先获得锁的情况)。
所以方案三的实现逻辑是这样的:
创建了一个ReentrantLock 独占式非公平锁 《==》 一个 AQS的队列;创建了三个Condition 《==》 三个单向等待队列;
首先TaskA先执行,因为没有其它竞争锁的线程,所以线程1通过lock.lock()获得锁,taskA得到执行机会。输出“a”,继续执行conditionB.signal(),试图从conditionB对应的队列(简称B队列)头取出一个线程移入AQS队列,但此时B队列是空的,所以这行等于啥也没做;然后执行conditionA.await(),线程1进入conditionA对应的队列(简称A队列),转换为等待状态,并由lock.unlock()释放获得的锁;
100ms后TaskB执行,此时也是没有其它线程竞争锁,所以线程2获得锁,TaskB得到机会执行。输出“b”,继续执行conditionC.signal()试图从conditionC对应的队列(简称C队列)头取出一个线程移入AQS队列,但此时C队列是空的,所以也是没实际作用;然后执行conditionB.await(),线程2进入B队列,转换为等待状态,并由lock.unlock()释放获得的锁;
又过100msTaskC执行,此时同样没有其它线程竞争锁,所以线程3获得锁,TaskC得到机会执行。输出“c”,继续执行conditionA.signal(),从A队列(A队列仅有线程1)头取出线程1放入AQS队列,线程1尝试获取锁,但是此时锁被线程3所得,所以线程1处于阻塞态,一直尝试获取锁;然后执行conditionC.await(),线程3进入C队列,转换为等待状态,并由lock.unlock()释放获得的锁;之后由于线程3释放了锁,所以在AQS队列中的线程1得到了锁(同时从AQS队列移除),TaskA得到执行,输出第二轮的“a”之后,从B队列取出线程2放入AQS队列,线程1进入A队列,然后释放自身获得的锁;之后AQS队列中的线程2得到锁(同时从AQS队列移除),TaskB得到执行,输出第二轮的“b”,从C队列取出线程3放入AQS队列,线程2进入B队列,然后释放自身获得的锁;再之后AQS队列中的线程3得到锁(同时从AQS队列移除),TaskC得到执行机会,输出第二轮的“c”,从A队列取出线程1放入AQS队列,线程3进入C队列,然后释放自身获得的锁。再触发第三轮循环,一直下去,所以程序可以一直按照顺序输入abc abc abc......
以上是就方案三简述了一下condition和reentrantlock联合实现的原理和具体底层逻辑,更加详细的锁分析,AQS和ABS锁核心,Condition底层原理,可重入锁底层原理等在《Java专题--锁》有更详细的分析,欢迎大家种草。
以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!