常用进一步封装的锁类和最佳实践后续还会补充, 先挖个坑
基础概念
- 线程的状态
- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行
run()方法的Java代码; - Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting:运行中的线程,因为执行
sleep()方法正在计时等待; - Terminated:线程已终止,因为
run()方法执行完毕。
注意Blocked和Waiting的区别:
- Blocked: 处于锁竞争状态, 但未获取到锁
- Waiting: 等待其他线程执行完成, 会将持有的锁释放
- 守护线程
- 所有非守护线程都执行完毕后,无论有没有守护线程,JVM都会自动退出。
- 使用守护线程的一个常见例子是在后台执行周期性的任务,例如GC, 定时任务或日志清理等。
- 线程同步
- 临界区: 加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
对于语句:
n = n + 1;
看上去是一行语句,实际上对应了3条指令:
ILOAD
IADD
ISTORE
我们假设n的值是100,如果两个线程同时执行n = n + 1,得到的结果很可能不是102,而是101,原因在于:
加锁后:
- JVM规范定义的几种原子操作(单行, 多行赋值还是需要同步的):
基本类型(long和double除外)赋值,例如:int n = m;
引用类型赋值,例如:List<String> list = anotherList。
long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。
单条原子操作的语句不需要同步。
- 死锁
一个线程可以获取一个可重入锁后,再继续获取另一个可重入锁:
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()方法时:
- 线程1:进入
add(),获得lockA; - 线程2:进入
dec(),获得lockB。
随后:
- 线程1:准备获得
lockB,失败,等待中; - 线程2:准备获得
lockA,失败,等待中。
此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。
那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA,再获取lockB的顺序,改写dec()方法如下:
public void dec(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
} // 释放lockB的锁
} // 释放lockA的锁
}
常用关键字
sycronized:
属于可重入锁:
JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出
synchronized块,记录-1,减到0的时候,才会真正释放锁。
用sycronized修饰的实例方法, 等价于sycronized(this){ ... }, 对于静态方法则是sycronized(Foo.class){ ... }, 任何一个类都有一个由JVM自动创建的Class实例
在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁
在 JDK 1.6 之后,
synchronized进行了改进优化。其底层实现主要依靠 Lock-Free 的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和 CAS 类似的性能;而线程冲突严重的情况下,性能远高于 CAS。
volatile: 在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的
TODO 安全点
TODO 堆栈一致性
因此,volatile关键字的目的是告诉虚拟机:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。
锁的种类
- 独占锁和共享锁:
- 独占锁:也称为互斥锁,同一时刻只能有一个线程持有该锁,其他线程无法获取该锁。在Java中,
sycronized及ReentrantLock就是一种独占锁。 - 共享锁:允许多个线程同时持有锁,适用于多个线程可以同时读取共享资源的情况。在Java中,
ReadWriteLock提供了共享锁的机制。
-
可重入锁:可重入锁允许同一个线程多次获取同一个锁,而不会被自己所持有的锁所阻塞。在Java中,
ReentrantLock就是可重入锁的一种实现。 -
公平锁和非公平锁: 公平锁:按照线程的请求顺序来获取锁,保证线程获取锁的公平性。在Java中,
ReentrantLock可以作为公平锁使用。 非公平锁:对锁的获取没有公平性保证,允许新的线程插队抢占锁。在Java中,ReentrantLock可以作为非公平锁使用,默认情况下为非公平锁。 -
悲观锁和乐观锁:
- 悲观锁:假设并发情况下会发生竞争,因此在每次访问共享资源时都会进行加锁操作,防止其他线程修改资源, 例如
ReadWriteLock。 - 乐观锁:假设并发情况下竞争不会发生,因此在访问共享资源时不进行加锁操作,而是在更新资源时检查是否有其他线程对资源进行了修改, 例如
StampedLock。
- 自旋锁:自旋锁是一种特殊的锁,当线程尝试获取锁失败时,不立即阻塞线程,而是进行一段忙等待,不断尝试获取锁,直到成功或超过一定次数。在Java中,
Atomic包中的类(如AtomicInteger、AtomicReference等)就是基于CAS算法实现的锁。
常用锁类
ReentrantLock:可重入锁, 和synchronized类似
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
...
}
catch(...) {
...
}
finally {
lock.unlock();
}
}
优点:
- 更灵活,
lock()和unlock()可以跨多个不同的方法, 不同的代码块调用,tryLock()可以高性能, 超过时间就不再等待tryLock()可以提高安全性, 避免死锁缺点:
- 需要在
finally代码块手动调用unlock()- 需要自己处理异常
Condition: 可以从Lock对象的实例获取其Condition(其它比如ReadWriteLock是没有Condition的),Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String s) {
lock.lock();
try {
queue.add(s);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() {
lock.lock();
try {
while (queue.isEmpty()) {
condition.await();
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
和
tryLock()类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来if (condition.await(1, TimeUnit.SECOND)) { // 被其他线程唤醒 } else { // 指定时间内没有被其他线程唤醒 }
ReadWriteLock:悲观读锁, 把读写操作分别用读锁和写锁来加锁, 允许多个线程同时读(当有一个线程持有读锁, 其他线程可以获取读锁, 这样就大大提高了并发读的执行效率), 但它只允许一个线程写入(当有一个线程持有写锁, 其他线程读锁和写锁都获取不到)
public class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
private int[] counts = new int[10];
public void inc(int index) {
wlock.lock(); // 加写锁
try {
counts[index] += 1;
} finally {
wlock.unlock(); // 释放写锁
}
}
public int[] get() {
rlock.lock(); // 加读锁
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock(); // 释放读锁
}
}
}
StampedLock:乐观读锁,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);
}
}
主要关注读的方法distanceFromOrigin(), validate()获取版本号,如果在读取过程中有写入,版本号和乐观读锁tryOptimisticRead()的不同, 则获取悲观读锁, lock()之后, 再重新获取一次最新值
Semaphore: 可以理解为限流锁, 用于限制同一时间的最大访问数量。比如同一时刻最多创建100个数据库连接,最多允许10个用户下载等。
public class AccessLimitControl {
// 任意时刻仅允许最多3个线程获取许可:
final Semaphore semaphore = new Semaphore(3);
public String access() throws Exception {
// 如果超过了许可数量,其他线程将在此等待,最多等待3秒:
if (semaphore.tryAcquire(3, TimeUnit.SECONDS)) {
try {
// TODO:
return UUID.randomUUID().toString();
} finally {
semaphore.release();
}
}
}
}
Thread类常用方法
join(): 等待线程执行完成, 可指定等待时间, 超过即不再等待;stop(): 强行终止线程, 不推荐使用, 推荐用interrupt设置中断标记 TODO: 原因暂缓了解interrupt(): 设置中断标记, 对于处在Waiting状态的线程, 会立刻抛出InterruptedException;
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println("interrupted!");
}
hello.interrupt();
}
}
class HelloThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " hello!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}
main线程通过调用t.interrupt()从而通知t线程中断,而此时t线程正位于hello.join()的等待中,此方法会立刻结束等待并抛出InterruptedException。由于我们在t线程中捕获了InterruptedException,因此,就可以准备结束该线程。在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出。
可使用自定义的变量来代替interrupt()以避免抛出异常, 而且更加灵活, 注意要用volatile修饰变量以保证其可见性
-
setDeamon()/isDeamon(): 设定/检查是否为守护线程 -
setPriority(): 设定线程优先级1-10, 默认为5, 优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。 -
wait()和notify()/notifyAll():-
虽然他们是
Object类的方法, 但和Thread类紧密相关, 这里一并讨论 -
wait()使线程进入等待状态,wait()方法返回时需要重新获得锁 -
使用
notifyAll()将唤醒所有当前正在this锁等待的线程,而notify()只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。通常来说,notifyAll()更安全。有些时候,如果我们的代码逻辑考虑不周,用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。 -
调用
wait()和notify()/notifyAll()的方法必须是synchronized的, 否则会抛出IllegalMonitorStateException -
wait()和notify()/notifyAll()都是object类的方法, 必须在已获得的锁对象上调用它们
synchronized解决了多线程竞争的问题, 但没有解决并没有解决多线程协调的问题
class TaskQueue { Queue<String> queue = new LinkedList<>(); public synchronized void addTask(String s) { this.queue.add(s); } public synchronized String getTask() { while (queue.isEmpty()) { } return queue.remove(); } }while()循环永远不会退出。因为线程在执行while()循环时,已经在getTask()入口获取了this锁,其他线程根本无法调用addTask(),因为addTask()执行条件也是获取this锁。- 结合一个完整的例子总结下
wait()和notify()/notifyAll()的特性
@SpringBootApplication public class DemoApplication implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Override public void run(String... args) throws Exception { TaskQueue q = new TaskQueue(); List<Thread> ts = new ArrayList<>(); for (int i = 0; i < 3; i++) { Thread t = new Thread(() -> { try { String s = q.getTask(); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getId() + " is interrupted "); return; } }, "tGet" + i); t.start(); ts.add(t); } Thread add = new Thread(() -> { for (int i = 0; i < 2; i++) { try { Thread.sleep(2000); } catch (InterruptedException e) { } String s = "t-" + Math.random(); System.out.println(LocalTime.now() + " add element: " + s); q.addTask(s); } }, "tAdd"); add.start(); add.join(); Thread.sleep(100); for (Thread t : ts) { t.interrupt(); } System.out.println("main function end"); } public 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(); } }-
看下这段代码会怎么执行, 这里要关注调用
wait()的3个线程状态的变化, 以及this锁的持有者
-
主线程循环发起3个线程, 调用
getTask()开始等待,getTask()方法会调用wait(), 因此这3个线程会依次获取/释放掉this锁, 状态Runnable->Waiting -
主线程循环发起2个线程, 每隔5s调用一次
addTask(),addTask()方法会获取this锁, 加入1个元素, 以及调用notifyAll()方法唤醒所有等待的线程, 最终释放锁 -
加入第一个元素, 调用
notifyAll()方法, 唤醒所有线程, 被唤醒的3个线程开始竞争锁, 它们的状态Waiting->Blocked -
拿到锁的线程, 跳出
while循环remove()元素返回后,getTask()方法调用结束,this锁释放, 该线程的状态Blocked->Runnable->Terminated -
没拿到锁的另外2个线程进入下一次
while循环 继续等待 它俩的状态Blocked->Waiting -
继续加入第二个元素, 状况和上面两步雷同
-
最后, 有3个线程等待获取元素,但我们一共只加入了2个,最终会有1个线程等不到元素还在
Waiting,他会被主线程interrupt掉
可以加个
sleep()方便观察到Blocked状态: stackoverflow.com/q/76748466/…可见
wait()和notify()/notifyAll()用法较为繁琐, 稍不注意就会出问题 -
进一步封装的类
Atomic: Java的java.util.concurrent包除了提供底层锁、并发集合外,还提供了一组原子操作的封装类,它们位于java.util.concurrent.atomic包。 我们以AtomicInteger为例,它提供的主要操作有:
- 增加值并返回新值:
int addAndGet(int delta) - 加1后返回新值:
int incrementAndGet() - 获取当前值:
int get() - 用CAS方式设置:
int compareAndSet(int expect, int update)
CAS(Compare and Set)算法:
- 读取内存地址V的值保存在A中
- 在原子操作中比较内存地址V的值是否与A相同
- 相同时,修改内存地址V的值为B,原子操作成功。
Java 并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现, 通过 JNI 调用。
- ABA 问题: TODO
以incrementAndGet()为例,它基于CAS和自旋锁实现, 可用代码简单描述为:
public int incrementAndGet(AtomicInteger var) {
int prev, next;
do {
prev = var.get();
next = prev + 1;
} while (!var.compareAndSet(prev, next));
return next;
}
先获取一次当前值prev, 得到next值为prev + 1, 再循环判断compareAndSet的结果
- 如果
compareAndSet返回true, 即当前值就是最新值, 没有变化, 则将此AtomicInteger实例的值设为next, 跳出循环 - 如果
compareAndSet返回false, 即当前值被其他线程修改, 发生了变化, 则继续循环, 直到compareAndSet返回true为止
- 自旋锁(SpinLock): 指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环, 其实就是上文的
while (!var.compareAndSet(prev, next))自旋锁的实现基础是CAS算法
- 自旋锁的优点:
- 线程在正常执行(
Runnable)时, 对操作系统来说该线程是用户线程, 而当线程被阻塞(Blocked)时, 该线程转变为内核线程, 由操作系统内核直接管理- 对于
syncronize等独占锁,会让没有得到锁资源的线程进入Blocked状态, 在争夺到锁资源后恢复为Runnable状态,这个过程中涉及到操作系统内核态和用户态的切换,代价比较高。而自旋锁不会使线程进入Blocked状态,而是一直保持Runnable, 如果自旋锁已经被别的线程持有,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。
- 内核态(Kernel Mode): 是计算机操作系统中运行在特权级别最高的一种执行模式。在内核态下,操作系统内核拥有对计算机硬件和资源的完全控制权,并能执行所有的指令和访问所有的系统资源。相比之下,用户态(User Mode)是计算机操作系统中较低的特权级别。在用户态下,应用程序运行在受限的环境中,只能访问受限的硬件资源和执行受限的指令。操作系统会尽量减少用户态和内核态之间的切换次数,以提高系统的性能。
- 在高度竞争的情况下,还可以使用Java 8提供的
LongAdder和LongAccumulator, 与AtomicLong相比,LongAdder和LongAccumulator能将一个long值拆分成多个部分,每个线程在更新值时独立操作一个部分,最后将所有部分的值相加得到最终结果。这种方式减少了竞争,大大提高了并发性能。但有可能得到一个近似的结果, 而AtomicLong的结果是精准的
Future/FutureTask<T>/CompletableFuture:
先看下两个相关的接口
Future接口: 代表Runnable或者Callable<T>任务运行的未来结果,Future定义的方法有:
get():调用方线程阻塞, 等待异步任务完成后获取结果get(long timeout, TimeUnit unit):同get(),但只等待指定的时间;cancel(boolean mayInterruptIfRunning):取消当前任务;isDone():判断任务是否已完成。
注意
Future是任务的未来结果, 不是执行者, 任务还是要由Thread或者线程池中的Worker执行
CompletionStage接口: 定义了任务完成之后执行的回调函数, 所有的回调函数都会返回CompletionStage, 以便于链式调用,可以分为以下几组
当单个异步任务完成后
thenApply: 对其结果执行Function<T,U>, 返回一个新CompletionStagethenAccept: 对其结果执行ConsumerthenRun: 执行一个RunnablethenCompose: 类似thenApply, 他们的回参类型都是CompletionStage, 但thenCompose执行的是不是一个普通的Function<T,U>, 而是Function<T,CompletionStage<U>>, 当现有的方法返回已经是一个CompletionStage时, 相比thenApply,thenCompose不会嵌套, 因此thenCompose适合用来串接多个CompletionStage
// 回调是普通方法
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之前的?TODO异步操作抛出异常时,可以对这个异常进行处理,并返回一个新的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);
}
});
截至目前为止,
CompletionStage接口下所有的方法都分类完了, 以上方法都带有一个Async版本和一个可以指定线程池的Async版本(默认提交到ForkJoinPool.commonPool()线程池中执行)Async和Non-Async的区别, 官方文档上说的很模糊, 以thenRunAsync(Runnable action)为例, 大致可理解为Async方法一定会从线程池中选取一个空闲?(TODO 选取的依据?)worker去执行回调action, 而Non-Async方法会尝试由主线程(准确地说是调用thenRunAsync方法的线程)去执行回调action, 这个主要是看调用thenRun的时候任务是否已经完成。如果没有完成,那么action会在完成任务的线程中执行。如果任务执行的非常快 ,已经完成,则action会在调用thenRun方法的线程中执行。为避免不确定性, 以及防止主线程阻塞, 实践中应避免使用
Non-Async版本参考:
// supplyAsync任务执行的非常快, thenRun方法由主线程执行, 导致主线程被阻塞10s
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
System.out.println("[" + Thread.currentThread().getName() + "] " + " in task");
return 1;
}).thenRun(() -> {
System.out.println("[" + Thread.currentThread().getName() + "] " + " in action start");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("[" + Thread.currentThread().getName() + "] " + " in action end");
});
future.get();
再看相关的实现类
FutureTask<T>: 同时继承了Runnable和Future
手动创建一个FutureTask<T>, 再创建一个Thread执行FutureTask<T>, 最后由Future获取结果:
FutureTask<String> futureTask =
new FutureTask<String>(() -> {
TimeUnit.SECONDS.sleep(3);
return "hello";
});
Thread t = new Thread(futureTask);
t.start();
String result = futureTask.get();
System.out.println(result);
Thread的构造只接受Runnable, 不能执行Callable<T>, 而FutureTask<T>同时继承了Runnable和Future, 意味着FutureTask<T>既是可被执行的任务本身, 又是任务未来结果
由线程池直接执行Callable<T>, submit方法会返回Future, 最后由Future获取结果:
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<String> future = executor.submit(() -> {
TimeUnit.SECONDS.sleep(3);
return "hello";
});
String result = future.get();
System.out.println(result);
CompletableFuture: 同时继承了CompletionStage和Future
上文已经详细说了CompletionStage和Future所定义的方法, 除了这些方法外, CompletableFuture还提供了一些实用的扩展, 我们先看静态方法:
runAync/supplyAsync: 执行异步任务, 可指定提交的线程池, 默认使用ForkJoinPool.commonPool()anyOf/allOf:applyToEither/applyToBoth,acceptEither/acceptBoth,runAfterEither/runAfterBoth的强化版, 可以组合2个以上的CompletableFuturecompletedFuture: 创建一个已完成的任务, 并指定它的返回值, 可用于在异步调用链中返回常量
再看实例方法:
join(): 调用方线程阻塞, 等待异步任务完成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中包裹着真正的异常信息- 对异常的处理更推荐使用
exceptionally
Checked Exception: 已知的可能抛出的异常, 方法签名后用throws标记, 暗示调用方可能发生的异常, 并要求调用方必须处理, 否则无法通过编译检查
public void readFile(String fileName) throws IOException {
// 读取文件的代码,可能抛出IOException
}
Unchecked Exception: 这种异常不强制要求调用方处理一种是在运行时发生的意料外的异常, 常见的例如
NullPointerException,ArrayIndexOutOfBoundsException还有一种情况是手动抛出的异常, 并且方法签名后没有加
throws
public void divide(int dividend, int divisor) {
if (divisor == 0) {
throw new ArithmeticException("Division by zero");
}
int result = dividend / divisor;
}
最后再看属性:
isCompletedExceptionally:如果异步任务异常结束时返回true, 未完成或已正常完成返回falsegetNumberOfDependents:NumberOfDependents指的是指的是通过类似thenApply,thenAccept,thenRun,thenCompose,thenCombine,whenComplete,handle等方法创建的异步任务,getNumberOfDependents返回正在等待主要(第一个)异步任务完成的依赖任务的数量, 注意不是未完成(包括正在执行中)的依赖任务(我原先的理解是错的)注意不是
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello, CompletableFuture!";
});
future.thenApplyAsync(result -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Transformed1: " + result;
});
future.thenApplyAsync(result -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Transformed1: " + result;
});
System.out.println("Number of dependents for future: " + future.getNumberOfDependents());
TimeUnit.SECONDS.sleep(2);
System.out.println("Number of dependents for future: " + future.getNumberOfDependents());
TimeUnit.SECONDS.sleep(4);
System.out.println("Number of dependents for future: " + future.getNumberOfDependents());
TimeUnit.SECONDS.sleep(6);
System.out.println("Number of dependents for future: " + future.getNumberOfDependents());
// Number of dependents for future: 2
// Number of dependents for future: 0
// Number of dependents for future: 0
// Number of dependents for future: 0
// 1秒之后主任务已完成, 因此后续没有等待它的以来任务
// 如果按照错误的"未完成(包括正在执行中)的依赖任务"去理解 应该是2 2 1 0才对了
ForkJoin: TODO 如何确保利用到多个核心? 在容器内的表现?ThreadLocal: TODO 其Entry使用的是K-V方式来组织数据,Entry中key是ThreadLocal对象,且是一个弱引用。
- 弱引用: 生命周期只能存活到下次GC前
线程池
- 使用线程池主要有以下三个原因:
- 创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程。
- 控制并发的数量。并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。(主要原因)
- 可以对线程做统一管理。
- 线程的状态
-
线程池创建后处于RUNNING状态。
-
调用shutdown()方法后处于SHUTDOWN状态,线程池不能接受新的任务,清除一些空闲worker,不会等待阻塞队列的任务完成。
-
调用shutdownNow()方法后处于STOP状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执行的任务全部丢弃。此时,poolsize=0,阻塞队列的size也为0。
-
当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。接着会执行terminated()函数。
ThreadPoolExecutor中有一个控制状态的属性叫
ctl,它是一个AtomicInteger类型的变量。线程池状态就是通过AtomicInteger类型的成员变量ctl来获取的。获取的
ctl值传入runStateOf方法,与~CAPACITY位与运算(CAPACITY是低29位全1的int变量)。~CAPACITY在这里相当于掩码,用来获取ctl的高3位,表示线程池状态;而另外的低29位用于表示工作线程数 -
线程池处在TIDYING状态时,执行完terminated()方法之后,就会由 TIDYING -> TERMINATED, 线程池被设置为TERMINATED状态。
- 任务提交的处理流程
Java标准库提供了ExecutorService接口表示线程池, 几个常用实现类有:
FixedThreadPool:线程数固定的线程池
// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);
CachedThreadPool:线程数根据任务动态调整的线程池
用法和FixedThreadPool类似, 但如果我们想把线程池的大小限制在4~10个之间动态调整怎么办?我们查看Executors.newCachedThreadPool()方法的源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
因此,想创建指定动态范围的线程池,可以这么写:
int min = 4;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(min, max,
60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
创建线程池时, 建议使用
ThreadPoolExecutor可以进行更详细的设置, 避免Executors创建线程池导致的因最大线程数过大(Integer.MAX_VALUE)或者使用无界队列导致的OOM
min: 线程池的最小线程数, 又叫核心线程数,表示线程池中始终保持的活动线程数。即使线程是空闲的,也不会被销毁,除非设置了适当的线程超时策略。在你的代码中,核心线程数为4,意味着线程池会始终保持至少4个活动线程。max: 线程池的最大线程数,表示线程池允许创建的最大线程数。如果活动线程数达到最大线程数并且任务队列也已满,后续的任务会触发线程池的拒绝策略。在你的代码中,最大线程数为10,所以线程池允许最多同时创建10个线程。60L: 线程的存活时间,即非核心线程(超出核心线程数的线程)在空闲状态下的最大存活时间。如果线程池中的线程数超过核心线程数,且空闲时间超过指定时间,这些非核心线程会被回收。在你的代码中,非核心线程的空闲时间为60秒,如果一个非核心线程在60秒内没有任务可执行,它将被销毁。TimeUnit.SECONDS: 存活时间的时间单位,这里使用的是秒。new SynchronousQueue<Runnable>(): 任务阻塞队列的实现,SynchronousQueue是一个零容量的队列,它不会保存任务,而是直接将任务交给消费者线程处理。这意味着每个任务都需要等待一个线程来处理,所以线程池在这里使用了SynchronousQueue来确保每个任务都会创建一个新线程,直到达到最大线程数。
阻塞队列: TODO
ScheduledThreadPool:有一种任务,需要定期反复执行,例如,每秒刷新证券价格。
创建一个ScheduledThreadPool仍然是通过Executors类:
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
我们可以提交一次性任务,它会在指定延迟后只执行一次:
// 1秒后执行一次性任务:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
如果任务以固定的每3秒执行,我们可以这样写:
// 2秒后开始执行定时任务,每3秒执行:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
如果任务以固定的3秒为间隔执行,我们可以这样写:
// 2秒后开始执行定时任务,以3秒为间隔执行:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
注意FixedRate和FixedDelay的区别。FixedRate是指任务总是以固定时间间隔触发,不管任务执行多长时间:
而FixedDelay是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务:
线程复用的原理:
shutdown()/shutdownNow(): 都可用于关闭线程池
- 调用后都不会再接收新的任务, 如果调用了
submit(), 会抛出RejectedExecutionException- 调用后都会立即返回, 等待不会造成阻塞, 如果希望阻塞等待可以调用
awaitTermination()方法。
shutdown()会将线程池状态设为SHUTDOWN, 调用后等待所有已经提交的任务执行完成, 包括正在执行的和在阻塞队列中等待执行的, 同时向空闲的核心线程发送interrupt信号shutdownNow()会将线程池状态设为STOP, 调用后会向线程池中的所有线程发送interrupt信号, 最后返回阻塞队列中等待执行的任务(List<Runnable>)
awaitTermination(): 阻塞等待线程池关闭, 可指定最大等待时间, 并返回布尔值告知目前线程池是否已经真正关闭了(TERMINATED)
- 一个典型的场景是调用
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();
}
- Guava的线程池 // TODO
最佳实践
分场景: eg 读多写少