前言
这篇文章根据场景分类总结实践的最优做法以及注意事项, 不详细解释基本概念。 更灵活, 性能更好往往意味着代码复杂度的增加。 根据实际业务选择, 有时最简单的写法反而更合适当下的场景。
- 2023-10-15 更新常用并发容器章节
线程池
创建
- 线程池必须通过
ThreadPolExecutor的构造函数来显式地声明 - 构造时使用有界队列,控制线程创建数量, , 避免
Executors创建线程池导致的因最大线程数过大(Integer.MAX_VALUE)或者使用无界队列导致的OOM - 不同业务使用不同线程池, 并且在构造时给我们的线程池命名, 赋予其业务含义
- 应设置合适的核心线程数和最大线程数, 如果过小, 则会造成任务的阻塞堆积, 过大则会增加CPU上下文切换时间
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
*corePoolSize: 线程池的最小线程数, 又叫核心线程数,表示线程池中始终保持的线程数。即使线程是空闲的,也不会被销毁。*maximumPoolSize: 线程池所有线程(包含核心线程和非核心线程在内)的最大线程数,表示线程池允许创建的最大线程数。如果活动线程数达到最大线程数并且任务队列也已满,后续的任务会触发线程池的拒绝策略。keepAliveTime: 非核心线程(超出核心线程数的线程)在空闲状态下的最大存活时间。如果线程池中的线程数超过核心线程数,且空闲时间超过指定时间,这些非核心线程会被回收。unit:keepAliveTime的时间单位。*workQueue: 任务阻塞队列threadFactory: 生产线程池的工厂*handler: 拒绝策略
- 重点需要关注的是
corePoolSize,maximumPoolSize,workQueue的队列长度, 以及handler拒绝策略, 这4个参数决定了线程池的运作模式- 假设
corePoolSize是6,maximumPoolSize是10,workQueue的size为2:- 当池内线程超过
6时, 第7个任务将被放入workQueue- 当任务总数达到
8时, 这时workQueue也已经满了, 此时将会根据maximumPoolSize创建新的线程, 这里大家应该发现了,workQueue不能设的太大, 否则线程池无法扩容- 当任务总数达到
12时, 这时workQueue是满的, 而且池内的线程数量已经达到maximumPoolSize, 此时将会触发handler, 执行拒绝策略的逻辑
corePoolSize
有一个简单并且适用面比较广的公式:
- CPU 密集型任务(N+1): 多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程。
- N为逻辑CPU逻辑核心数, 可以用
Runtime.getRuntime().availableProcessors()获取- 例如, 对于8c16t的机器, N为16
maximumPoolSize
对于I/O 密集型任务, 它的值取决于最大瞬时并发数
BlockingQueue
最常用的是LinkedBlockingQueue
- 注意构造队列时传入容量, 防止无界队列
ThreadFactory
除了直接继承ThreadFactory实现外, 使用Guava的ThreadFactoryBuilder会更方便
new ThreadFactoryBuilder()
.setNameFormat("pool-name" + "-%d")
.setDaemon(true)
.setUncaughtExceptionHandler((t, e) -> {
System.out.println(t.getName());
e.printStackTrace();
})
.build();
setNameFormat(): 程池中的线程生成名称, pool-name-1, pool-name-2, pool-name-3, etc.setDaemon(): 设置是否为守护线程setUncaughtExceptionHandler(): 处理未捕获的异常
RejectedExecutionHandler
JDK提供了四种拒绝策略实现:
AbortPolicy: 默认策略, 抛出RejectedExecutionException异常。CallerRunsPolicy:让提交任务的线程自己去执行该任务,即在调用线程中执行被拒绝的任务。DiscardPolicy:默默丢弃被拒绝的任务,不抛出异常也不执行任务。DiscardOldestPolicy:丢弃工作队列中最老的任务,然后重新尝试提交当前任务。
监控
可以通过一些手段来检测线程池的运行状态比如 SpringBoot 中的 Actuator 组件。
简单的场景,我们还可以利用 ThreadPoolExecutor 的相关 API 做一个简陋的监控。
getPoolSize():获取线程池当前的线程数(包括核心线程和非核心线程)getActiveCount():获取活跃线程数,也就是正在执行任务的线程数量getQueue():获取线程池中的阻塞队列, 队列长度即正在等待执行的任务数getCompletedTaskCount(): 获取线程池已完成的任务数量getLargestPoolSize():获取线程池曾经到过的最大工作线程数getTaskCount():获取历史已完成以及正在执行的总的任务数量
除此之外, 还可以重写ThreadPoolExecutor中的钩子函数:
beforeExecute():Worker线程执行任务之前会调用的方法afterExecute():Worker线程执行任务之后会调用的方法terminated():当线程池变为TERMINATED状态之前调用的方法
比如, 我们可以用beforeExecute和afterExecute监听线程执行的时间,在afterExecute或 terminated时释放资源。
动态管理
ThreadPoolExecutor 提供了相关 API 在运行时动态调整线程池的核心线程数,最大线程数,最大存活时间和拒绝策略, 但任务阻塞队列的容量是final的, 如果想要调整的话, 可以参考ElasticSearch里面实现的ResizableBlockingQueue, 自己实现一个容量可变的容器
- 需注意任务阻塞队列的容量从大减小可能会导致的问题 // TODO
使用
结合Spring Boot提供的@EnableAsync和@Async注解, 使得线程池的使用更加方便
关闭
- 当线程池不再需要使用时,应该显式地关闭线程池,释放线程资源。
shutdown()和shutdownNow()都可用于关闭线程池 - 如有必要, 线程逻辑中应能正常响应中断信号, 用来做释放资源等操作
相同点:
- 调用后都不会再接收新的任务, 如果调用了
submit(), 会抛出RejectedExecutionException- 调用后都会立即返回, 等待不会造成阻塞, 如果希望阻塞等待可以调用
awaitTermination()方法。
不同点:
shutdown()会将线程池状态设为SHUTDOWN, 调用后等待所有已经提交的任务执行完成, 包括正在执行的和在阻塞队列中等待执行的, 同时向空闲的核心线程发送interrupt信号shutdownNow()会将线程池状态设为STOP, 调用后会向线程池中的所有线程发送interrupt信号, 最后返回阻塞队列中等待执行的任务(List<Runnable>)
一个典型的场景是调用shutdown()之后再调用awaitTermination()等待指定的时间, 超过时间后或发生异常时不再等待, 调用shutdownNow()向所有线程发送中断信号
ExecutorService es = Executors.newFixedThreadPool(10);
es.execute(new Thread(() - > {...}));
try {
es.shutdown();
if(!es.awaitTermination(5, TimeUnit.SECONDS)){
// 到达5s指定时间,还有线程没执行完,不再等待
es.shutdownNow();
}
} catch (Throwable e) {
es.shutdownNow();
}
可见性
volatile: 对于一个多线程共享的变量, 每次访问变量时,总是获取主内存的最新值, 且当某个线程在其本地内存副本中修改了该变量的值, 立刻回写到主内存
线程同步
原子操作
赋值不需要同步
- 基本类型(
long和double除外)赋值,例如:int n = m,long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。 - 引用类型赋值,例如:
List<String> list = anotherList
Atomic 原子类
把单个变量引用或者值的变化封装为原子操作, 可分为4类
基本类型
使用原子的方式更新基本类型
AtomicInteger:整型原子类AtomicLong:长整型原子类AtomicBoolean:布尔型原子类
数组类型
使用原子的方式更新数组里的某个元素
AtomicIntegerArray:整型数组原子类AtomicLongArray:长整型数组原子类AtomicReferenceArray:引用类型数组原子类
引用类型
AtomicReference:引用类型原子类AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来。AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
ABA问题不是必须解决的, 如果业务关注变量的值而不在意值变化的过程, 那就不需要处理。
对象的属性修改类型
AtomicIntegerFieldUpdater:原子更新整型字段的更新器AtomicLongFieldUpdater:原子更新长整型字段的更新器AtomicReferenceFieldUpdater:原子更新引用类型里的字段
注意: 需要原子操作的属性必须用
public volatile修饰
一般场景
使用synchronized或ReentrantLock
ReentrantLock性能更好, 提供了tryLock()方法限制了最大阻塞时间ReentrantLock更灵活, 临界区可跨多个代码块ReentrantLock更适用于顺序敏感的场景,synchronized和ReentrantLock默认都是非公平锁, 但ReentrantLock可以在构造时new ReentrantLock(true)设置为公平锁
读多写少
使用ReadWriteLock或StampedLock, 将读锁和写锁分离, 提高读并发性能
ReadWriteLock: 悲观, 把读写操作分别用读锁和写锁来加锁, 允许多个线程同时读(当有一个线程持有读锁, 其他线程也可以获取读锁, 这样就大大提高了并发读的执行效率), 但它只允许一个线程写入(当有一个线程持有写锁, 其他线程读锁和写锁都获取不到)StampedLock: 乐观, 它和ReadWriteLock相比,不同之处在于,读的过程中也允许获取写锁,这样一来,我们读的数据就可能不一致,但需要一点额外的代码来判断读的过程中是否有写入
public class Point {
private final StampedLock stampedLock = new StampedLock();
private double x;
private double y;
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
// 注意下面两行代码不是原子操作
// 假设x,y = (100,200)
double currentX = x;
// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
double currentY = y;
// 此处已读取到y,如果没有写入,读取是正确的(100,200)
// 如果有写入,读取是错误的(100,400)
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
死锁
各线程获取可重入锁的顺序一定要相同
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}
public void dec(int m) {
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
} // 释放lockA的锁
} // 释放lockB的锁
}
对于上述代码,线程1和线程2如果分别执行add()和dec()方法时, 执行到内层的synchronized时就会永远等待下去, 造成死锁
线程协同
等待-唤醒
使用synchronized+wait/notify, 或者ReentrantLock+Condition可以做到最细粒度的控制, 且进入WAITING状态会释放锁, 不会阻塞其他线程, 但代码较为繁琐
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll();
}
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
}
ThreadLocal
-
ThreadLocal用于存放每个线程各自的变量, 本质上是以线程实例自身的弱引用作为key, 从一个全局的ThreadLocal.ThreadLocalMap(可以理解为一个Map<WeakReference<ThreadLocal<?>>, Object>)中取值 -
需要注意的是, 当前线程执行完相关代码后,很可能会被重新放入线程池中,如果
ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。因此保证, 当前线程执行的任务, 在结束时能移除ThreadLocal关联的值, 我们借助try (resource) {...}结构,可以这么写:
public class UserContext implements AutoCloseable {
static final ThreadLocal<String> ctx = new ThreadLocal<>();
public UserContext(String user) {
ctx.set(user);
}
public static String currentUser() {
return ctx.get();
}
@Override
public void close() {
ctx.remove();
}
}
使用的时候:
try (var ctx = new UserContext("Bob")) {
// 可任意调用UserContext.currentUser():
String currentUser = UserContext.currentUser();
}
InheritableThreadLocal(ITL)
各线程的ThreadLocal之间没有父子关系, 如果子线程想获取父线程ThreadLocal的值, 就需要用到ThreadLocal
TransmittableThreadLocal(TTL)
InheritableThreadLocal仍然有缺陷,一般我们做异步化处理都是使用的线程池,而InheritableThreadLocal是在new Thread中的init()方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。
当然,有问题出现就会有解决问题的方案,阿里巴巴开源了一个TransmittableThreadLocal(TTL)组件, 用其包裹线程池实例即可实现父子线程的正确传值:
ExecutorService executorService = ...;
executorService = TtlExecutors.getTtlExecutorService(executorService);
还可以通过增加启动参数的方式以零侵入的方式实现, 这里不再详述, 项目在这里alibaba/transmittable-thread-local - GitHub
说到线程间的数据交换, 很自然地想到Exchanger, 虽然它在传递数据时不局限于父传子, 但各线程要在同步点等待, 显然性能较差
CompletableFuture
CompletableFuture在实践中较为常用, 下面详细说明
1. 实例方法
实例方法较多, 需要注意的是, 不带Aync的方法, 其执行者有可能是上文执行任务的子线程, 也有可能是调用方线程, 建议使用Aync版本的方法, 同时每一个步骤都指定线程池, 防止阻塞调用方线程, 确保任务由同一个线程中的线程执行, 消除执行者的不确定性
Actions supplied for dependent completions of non-async methods may be performed by the thread that completes the current CompletableFuture, or by any other caller of a completion method.
当单个异步任务完成后
thenApply: 对其结果执行Function<T,U>, 返回一个新CompletionStagethenAccept: 对其结果执行ConsumerthenRun: 执行一个RunnablethenCompose: 类似thenApply, 他们的回参类型都是CompletionStage, 但thenCompose执行的是不是一个普通的Function<T,U>, 而是Function<T,CompletionStage<U>>, 当现有的方法返回已经是一个CompletionStage时, 相比thenApply,thenCompose不会嵌套, 因此thenApply适合用来编写新的异步逻辑, 而thenCompose更适合用来串接多个已有的CompletableFuture
// 回调是普通方法
CompletableFuture<Integer> futureApply = CompletableFuture
.supplyAsync(() -> 1)
.thenApply(x -> x+1);
CompletableFuture<Integer> futureCompose = CompletableFuture
.supplyAsync(() -> 1)
.thenCompose(x -> CompletableFuture.supplyAsync(() -> x+1));
// 回调是已有的异步方法, thenApply会嵌套一层而thenCompose不会
public CompletableFuture<UserInfo> getUserInfo(userId)
public CompletableFuture<UserRating> getUserRating(UserInfo)
CompletableFuture<CompletableFuture<UserRating>> f =
userInfo.thenApply(this::getUserRating);
CompletableFuture<UserRating> relevanceFuture =
userInfo.thenCompose(this::getUserRating);
当两个异步任务都完成后
thenCombine: 对它们的结果执行BiFunction, 返回一个新结果thenAcceptBoth: 对它们的结果执行BiConsumerrunAfterBoth: 执行一个Runnable
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (result1, result2) -> result1 + result2);
int combinedResult = combinedFuture.join(); // 或者使用 get() 方法获取结果
System.out.println(combinedResult); // 输出:30,因为 future1 返回 10,future2 返回 20,合并结果为 10 + 20 = 30
当两个异步任务中的任意一个完成后
applyToEither: 对其结果后执行Function, 返回一个新结果, 不需要等待两个任务都完成acceptEither: 对其结果执行ConsumerrunAfterEither: 执行一个Runnable
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000); // 模拟任务1耗时2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
});
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000); // 模拟任务2耗时1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
return 20;
});
CompletableFuture<Integer> resultFuture = future1.applyToEither(future2, result -> result * 2);
int result = resultFuture.join(); // 或者使用 get() 方法获取结果
System.out.println(result); // 输出:40,因为 future2 先完成,结果为 20,应用 fn 函数得到 20 * 2 = 40
异常处理相关
exceptionally: 入参是一个Function, 当exceptionally前的异步操作抛出异常时,可以对这个异常进行处理,并返回一个新的CompletionStagehandle: 和exceptionally类似, 但入参是一个BiFunction, 因此异常和正常的情况可以处理, ,并返回一个新的CompletionStage, 传递给后面whenComplete: 和handle类似, , 可以同时处理正常和异常的情况, 但入参是一个BiConsumer,Consumer是没有回参的, 所以whenComplete不产生新的异步结果
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// Simulating an exception
throw new RuntimeException("Oops, something went wrong!");
}).exceptionally(ex -> {
// Handling the exception
System.out.println("Caught exception: " + ex.getMessage());
return 0; // Providing a default value
});
future.thenAccept(result -> {
System.out.println("Final result: " + result);
});
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 10 / 2;
});
CompletableFuture<String> handledFuture = future.handle((result, ex) -> {
if (ex != null) {
return "Error: " + ex.getMessage();
} else {
return "Result: " + result;
}
});
handledFuture.thenAccept(result -> {
System.out.println(result);
});
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 10 / 2;
});
future.whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("Exception occurred: " + ex.getMessage());
} else {
System.out.println("Result: " + result);
}
});
其它实例方法
Future接口下
get():调用方线程阻塞, 等待异步任务完成后获取结果get(long timeout, TimeUnit unit):同get(),但只等待指定的时间;cancel(boolean mayInterruptIfRunning):取消当前任务;isDone():判断任务是否已完成。join(): 调用方线程阻塞, 等待异步任务完成
CompletableFuture类下
getNow(T valueIfAbsent): 不会阻塞调用方线程, 如果任务已完成则返回结果, 否则返回给定的缺省值complete(T value)/completeExceptionally(Throwable ex): 直接手动完成异步任务, 返回给定的正常或异常结果, 如果在调用该方法之前已经有一个结果(包括正常结果或异常),则该方法不会生效, 这个方法可以用于模拟异步任务的完成,并将结果传递给等待该任务的其他部分。obtrudeValue(T value)/obtrudeException(Throwable ex): 类似complete(T value)/completeExceptionally(Throwable ex), 但会无视之前的结果, 强制替换为给定的值
备注
Future接口下的方法get()和CompletableFuture类下的join()方法的区别在于get()会抛出checked exception, 需要try...catch...手动处理异常, 而join()不需要, 发生异常时join()会抛出一个checked CompletionException,CompletionException中包裹着真正的异常信息- 对于异常的处理, 更好的方式还是在
get()或者join()之前就调用exceptionally
2. 静态方法
runAync/supplyAsync: 执行异步任务, 可指定提交的线程池, 默认使用ForkJoinPool.commonPool()anyOf/allOf:applyToEither/applyToBoth,acceptEither/acceptBoth,runAfterEither/runAfterBoth的强化版, 可以组合2个以上的CompletableFuturecompletedFuture: 创建一个已完成的任务, 并指定它的返回值, 可用于在异步调用链中返回常量
3. 属性
isCompletedExceptionally:如果异步任务异常结束时返回true, 未完成或已正常完成返回false
其它协同工具
CountDownLatch
CountDownLatch阻塞主线程, 等待指定数量的线程完成后, 执行指定的逻辑, 基本上可以被静态方法CompletableFuture.allOf()取代
CyclicBarrier
- 可循环屏障, 各子线程可设置同步点, 运行到同步点时
CyclicBarrier会阻塞子线程, 类似一道屏障拦截在多个线程上, 屏障本身包含一段逻辑, 线程经过屏障时会等待, 所有线程都通过屏障时, 执行屏障逻辑, 各子线程也继续往下执行 CyclicBarrier不会阻塞主线程
Exchanger
- 和
CyclicBarrier类似, 不过Exchanger的各子线程在到达同步点时, 可以获取其他线程的数据, 而不是执行屏障逻辑 Exchanger不会阻塞主线程
Phaser
- 更加强大和灵活的屏障, 相比
CyclicBarrier,可以实现多段同步, 而且可以动态地注册和取消注册线程
Semaphore
Semaphore用于限制同一时间并发访问的线程数量
大任务分割
ForkJoinPool线程池可以把一个大任务递归地分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction, 但代码较为繁琐, 简单场景推荐使用进一步封装的parallelStream()
应使用
parallelStream(), 它的性能优于stream().parallel()
常用并发容器
阻塞
虽然性能不如非阻塞容器, 阻塞容器也有其适用的场景, 比如:
- 线程池的任务队列
- 消息队列使用的的生产者-消费者模式
- 限流控制
List, Map, Set - Collections.synchronizedList/Map/Set
- 使用装饰器产生阻塞容器, 内部实现就是在原集合的各方法上加上
synchronized, 注意使用迭代器遍历时还是要加synchronized, 以List为例:
List<String> elements = Collections.synchronizedList(new ArrayList<>());
synchronized (elements) {
for (String element : elements) {
...
}
}
Queue - LinkedBlockingQueue
- JDK没有提供
Collections.synchronizedQueue装饰器, 我们可以用LinkedBlockingQueue, 其内部用ReentrantLock实现, 在构造时, 通常会指定其大小,如果未指定,容量等于Integer.MAX_VALUE
非阻塞
非阻塞容器提供更好的并发性能
List - CopyOnWriteArrayList
CopyOnWriteArrayList 线程安全的核心在于其采用了 写时复制(Copy-On-Write) 策略,当需要修改( add,set、remove 等操作) 时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。
Map, Set - ConcurrentHashMap
在 JDK1.7 的时候, ConcurrentHashMap使用分段锁(Segment), 当一个线程需要修改某个段的数据时,只需要获取该段对应的锁,而不会影响其他段的操作。这样可以减小锁的粒度,从而提高并发性能。
到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,并发控制使用 synchronized 和 CAS 实现。虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。
如果需要Set类型的容器, 可以调用ConcurrentHashMap.keySet()获取
Queue - ConcurrentLinkedQueue
ConcurrentLinkedQueue 内部使用 CAS 实现, 代码我们就不分析了。
容器内并发场景
// TODO
附录
Java内存模型(Java Memory Model)
线程状态
注意Blocked和Waiting的区别:
- Blocked: 处于锁竞争状态, 但未获取到锁
- Waiting: 等待其他线程执行完成, 会将持有的锁释放
线程池状态
-
线程池创建后处于
RUNNING状态。 -
调用
shutdown()方法后处于SHUTDOWN状态,线程池不能接受新的任务,清除一些空闲worker,不会等待阻塞队列的任务完成。 -
调用
shutdownNow()方法后处于STOP状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执行的任务全部丢弃。此时,poolsize = 0,阻塞队列的size = 0。 -
当所有的任务已终止,
ctl记录的任务数量为0,线程池会变为TIDYING状态。接着会执行terminated()函数。 -
线程池处在
TIDYING状态时,执行完terminated()方法之后,就会由TIDYING变为TERMINATED状态。
ThreadPoolExecutor中有一个控制状态的属性叫
ctl,它是一个AtomicInteger类型的变量。线程池状态就是通过AtomicInteger类型的成员变量ctl来获取的。
线程池任务提交流程
CAS - Compare and Swap
以原子类AtomicInteger的incrementAndGet()方法为例,它使用基于CAS算法的自旋锁(Spin Lock)实现, 可用代码简单描述为:
public int incrementAndGet(AtomicInteger var) {
int prev, next;
do {
prev = var.get();
next = prev + 1;
} while (!var.compareAndSet(prev, next));
return next;
}
如果AtomicInteger的当前值是prev,那么就更新为next,返回true。如果AtomicInteger的当前值不是prev,就什么也不干,返回false。配合do ... while循环,假设有其他线程修改了AtomicInteger的值,会再走一次do里面的逻辑, 重新get()最新的值, 保证最终的结果是正确的。
那么这里的compareAndSet()方法是怎么获取到最新的当前值的呢? 其实, CAS在JDK中是通过 C++ 内联汇编的形式实现, 通过 JNI 调用的。