JAVA併發編程-2-線程併發工具類
上一章:看這裏JAVA併發編程-1-線程基礎
本章主要介紹java.util.concurrent下給我們提供的線程併發工具類的作用和使用場景。
一、Fork/Join
1、分而治之與工作密取
Fork/Join框架體現了分而治之的思想,就是在必要的情況下,將一個大任務進行拆分(fork)成若干個小任務(拆到不可再拆時),再將一個個小任務運算的結果進行join彙總。
二叉查找樹、快速排序算法,Hadoop中的map-reduce都是典型的分而治之思想,相信大家也並不難理解。
分完之後的結果可以交給不同的線程去執行,並且平均分配。但是並不能保證每個線程分到的任務都同時執行完,也就是說有的線程執行的快,有的執行的慢,而我們期望最終同時得到的是一個join後的結果,所以讓執行的快的線程去執行的慢的線程的任務隊列的尾部“偷”一個任務來幫它執行,執行的結果還是交給原線程來join,這種機制就是工作密取。
2、使用標準範式
abstract class ForkJoinTask< V> 是forkjoin的基礎Task類,他有兩個實現:用於同步方法的有返回值的RecursiveTask< V> 和用於異步方法的沒有返回值的 RecursiveAction,我們自己的方法要繼承自它們,實現對應的compute()方法。
使用流程如下:
3、Fork/Join的同步用法
下面我們用Fork/Join框架來將10000個數相加,做到每個最終任務只加1000個數,通過Fork/Join體會一下用法。
public class SumArray1 {
public static class MakeArray {
//數組長度
public static final int ARRAY_LENGTH = 10000;
public static int[] makeArray() {
//new一個隨機數發生器
Random r = new Random();
int[] result = new int[ARRAY_LENGTH];
for (int i = 0; i < ARRAY_LENGTH; i++) {
//用隨機數填充數組
result[i] = r.nextInt(ARRAY_LENGTH * 3);
}
return result;
}
}
public static class SumTask extends RecursiveTask<Integer> {
//定義我們最終拆分的任務的數組的最大長度
private final static int THRESHOLD = MakeArray.ARRAY_LENGTH / 10;
private int[] nums;
private Integer fromIndex;
private Integer endIndex;
public SumTask(int[] nums, Integer fromIndex, Integer endIndex) {
this.nums = nums;
this.fromIndex = fromIndex;
this.endIndex = endIndex;
}
@Override
protected Integer compute() {
//小於最大限制,說明已經是最終任務,就去執行相加邏輯
if (endIndex - fromIndex <= THRESHOLD) {
int count = 0;
for (int i = fromIndex; i <= endIndex; i++) {
//模擬1ms,因爲實際執行太快了
SleepTools.ms(1);
count = count + nums[i];
}
return count;
} else {
//如果沒達到,就要繼續拆分
int mid = (endIndex - endIndex) / 2;
SumTask left = new SumTask(nums, fromIndex, mid);
SumTask right = new SumTask(nums, fromIndex, mid);
invokeAll(left, right);
return left.join() + right.join();
}
}
}
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
int[] nums = MakeArray.makeArray();
SumTask sumTask = new SumTask(nums, 0, nums.length - 1);
long start = System.currentTimeMillis();
pool.invoke(sumTask);
System.out.println("Task is Running.....");
System.out.println("The count is " + sumTask.join()
+ " spend time:" + (System.currentTimeMillis() - start) + "ms");
}
}
在計算的過程中,我們sleep了一秒,主要是爲了模擬較複雜的計算過程。因爲實際上你會發現,如果單純的加10000個數,單線程for循環是非常的快的。這個很好理解,一直強調的是,多線程開發是有本身的線程上下文切換等的資源消耗要考慮的,甚至本身消耗的資源和時間是比執行的任務佔用的資源和時間還要大,這個時候多線程的執行並不能帶來性能上的提升。所以我們在使用多線程進行開發時一定要分析清楚實際的情況,再決定是否要使用多線程。
4、Fork/Join的異步用法
異步方法很好理解,就是不需要子線程的返回值或者子線程執行過程中需要主線程去完成其它的工作。
假設我們要尋找磁盤D上的所有.txt文件並且將文件名輸出到控制檯,不言而喻的是,一個文件夾下既會有若干文件,又會有文件夾,文件就直接校驗它的後綴名,文件夾時將任務再拆分。來看一下具體實現:
public class FindDirsFiles extends RecursiveAction {
private File path;//當前任務需要搜尋的目錄
public FindDirsFiles(File path) {
this.path = path;
}
public static void main(String[] args) {
try {
// 用一個 ForkJoinPool 實例調度總任務
ForkJoinPool pool = new ForkJoinPool();
FindDirsFiles task = new FindDirsFiles(new File("D:/"));
pool.execute(task);//異步調用
System.out.println("Task is Running......");
Thread.sleep(1);
int otherWork = 0;
for (int i = 0; i < 100; i++) {
otherWork = otherWork + i;
}
System.out.println("Main Thread done sth......,otherWork=" + otherWork);
task.join();//阻塞的方法
System.out.println("Task end");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void compute() {
List<FindDirsFiles> subTasks = new ArrayList<>();
File[] files = path.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
//文件夾需要再拆分
subTasks.add(new FindDirsFiles(file));
} else {
//遇到文件,檢查
if (file.getAbsolutePath().endsWith("txt")) {
System.out.println("文件:" + file.getAbsolutePath());
}
}
}
if (!subTasks.isEmpty()) {
//<T extends ForkJoinTask<?>> Collection<T> invokeAll(Collection<T> tasks)
for (FindDirsFiles subTask : invokeAll(subTasks)) {
subTask.join();//等待子任務執行完成
}
}
}
}
}
在主線程中調用了join方法進行等待,如果主線程太快執行完就看不到子線程執行結果了。
二、CountDownLatch
作用:一組線程等待其他的線程完成工作以後再執行,加強版join。
怎麼理解呢?
首先簡單來說它就是一個計數器
最主要的就三個方法
構造方法:public CountDownLatch(int count) 創建一個計數器,參數是計數器初始值
減數值方法:countDown() 給計數器的值減1
等待方法:await() 調用後開始等待,直到計數器值減爲0接着執行
看一個例子,略複雜:
/**
* 類說明:演示CountDownLatch,有5個初始化的線程,6個扣除點,
* 扣除完畢以後,主線程和業務線程才能繼續自己的工作
*/
public class UseCountDownLatch {
static CountDownLatch latch = new CountDownLatch(6);
//初始化線程(只有一步,有4個)
private static class InitThread implements Runnable {
@Override
public void run() {
System.out.println("Thread_" + Thread.currentThread().getId()
+ " ready init work......");
latch.countDown();//初始化線程完成工作了,countDown方法只扣減一次;
for (int i = 0; i < 2; i++) {
System.out.println("Thread_" + Thread.currentThread().getId()
+ " ........continue do its work");
}
}
}
//業務線程
private static class BusiThread implements Runnable {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 3; i++) {
System.out.println("BusiThread_" + Thread.currentThread().getId()
+ " do business-----");
}
}
}
public static void main(String[] args) throws InterruptedException {
//單獨的初始化線程,初始化分爲2步,需要扣減兩次
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
SleepTools.ms(1);
System.out.println("Thread_" + Thread.currentThread().getId()
+ " ready init work step 1st......");
latch.countDown();//每完成一步初始化工作,扣減一次
System.out.println("begin step 2nd.......");
SleepTools.ms(1);
System.out.println("Thread_" + Thread.currentThread().getId()
+ " ready init work step 2nd......");
latch.countDown();//每完成一步初始化工作,扣減一次
}
});
thread1.start();
Thread thread2 = new Thread(new BusiThread());
thread2.start();
for (int i = 0; i <= 3; i++) {
Thread thread = new Thread(new InitThread());
thread.start();
}
latch.await() ;
System.out.println("Main do ites work........");
}
}
在上面的代碼中創建了CountDownLatch(6)的初始值爲6
thread2和main線程都調用了latch.await() 方法
for循環中創建了4個線程,每個線程調用一次countDown()方法
thread1線程中調用了2次countDown()方法
期望的執行流程:main和thread2等待其它5個線程執行6次countDown()將值減爲0後接着執行
值得注意的幾點:
1,一個線程中可以執行多次countDown()方法
2,所有的一組等待線程是靠另外的線程控制是否繼續執行的
3,另外有await(long timeout, TimeUnit unit)方法可以等待某個時間後不再等待
三、CyclicBarrier
作用:讓一組線程達到某個屏障,被阻塞,一直到組內最後一個線程達到屏障時,屏障開放,所有被阻塞的線程會繼續運行
同樣作爲一個計數器
只有兩個方法:
構造方法:CyclicBarrier(int parties) 傳入計數器初始值
等待方法:await() 執行到此對初始值減1並等待,直到初始值爲0,接着執行
CyclicBarrier跟CountDownLatch最大的不同是,它是一組線程內的屏障或者計數器,不能被該組線程外的其他線程的執行影響到
構造方法CyclicBarrier(int parties, Runnable barrierAction),屏障開放或者說計數器減爲0,barrierAction定義的任務會執行
來看例子:
public class UseCyclicBarrier {
private static CyclicBarrier barrier
= new CyclicBarrier(5, new CollectThread());
private static ConcurrentHashMap<String, Long> resultMap
= new ConcurrentHashMap<>();//存放子線程工作結果的容器
public static void main(String[] args) {
for (int i = 0; i <= 4; i++) {
Thread thread = new Thread(new SubThread());
thread.start();
}
}
//負責屏障開放以後的工作
private static class CollectThread implements Runnable {
@Override
public void run() {
StringBuilder result = new StringBuilder();
for (Map.Entry<String, Long> workResult : resultMap.entrySet()) {
result.append("[" + workResult.getValue() + "]");
}
System.out.println(" the result = " + result);
System.out.println("do other business........");
}
}
//工作線程
private static class SubThread implements Runnable {
@Override
public void run() {
long id = Thread.currentThread().getId();//線程本身的處理結果
resultMap.put(Thread.currentThread().getId() + "", id);
Random r = new Random();//隨機決定工作線程的是否睡眠
try {
if (r.nextBoolean()) {
Thread.sleep(2000 + id);
System.out.println("Thread_" + id + " ....do something ");
}
System.out.println(id + "....is await");
barrier.await();
Thread.sleep(1000 + id);
System.out.println("Thread_" + id + " ....do its business ");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
new CyclicBarrier(5, new CollectThread()創建了一個CyclicBarrier,for循環中創建了5個SubThread,每個線程執行到 barrier.await()時會將CyclicBarrier的值減1並等待,減到0後會執行CollectThread任務和各個線程後面的任務
四、Semaphore信號量
作用:控制同時訪問某個特定資源的線程數量,用在流量控制
用一個獲取線程池連接的例子來感受下用法:
ublic class DBPoolSemaphore {
private final static int POOL_SIZE = 10;
private final Semaphore useful;//useful表示可用的數據庫連接,useless表示已用的數據庫連接
public DBPoolSemaphore() {
this.useful = new Semaphore(POOL_SIZE);
}
//存放數據庫連接的容器
private static LinkedList<Connection> pool = new LinkedList<Connection>();
//初始化池
static {
for (int i = 0; i < POOL_SIZE; i++) {
pool.addLast(SqlConnectImpl.fetchConnection());
}
}
/*歸還連接*/
public void returnConnect(Connection connection) throws InterruptedException {
if (connection != null) {
System.out.println("當前有" + useful.getQueueLength() + "個線程等待數據庫連接!!"
+ "可用連接數:" + useful.availablePermits());
synchronized (pool) {
pool.addLast(connection);
}
useful.release();
}
}
/*從池子拿連接*/
public Connection takeConnect() throws InterruptedException {
//如果拿不到,會阻塞,直到拿到爲止
useful.acquire();
Connection conn;
synchronized (pool) {
conn = pool.removeFirst();
}
return conn;
}
private static DBPoolSemaphore dbPool = new DBPoolSemaphore();
//業務線程
private static class BusiThread extends Thread {
@Override
public void run() {
Random r = new Random();//讓每個線程持有連接的時間不一樣
long start = System.currentTimeMillis();
try {
Connection connect = dbPool.takeConnect();
System.out.println("Thread_" + Thread.currentThread().getId()
+ "_獲取數據庫連接共耗時【" + (System.currentTimeMillis() - start) + "】ms.");
SleepTools.ms(100 + r.nextInt(100));//模擬業務操作,線程持有連接查詢數據
System.out.println("查詢數據完成,歸還連接!");
dbPool.returnConnect(connect);
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
Thread thread = new BusiThread();
thread.start();
}
}
}
初始化一個大小爲10的Semaphore,takeConnect中用到acquire(),它會阻塞直到拿到連接,returnConnect中用到**release()**將一個數量歸還給Semaphore
執行結果來看,最開始有10個線程馬上拿到連接,後面的會阻塞等待Semaphore的有值
五、Exchanger
作用:用於兩個線程間的數據交換
看一段僞代碼:
private static final Exchanger<Set<String>> exchange
= new Exchanger<Set<String>>();
public static void main(String[] args) {
//第一個線程
new Thread(new Runnable() {
@Override
public void run() {
Set<String> setA = new HashSet<String>();//存放數據的容器
try {
/*添加數據
* set.add(.....)
* */
setA = exchange.exchange(setA);//交換set
/*處理交換後的數據*/
} catch (InterruptedException e) {
}
}
}).start();
//第二個線程
new Thread(new Runnable() {
@Override
public void run() {
Set<String> setB = new HashSet<String>();//存放數據的容器
try {
/*添加數據
* set.add(.....)
* set.add(.....)
* */
setB = exchange.exchange(setB);//交換set
/*處理交換後的數據*/
} catch (InterruptedException e) {
}
}
}).start();
}
exchange(value)方法用於交換數據信息。
exchange使用較少。簡單舉個使用場景幫助理解,生產者線程生成了一個list用於給消費者線程使用,則可以在生產者生成數據結束後使用exchange做兩個線程間的數據交換,消費者拿到數據,處理後再用exchange將結果交給生產者。
本帖介紹了java.util.concurrent下的線程併發工具類,關於它們的使用相信大家有了初步的瞭解,這些工具類可以通過自己的使用的來逐漸掌握它們,來解決我們的業務問題。
關於他們的源碼和實現機制本章沒有涉及到,因爲它們的原理基本都是基於AQS,關於AQS這座java併發編程的高山會在後面單獨開帖來講解,到時候也會結合本章的工具類源碼來理解。