背景
ReentrantLock類
基本使用
基本特點
tryLock使用
總結
ReadWriteLock接口
特點
基本使用
適用條件
總結
Condition接口
基本使用
方法原理
總結
Concurrent集合
基本使用
Blocking集合
總結
Atomic包
使用舉例
總結
ExecutorService線程池
線程池
常用ExecutorService
ScheduledThreadPool
模式
思考
Timer
總結
Future
Callable<T>接口
Future接口
總結
CompletableFuture類
基本使用
優點
用法詳解
1、創建對象
2、設置回調
3、主線程等待
多任務串行執行
多任務並行執行
anyOf
allOf
CompletableFuture方法命名規則
總結
Fork / Join
案例
總結
背景
前面已經提到,Java提供了synchronized/wait/notify等方法來解決多線程競爭和協調問題,但是編寫多線程的同步依然比較困難,步驟很負責。在JDK1.5開始Java提供了一個高級的concurrent包來處理多線程問題
java.util.concurrent
ReentrantLock類
基本使用
使用ReentrantLock可以替代synchronized
class Counter {
// 創建ReentrantLock對象(實現了Lock接口)
final Lock lock = new ReentrantLock();
public void add() {
// 加鎖,要寫在try之前,因爲可能會失敗
lock.lock();
try {
n = n + 1;
} finally {
// 釋放鎖。爲了保證一定能釋放鎖,必須使用try...finally...
lock.unlock();
}
}
}
基本特點
- 可重入鎖,一個線程可多次獲取同一個鎖
- lock()方法可獲取鎖
- tryLock() 方法可嘗試獲取鎖並可指定超時時間
tryLock使用
class Counter {
// 創建ReentrantLock對象
final Lock lock = new ReentrantLock();
public void add() {
// 加鎖,並指定超時時間
if (lock.tryLock(1,TimeUnit.SECONDS)) {
try {
n = n + 1;
} finally {
// 釋放鎖。
lock.unlock();
}
}
}
總結
- ReentrantLock可以替代synchronized
- ReentrantLock獲取鎖更安全
- 必須使用try… finally保證正確獲取鎖和釋放鎖
ReadWriteLock接口
特點
- 只允許一個線程寫入(其他線程既不能寫入也不能讀取)
- 沒有寫入時,多個線程允許同時讀(提高性能)
基本使用
class Counter {
// 創建ReadWriteLock接口的實現類對象
final ReadWriteLock lock = new ReentrantReadWriteLock();
// 獲取讀鎖
final Lock rLock = lock.readLock();
// 獲取寫鎖
final Lock wLock = lock.writeLock();
// 寫方法
public void add() {
// 使用寫鎖來加鎖
wLock.lock();
try {
value += 1;
} finally {
wLock.unlock();
}
}
// 讀方法
public void get() {
// 使用讀鎖來加鎖
rLock.lock();
try {
return this.value;
} finally {
rLock.unlock();
}
}
}
適用條件
- 同一個實例,有大量線程讀取,僅有少數線程修改(比如文章的評論)
總結
使用ReadWriteLock可以提高讀取效率
- ReadWriteLock只允許一個線程寫入
- ReadWriteLock允許多個線程同時讀取
- ReadWriteLock適合讀多寫少的場景
Condition接口
前面已經提到,使用ReentrantLock可以替代synchronized,但是不能直接實現wait和notify的功能,Java提供了一個Condition接口來配合ReentrantLock來實現wait和notify功能。
基本使用
class TaskQueue {
final Lock lock = new ReentrantLock();
// condition對象必須從ReentrantLock獲取
final Condition condition = lock.newCondition();
public String getTask() {
lock.lock();
try {
while (this.queue.isEmpty()) {
// 線程等待方法 await()
// 等同於synchronized的wait()方法
condition.await();
}
return queue.remove();
} finally {
lock.unlock();
}
}
public void addTask(String name) {
lock.lock();
try {
this.queue.add(name);
// 喚醒方法 signal/signalAll
// 等同於synchronized的notify/notifyAll
condition.signalAll();
} finally {
lock.unlock();
}
}
}
方法原理
Condition中的await / signal / signalAll原理和 synchronized中的wait / notifu/ notifyAll一致
- await()會釋放當前鎖,進入等待狀態
- signal()會喚醒某個等待線程
- signalAll()會喚醒所有等待線程
- 喚醒線程從await()返回後需要重新獲得鎖
總結
- Condition可以替代wait / notify
- Condition對象必須從ReentrantLock對象獲取
- ReentrantLock+Condition可以替代synchronized + wait / notify
Concurrent集合
前面的案例中,從一個隊列中讀取數據時,如果沒有數據則需要等待,這種情況下的隊列被稱爲Blocking Queue。Java提供了線程安全的Blocking Queue來簡化開發。
基本使用
class WorkerThread extends Thread {
BlockingQueue<String> taskQueue;
public WorkerThread(BlockingQueue<String> taskQueue) {
this.taskQueue = taskQueue;
}
@Override
public void run() {
while (!isInterrupted()) {
String name;
try {
// 從隊列中取數據,如果沒有數據就會進行等待
name = taskQueue.take();
} catch (InterruptedException e) {
break;
}
String result = "Hello, " + name + "!";
System.out.println(result);
}
}
}
public class Main {
public static void main(String[] args) throws Exception {
BlockingQueue<String> taskQueue = new ArrayBlockingQueue<>(100);
WorkerThread worker = new WorkerThread(taskQueue);
worker.start();
taskQueue.put("Alice");
Thread.sleep(1000);
taskQueue.put("Bob");
Thread.sleep(1000);
taskQueue.put("Tim");
Thread.sleep(1000);
worker.interrupt();
worker.join();
System.out.println("END");
}
}
Blocking集合
總結
使用concurrent提供的Blocking集合可以簡化多線程編程
- 多線程同時訪問Blocking集合是安全的
- 儘量使用JDK提供的concurrent集合,避免自己編寫同步代碼
Atomic包
java.util.concurrent.atomic提供了一組原子類型操作:
- AtomicInteger
- int addAndGet(int delta)
- int incrementAndGet()
- int get()
- int compareAndSet(int expect, int update)
Atomic類可以實現
- 無鎖(lock-free)實現的線程安全(thread-safe)訪問
使用舉例
class IdGenerator {
AtomicLong var = new AtomicLong(0);
/*
多線程安全的ID序列生成器
*/
public long getNextId() {
// 加1並返回值
return var.incrementAndGet();
}
}
總結
使用java.util.atomic提供的原子操作可以簡化多線程編程:
- AtomicInteger / AtomicLong / AtomicIntegerArray 等
- 原子操作實現了無鎖的線程安全
- 適用於計數器, 累加器等
ExecutorService線程池
創建線程會消耗系統資源,而且頻繁創建和銷燬線程需要消耗大量時間,如果能夠複用線程將大大提高運行效率,降低資源消耗,因此線程池應運而生。
線程池
- 線程池維護若干個線程,處於等待狀態
- 如果有新任務,就分配一個空閒線程執行
- 如果所有線程都處於忙碌狀態,新任務放入隊列等待
JDK提供了ExecutorService接口表示線程池
ExecutorService executor = Executors.newFixedThreadPool(4);// 固定大小的線程池
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
...
常用ExecutorService:
- FixedThreadPool:線程數固定
- CachedThreadPool:線程數根據任務數動態調整
- SingleThreadExecutor:僅單線程執行,只創建一個線程
// 固定線程數的線程池
ExecutorService executor1 = Executors.newFixedThreadPool(10);
// 線程數根據任務數量動態調整的線程池
ExecutorService executor2 = Executors.newCachedThreadPool();
// 單線程的線程池
ExecutorService executor3 = Executors.newSingleThreadExecutor();
雖然CachedThreadPool不可可以設置固定的線程數,但是查看其源碼
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
其中ThreadPoolExecutor方法的第二個參數爲線程數量,因此可以通過創建這個對象來創建固定線程數量的線程池
ExecutorService executor = new ThreadPoolExecutor(0, 5,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
ScheduledThreadPool
可以定期反覆執行一個任務的線程池
模式
- Fixed Rate 定期多久執行一次任務,不管任務執行多久
// 創建一個定期執行的線程池,並指定維持的線程數量
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
// 參數一:執行的任務
// 參數二:多長時間後開始執行任務
// 參數三:每隔多久執行一次任務
// 參數四:時間單位
// 1秒後開始執行任務,而且每3秒執行一次
executor.scheduleAtFixedRate(new Thread(), 1, 3, TimeUnit.SECONDS);
- Fixed Delay 一次任務結束後間隔多久執行下一次任務
// 創建一個定期執行的線程池,並指定維持的線程數量
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
// 參數一:執行的任務
// 參數二:多長時間後開始執行任務
// 參數三:兩次任務的間隔
// 參數四:時間單位
// 1秒後開始執行任務,上一次任務結束後3秒執行下一次任務
executor.scheduleWithFixedDelay(new Task("002"), 1, 3, TimeUnit.SECONDS);
思考
- FixedRate模式下,如果任務執行時間過長,後續任務會不會併發執行?(不會)
- 如果任務拋出了異常,後續任務是否繼續執行?(會)
Timer
java.util.Timer
- 一個Timer對應一個Thread
- 必須在主線程結束時調用Timer.cancel()
總結
- JDK提供了ExecutorService實現了線程池功能
- 線程池內部維護一組線程,可以高效執行大量小任務
- Executors提供了靜態方法創建不同類型的ExecutorService
- 必須調用shutdown()關閉ExecutorService
- ScheduledThreadPool可以定期調度多個任務
Future接口
Callable<T>接口
Callable和Runnable相似,但是Runnable沒有返回值,Callable有返回值,所以實現Callable時需要指定範型,並重寫call()方法指定返回值類型
// Callable接口源碼
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
// 實現Callable接口
class Task implements Callable<String> {
@Override
public String call() throws Exception {
return "Hello";
}
}
Future接口
表示一個任務未來可能會返回的結果
- get() 獲取執行結果
- get(long timeout, TimeUnit unit) 指定最大等待結果時間
- cancel(boolean mayInterruptIfRunning) 中斷異步任務的執行
- isDone() 判斷異步任務是否完成
Callable<String> Task = new Task();
ExecutorService executor = Executors.newFixedThreadPool(4);
// 接收任務執行結果對象
Future<String> future = executor.submit(task);
// 獲取具體的返回值,如果任務還沒結束會阻塞,一直到任務執行結束返回結果
String result = future.get();
Runnable VS Callable
總結
- 提交Callable任務,可以獲得一個Future對象
- 可以用Furure在將來某個時刻獲取結果
CompletableFuture類
使用Future獲取異步執行結果的方法:
- get():阻塞方法
- isDone():判斷任務是否完成來輪詢
以上兩種方式效率都比較低,JDK提供了一個CompletableFuture類,可以通過設置回調方法的形式在任務結束後來獲取結果。
基本使用
// 創建CompletableFuture對象,並指定範型(結果)類型
CompletableFuture<String> cf = ...
// thenAccept方法設置任務正常運行完成後的操作
cf.thenAccept(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println("異步任務正常運行結果 : " + s);
}
});
// exceptionally方法設置任務發生異常的操作
cf.exceptionally(new Function<Throwable, String>() {
@Override
public String apply(Throwable throwable) {
System.out.println("運行發生異常 : " + throwable.getLocalizedMessage());
return null;
}
});
/************************* 分割線 ******************************/
// JDK1.8函數式編程寫法(瞭解)
cf.thenAccept((result) -> {
System.out.println("異步任務正常運行結果 : " + s);
});
cf.exceptionally((t) -> {
System.out.println("運行發生異常");
return null;
});
優點
- 異步任務結束時,會自動回調某個對象的方法
- 異步任務出錯時,會自動回調某個對象的方法
- 主線程設置好回調後,不再關心異步任務的執行
用法詳解
1、創建對象
CompletableFuture<String> cf = CompletableFuture.supplyAsync(new ASupplier());
// Supplier接口源碼
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
通過CompletableFuture的supplyAsync方法創建對象,需要傳入一個Supplier實例對象,可以理解爲任務對象,重寫get()方法執行具體任務。
2、設置回調
// thenAccept方法設置任務正常運行完成後的操作
cf.thenAccept(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println("異步任務正常運行結果 : " + s);
}
});
// exceptionally方法設置任務發生異常的操作
cf.exceptionally(new Function<Throwable, String>() {
@Override
public String apply(Throwable throwable) {
System.out.println("運行發生異常 : " + throwable.getLocalizedMessage());
return null;
}
});
3、主線程等待
// 注意:主線程結束時默認使用的Executor會關閉,所以要使用join方法等待任務執行完畢
cf.join();
多任務串行執行
// 實例1對象
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(Supplier1);
// cf1通過thenApplyAsync方法獲取實例2對象
CompletableFuture<Float> cf2 = cf1.thenApplyAsync(Supplier2);
// cf2通過thenApplyAsync方法獲取實例3對象
CompletableFuture<Integer> cf3 = cf2.thenApplyAsync(Supplier3);
// 通過實例3獲取結果
cf3.thenAccept(實例3運行結果操作);
cf3.exceptionally(實例3運行異常操作);
// 多個任務都執行完後再獲取結果:cf1執行完 --> cf2執行完 --> cf3執行完 --> 獲取結果
多任務並行執行
anyOf
多個任務中只要有一個任務結束就獲取結果
// 創建兩個任務實例
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(Supplier1);
CompletableFuture<Float> cf2 = CompletableFuture.supplyAsync(Supplier2);
// 通過anyOf轉化爲新的實例,注意修改範型類型
CompletableFuture<Object> cf3 = CompletableFuture.anyOf(cf1, cf2);
// 通過新的實例獲取結果
cf3.thenAccept(運行結果操作);
cf3.join();
allOf
多個任務全部執行結束後纔會獲取結果
// 創建兩個任務實例
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(Supplier1);
CompletableFuture<Float> cf2 = CompletableFuture.supplyAsync(Supplier2);
// 通過allOf轉化爲新的實例,範型類型只能是Void
CompletableFuture<Void> cf3 = CompletableFuture.allOf(cf1, cf2);
// 通過新的實例獲取結果
cf3.thenAccept(運行結果操作);
cf3.join();
CompletableFuture方法命名規則
- xxx():表示繼續在已有的線程中執行
- xxxAsync():用Executor的新線程執行
總結
CompletableFuture對象可以指定異步處理流程:
- thenAccept() 處理正常結果
- exceptionally() 處理異常結果
- thenApplyAsync() 用於串行化另一個CompletableFuture對象
- anyOf / allOf 用於並行化多個CompletableFuture
Fork / Join
Fork/Join框架是Java7提供的一個用於並行執行任務的框架, 是一個把大任務分割成若干個小任務,最終彙總每個小任務結果後得到大任務結果的框架。使用工作竊取(work-stealing)算法,主要用於實現“分而治之”。
案例
// 任務類必須繼承自RecursiveTask(有返回值) / RecursiveAction(沒有返回值)
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(2);
} 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;
}
}
public class ForkJoinTaskSample {
public static void main(String[] args) throws Exception {
// 創建1000個隨機數組成的數組:
long[] array = new long[1000];
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);
}
}
總結
- Fork / Join是一種基於“分治”的算法:
- 分解任務 + 合併結果
- ForkJoinPool線程池可以把一個大任務分拆成小任務並行執行
- 任務類必須繼承自RecursiveTask(有返回值) / RecursiveAction(無返回值)
- 使用Fork / Join模式可以進行並行計算提高效率