Java多線程(三)高級concurrent包

背景
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模式可以進行並行計算提高效率
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章