wait和notify(多线程协调)
synchronized
解决了多线程竞争,但是synchronized
并没有解决多线程协调的问题。
多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
/*
*注意到在往队列中添加了任务后,线程立刻对this锁对象调用notify()方法,
* 这个方法会唤醒一个正在this锁等待的线程(就是在getTask()中位于this.wait()的线程),
* 从而使得等待线程从this.wait()方法返回。
*/
this.notify(); // 唤醒在this锁等待的线程
/**
* 内部调用了this.notifyAll()而不是this.notify(),使用notifyAll()将唤醒所有当前正在this锁等待的线程,
* 而notify()只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。
* 这是因为可能有多个线程正在getTask()方法内部的wait()中等待,
* 使用notifyAll()将一次性全部唤醒。通常来说,notifyAll()更安全。
* 有些时候,如果我们的代码逻辑考虑不周,用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。
*/
this.notifyAll();
}
public synchronized String getTask(String s) throws InterruptedException {
/**
* 因为线程被唤醒时,需要再次获取this锁。多个线程被唤醒后,只有一个线程能获取this锁,此刻,该线程执行queue.remove()可以获取到队列的元素,
* 然而,剩下的线程如果获取this锁后执行queue.remove(),此刻队列可能已经没有任何元素了,
* 所以,要始终在while循环中wait(),并且每次被唤醒后拿到this锁就必须再次判断:
*/
while (queue.isEmpty()){
// 释放this锁:
//在条件不满足时,线程进入等待状态,wait()方法必须在当前获取的锁对象上调用,这里获取的是this锁
/*
* 它不是一个普通的Java方法,而是定义在Object类的一个native方法,也就是由JVM的C代码实现的。
* 其次,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,
* wait()方法返回后,线程又会重新试图获得锁。
*/
this.wait();
// 重新获取this锁
}
return queue.remove();
}
}
小结
wait
和notify
用于多线程协调运行:
在
synchronized
内部可以调用wait()
使线程进入等待状态;必须在已获得的锁对象上调用
wait()
方法;在
synchronized
内部可以调用notify()
或notifyAll()
唤醒其他等待线程;必须在已获得的锁对象上调用
notify()
或notifyAll()
方法;已唤醒的线程还需要重新获得锁后才能继续执行。
ReentrantLock(锁)
高级的处理并发的java.util.concurrent
包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写
Java语言直接提供了synchronized
关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制
java.util.concurrent.locks
包提供的ReentrantLock
用于替代synchronized
加锁
public class Main {
private int count;
public void add(int n){
//synchronized是Java语言层面提供的语法,所以我们不需要考虑异常
synchronized (this){
count += n ;
}
}
}
class Counetr{
//ReentrantLock是Java代码实现的锁,我们就必须先获取锁,然后在finally中正确释放锁
//ReentrantLock是可重入锁,它和synchronized一样,一个线程可以多次获取同一个锁
private final Lock lock = new ReentrantLock();
private int count;
public void add(int n){
lock.lock();
try {
count += n;
} finally {
lock.unlock();
}
}
//和synchronized不同的是,ReentrantLock可以尝试获取锁
//代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,
// 程序就可以做一些额外处理,而不是无限等待下去。
//使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。
/*
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
*/
}
//ReentrantLock可以替代synchronized进行同步;
//ReentrantLock获取锁更安全;
//必须先获取到锁,再进入try {...}代码块,最后使用finally保证释放锁;
//可以使用tryLock()尝试获取锁。
Condition(唤醒)
synchronized
可以配合wait
和notify
实现线程在条件不满足时等待,条件满足时唤醒。
ReentrantLock
用Condition
对象来实现wait
和notify
的功能。
class TaskQueue{
private final Lock lock = new ReentrantLock();
//使用Condition时,引用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例。
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
/**
* Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的:
*
* await()会释放当前锁,进入等待状态;
* signal()会唤醒某个等待线程;
* signalAll()会唤醒所有等待线程;
* 唤醒线程从await()返回后需要重新获得锁。
*/
public void addTask(String s) {
lock.lock();
try {
queue.add(s);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() throws InterruptedException {
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 {
// // 指定时间内没有被其他线程唤醒
// }
//可见,使用Condition配合Lock,我们可以实现更灵活的线程同步。
}
//Condition可以替代wait和notify;
//Condition对象必须从Lock对象获取。
ReadWriteLock(读取效率)
使用ReadWriteLock
可以提高读取效率:
-
ReadWriteLock
只允许一个线程写入(其他线程既不能写入也不能读取); -
ReadWriteLock
允许多个线程在没有写入时同时读取(提高性能); -
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(); // 释放读锁
}
}
}
//把读写操作分别用读锁和写锁来加锁,在读取时,多个线程可以同时获得读锁,这样就大大提高了并发读的执行效率。
//使用ReadWriteLock时,适用条件是同一个数据,有大量线程读取,但仅有少数线程修改。
//例如,一个论坛的帖子,回复可以看做写入操作,它是不频繁的,但是,浏览可以看做读取操作,是非常频繁的,这种情况就可以使用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() {
//首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号
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)
//接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。
//如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
/**
* 可见,StampedLock把读锁细分为乐观读和悲观读,能进一步提升并发效率。但这也是有代价的:
* 一是代码更加复杂,二是StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁。
*
* StampedLock还提供了更复杂的将悲观读锁升级为写锁的功能,它主要使用在if-then-update的场景:即先读,
* 如果读的数据满足条件,就返回,如果读的数据不满足条件,再尝试写。
*
* StampedLock提供了乐观读锁,可取代ReadWriteLock以进一步提升并发性能;
*
* StampedLock是不可重入锁。
*
*/
}
Concurrent集合(安全性)
BlockingQueue
的意思就是说,当一个线程调用这个TaskQueue
的getTask()
方法时,该方法内部可能会让线程变成等待状态,直到队列条件满足不为空,线程被唤醒后,getTask()
方法才会返回。
可以直接使用Java标准库的java.util.concurrent
包提供的线程安全的集合:ArrayBlockingQueue
Map<String, String> map = new HashMap<>();
//改为:
Map<String, String> map = new ConcurrentHashMap<>();
//旧方法:
Map unsafeMap = new HashMap();
Map threadSafeMap = Collections.synchronizedMap(unsafeMap);
使用
java.util.concurrent
包提供的线程安全的并发集合可以大大简化多线程编程:多线程同时读写并发集合是安全的;
尽量使用Java标准库提供的并发集合,避免自己编写同步代码。
线程池(重点)
把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池。
简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
1.ExecutorService
接口
// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);
2.Executors类
- FixedThreadPool:线程数固定的线程池;
- CachedThreadPool:线程数根据任务动态调整的线程池;
- SingleThreadExecutor:仅单线程执行的线程池。
FixedThreadPool
import java.util.concurrent.*;
/**
* start task 0
* start task 2
* start task 3
* start task 1
* end task 3
* end task 1
* end task 2
* start task 4
* start task 5
* end task 0
* end task 4
* end task 5
*/
public class Main {
public static void main(String[] args) {
// 创建一个固定大小的线程池:
//一次性放入6个任务,由于线程池只有固定的4个线程,因此,前4个任务会同时执行,等到有线程空闲后,才会执行后面的两个任务。
ExecutorService es = Executors.newFixedThreadPool(4);
for (int i = 0; i < 6; i++) {
es.submit(new Task("" + i));
}
// 关闭线程池:
//使用shutdown()方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭。
//shutdownNow()会立刻停止正在执行的任务,awaitTermination()则会等待指定的时间让线程池关闭。
es.shutdown();
}
}
class Task implements Runnable {
private final String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("start task " + name);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("end task " + name);
}
}
CachedThreadPool
线程池的实现会根据任务数量动态调整线程池的大小,所以6个任务可一次性全部同时执行。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//把线程池的大小限制在4~10个之间动态调整
int min = 4;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(min, max,
60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
3.ScheduledThreadPool(定期反复,证券)
还有一种任务,需要定期反复执行,例如,每秒刷新证券价格。这种任务本身固定,需要反复执行的,可以使用ScheduledThreadPool
。放入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的区别。
Java标准库还提供了一个java.util.Timer
类,这个类也可以定期执行任务,但是,一个Timer
会对应一个Thread
,所以,一个Timer
只能定期执行一个任务,多个定时任务必须启动多个Timer
,而一个ScheduledThreadPool
就可以调度多个定时任务,所以,我们完全可以用ScheduledThreadPool
取代旧的Timer
。
JDK提供了
ExecutorService
实现了线程池功能:
线程池内部维护一组线程,可以高效执行大量小任务;
Executors
提供了静态方法创建不同类型的ExecutorService
;必须调用
shutdown()
关闭ExecutorService
;
ScheduledThreadPool
可以定期调度多个任务。
Future(返回)
Runnable
接口有个问题,它的方法没有返回值.
使用Callable
接口是一个泛型接口,可以返回指定类型的结果。
class Task implements Runnable {
public String result;
public void run() {
this.result = longTimeCalculation();
}
}
class Task implements Callable<String> {
public String call() throws Exception {
return longTimeCalculation();
}
}
获得异步执行的结果
ExecutorService.submit()
方法,可以看到,它返回了一个Future
类型,一个Future
类型的实例代表一个未来能获取结果的对象:
ExecutorService executor = Executors.newFixedThreadPool(4);
// 定义任务:
Callable<String> task = new Task();
// 提交任务并获得Future:
Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果:
String result = future.get(); // 可能阻塞
当我们提交一个Callable
任务后,我们会同时获得一个Future
对象,然后,我们在主线程某个时刻调用Future
对象的get()
方法,就可以获得异步执行的结果。在调用get()
时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么get()
会阻塞,直到任务完成后才返回结果。
一个
Future<V>
接口表示一个未来可能会返回的结果,它定义的方法有:
get()
:获取结果(可能会等待)get(long timeout, TimeUnit unit)
:获取结果,但只等待指定的时间;cancel(boolean mayInterruptIfRunning)
:取消当前任务;isDone()
:判断任务是否已完成。
CompletableFuture(重点,股票)
1.回调
使用Future
获得异步执行结果时,要么调用阻塞方法get()
,要么轮询看isDone()
是否为true
,这两种方法都不是很好,因为主线程也会被迫等待。
Java 8开始引入了CompletableFuture
,它针对Future
做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
获取股票价格
// CompletableFuture
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) throws Exception {
//创建一个CompletableFuture是通过CompletableFuture.supplyAsync()实现的,它需要一个实现了Supplier接口的对象:
/*
public interface Supplier<T> {
T get();
}
*/
//lambda语法简化,直接传入Main::fetchPrice,因为Main.fetchPrice()静态方法的签名符合Supplier接口的定义(除了方法名外)。
// 创建异步执行任务:
CompletableFuture<Double> cf = CompletableFuture.supplyAsync(Main::fetchPrice);
// 如果执行成功:
cf.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 如果执行异常:
cf.exceptionally((e) -> {
e.printStackTrace();
return null;
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(200);
}
static Double fetchPrice() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
if (Math.random() < 0.3) {
throw new RuntimeException("fetch price failed!");
}
return 5 + Math.random() * 20;
}
}
完成时,CompletableFuture
会调用Consumer
对象:
public interface Consumer<T> {
void accept(T t);
}
异常时,CompletableFuture
会调用Function
对象:
public interface Function<T, R> {
R apply(T t);
}
CompletableFuture
的优点是:
- 异步任务结束时,会自动回调某个对象的方法;
- 异步任务出错时,会自动回调某个对象的方法;
- 主线程设置好回调后,不再关心异步任务的执行。
2.串行执行
CompletableFuture
更强大的功能是,多个CompletableFuture
可以串行执行,例如,定义两个CompletableFuture
,第一个CompletableFuture
根据证券名称查询证券代码,第二个CompletableFuture
根据证券代码查询证券价格,这两个CompletableFuture
实现串行操作如下:
// CompletableFuture
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) throws Exception {
// 第一个任务:
CompletableFuture<String> cfQuery = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油");
});
// cfQuery成功后继续执行下一个任务:
CompletableFuture<Double> cfFetch = cfQuery.thenApplyAsync((code) -> {
return fetchPrice(code);
});
// cfFetch成功后打印结果:
cfFetch.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(2000);
}
static String queryCode(String name) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
return "601857";
}
static Double fetchPrice(String code) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
return 5 + Math.random() * 20;
}
}
3.并行执行
同时从新浪和网易查询证券代码,只要任意一个返回结果,就进行下一步查询价格,查询价格也同时从新浪和网易查询,只要任意一个返回结果,就完成操作:
// CompletableFuture
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) throws Exception {
// 两个CompletableFuture执行异步查询:
CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油", "https://finance.sina.com.cn/code/");
});
CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油", "https://money.163.com/code/");
});
// 用anyOf合并为一个新的CompletableFuture:
CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163);
// 两个CompletableFuture执行异步查询:
CompletableFuture<Double> cfFetchFromSina = cfQuery.thenApplyAsync((code) -> {
return fetchPrice((String) code, "https://finance.sina.com.cn/price/");
});
CompletableFuture<Double> cfFetchFrom163 = cfQuery.thenApplyAsync((code) -> {
return fetchPrice((String) code, "https://money.163.com/price/");
});
// 用anyOf合并为一个新的CompletableFuture:
CompletableFuture<Object> cfFetch = CompletableFuture.anyOf(cfFetchFromSina, cfFetchFrom163);
// 最终结果:
cfFetch.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(200);
}
static String queryCode(String name, String url) {
System.out.println("query code from " + url + "...");
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {
}
return "601857";
}
static Double fetchPrice(String code, String url) {
System.out.println("query price from " + url + "...");
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {
}
return 5 + Math.random() * 20;
}
}
anyOf()
可以实现“任意个CompletableFuture
只要一个成功”,
allOf()
可以实现“所有CompletableFuture
都必须成功”,
这些组合操作可以实现非常复杂的异步流程控制
CompletableFuture
的命名规则:
xxx()
:表示该方法将继续在已有的线程中执行;xxxAsync()
:表示将异步在线程池中执行。
CompletableFuture
可以指定异步处理流程:
thenAccept()
处理正常结果;exceptional()
处理异常结果;thenApplyAsync()
用于串行化另一个CompletableFuture
;anyOf()
和allOf()
用于并行化多个CompletableFuture
。
ForkJoin(任务拆分)
Fork/Join任务的原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务。
Fork/Join对大数据进行并行求和:
import java.util.Random;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws Exception {
// 创建2000个随机数组成的数组:
long[] array = new long[2000];
long expectedSum = 0;
for (int i = 0; i < array.length; i++) {
array[i] = random();
expectedSum += array[i];
}
System.out.println("Expected sum: " + expectedSum);
// fork/join:
ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
long startTime = System.currentTimeMillis();
Long result = ForkJoinPool.commonPool().invoke(task);
long endTime = System.currentTimeMillis();
System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
}
static Random random = new Random(0);
static long random() {
return random.nextInt(10000);
}
}
class SumTask extends RecursiveTask<Long> {
static final int THRESHOLD = 500;
long[] array;
int start;
int end;
SumTask(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// 如果任务足够小,直接计算:
long sum = 0;
for (int i = start; i < end; i++) {
sum += this.array[i];
// 故意放慢计算速度:
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
}
return sum;
}
// 任务太大,一分为二:
int middle = (end + start) / 2;
System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
SumTask subtask1 = new SumTask(this.array, start, middle);
SumTask subtask2 = new SumTask(this.array, middle, end);
invokeAll(subtask1, subtask2);
Long subresult1 = subtask1.join();
Long subresult2 = subtask2.join();
Long result = subresult1 + subresult2;
System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
return result;
}
}
核心代码SumTask
继承自RecursiveTask
,在compute()
方法中,关键是如何“分裂”出子任务并且提交子任务:
class SumTask extends RecursiveTask<Long> {
protected Long compute() {
// “分裂”子任务:
SumTask subtask1 = new SumTask(...);
SumTask subtask2 = new SumTask(...);
// invokeAll会并行运行两个子任务:
invokeAll(subtask1, subtask2);
// 获得子任务的结果:
Long subresult1 = subtask1.join();
Long subresult2 = subtask2.join();
// 汇总结果:
return subresult1 + subresult2;
}
}
java.util.Arrays.parallelSort(array)
可以进行并行排序,它的原理就是内部通过Fork/Join对大数组分拆进行并行排序,在多核CPU上就可以大大提高排序的速度。
Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。
ForkJoinPool
线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask
或RecursiveAction
。使用Fork/Join模式可以进行并行计算以提高效率。
ThreadLocal(当前线程)
多线程是Java实现多任务的基础,Thread
对象代表一个线程,我们可以在代码中调用Thread.currentThread()
获取当前线程。
Java标准库提供了一个特殊的ThreadLocal
,它可以在一个线程中传递同一个对象。
ThreadLocal
实例通常总是以静态字段初始化如下:
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
它的典型使用方式如下:
void processUser(user) {
try {
threadLocalUser.set(user);
step1();
step2();
} finally {
threadLocalUser.remove();
}
}
实际上,可以把ThreadLocal
看成一个全局Map<Thread, Object>
:每个线程获取ThreadLocal
变量时,总是使用Thread
自身作为key:
Object threadLocalValue = threadLocalMap.get(Thread.currentThread());
因此,ThreadLocal
相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal
关联的实例互不干扰。
最后,特别注意ThreadLocal
一定要在finally
中清除:
try {
threadLocalUser.set(user);
...
} finally {
threadLocalUser.remove();
}
这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal
没有被清除,该线程执行其他代码时,会把上一次的状态带进去。
为了保证能释放ThreadLocal
关联的实例,我们可以通过AutoCloseable
接口配合try (resource) {...}
结构,让编译器自动为我们关闭。
使用的时候,我们借助try (resource) {...}
结构,可以这么写:
try (var ctx = new UserContext("Bob")) {
// 可任意调用UserContext.currentUser():
String currentUser = UserContext.currentUser();
} // 在此自动调用UserContext.close()方法释放ThreadLocal关联对象
这样就在UserContext
中完全封装了ThreadLocal
,外部代码在try (resource) {...}
内部可以随时调用UserContext.currentUser()
获取当前线程绑定的用户名。
ThreadLocal
表示线程的“局部变量”,它确保每个线程的ThreadLocal
变量都是各自独立的;
ThreadLocal
适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递);使用
ThreadLocal
要用try ... finally
结构,并在finally
中清除。
参考:Home - 廖雪峰的官方网站