標籤:事件驅動 建立線程 getname this fir cto com 返回結果 some
JAVA concurrent
本文主要講解Java並發相關的內容,包括鎖、訊號量、堵塞隊列、線程池等主要內容。
並發的優點和缺點
在講述怎麼利用多線程的情況下,我們先看一下採用多線程並發的優缺點。
優點
提高資源使用率
如讀取一個目錄下的所有檔案,如果採用單執行緒模式,則從磁碟讀取檔案的時候,大部分CPU用於等待磁碟去讀取資料。如果是採用多線程並發執行,則CPU可以在等待IO的時候去做其他的事情,以提高CPU的使用率,減少資源的浪費。
程式響應速度好
單執行緒模式下,假設一個http請求需要佔用大量的時間來處理,則其他的請求無法發送請求給服務端。而多線程模式下,監聽線程把請求傳遞給工作者線程,然後立刻返回去監聽,可以去接收新的請求,而工作者線程則能夠處理這個請求並發送一個回複給用戶端。明顯響應速度比單執行緒模式要好得多。
缺點
- 程式設計複雜度
多線程情況下,需要考慮線程間的通訊、共用資源的訪問,相對而言要比單線程程式負責一些。
- 環境切換開銷大
當CPU從執行一個線程切換到執行另外一個線程的時候,它需要先儲存當前線程的本地的資料,程式指標等,然後載入另一個線程的本機資料,程式指標等,最後才開始執行。這種切換稱為“環境切換”。CPU會在一個上下文中執行一個線程,然後切換到另外一個上下文中執行另外一個線程。尤其是當線程數量較多時,這種開銷很明顯。
資源消耗
線程在啟動並執行時候需要從電腦裡面得到一些資源。除了CPU,線程還需要一些記憶體來維持它本地的堆棧。它也需要佔用作業系統中一些資源來管理線程
並行存取模型並發系統可以採用多種並發編程模型來實現。並行存取模型指定了系統中的線程如何通過協作來完成分配給它們的作業。不同的並行存取模型採用不同的方式拆分作業,同時線程間的協作和互動方式也不相同。
並行工作者在並行工作者模型中,委派者(Delegator)將傳入的作業分配給不同的工作者。每個工作者完成整個任務。工作者們並行運作在不同的線程上,甚至可能在不同的CPU上。
假設電商系統中的秒殺活動採用了並行工作者模型,訂單->財務->倉儲->物流,工作者A拿到訂單請求,然後負責支付流程,查詢倉儲情況,直到發貨。
在Java應用系統中,並行工作者模型是最常見的並行存取模型,java.util.concurrent包中的許多並發工具 + 生產力都是設計用於這個模型的。
優點
易於理解,可以添加更多的工作者來提高系統的並行度
缺點
- 共用狀態可能會很複雜
在上面的電商系統中,由於共用的工作者經常需要訪問一些共用資料,無論是記憶體中的或者共用資料庫中的。
在等待訪問共用資料結構時,線程之間的互相等待將會丟失部分並行性。許多並發資料結構是阻塞的,意味著在任何一個時間只有一個或者很少的線程能夠訪問。這樣會導致在這些共用資料結構上出現競爭狀態。在執行需要訪問共用資料結構部分的代碼時,高競爭基本上會導致執行時出現一定程度的序列化。
- 無狀態的工作者
每次都重讀需要的資料,將會導致速度變慢,特別是狀態儲存在外部資料庫中的時候。
- 任務順序是不確定的
作業執行順序是不確定的,無法保證哪個作業最先或者最後被執行。如A先下單,B後下單,不根據時間進行商務邏輯的判斷,不能有可能B先於A收到貨。
流水線模式
流水線模式中,每個工作者只負責作業中的部分工作。當完成了自己的這部分工作時工作者會將作業轉寄給下一個工作者。每個工作者在自己的線程中運行,並且不會和其他工作者共用狀態。也稱反應器系統,或事件驅動系統。
以秒殺為例,工作者A執行訂單的處理,工作者B執行支付,工作者C檢查倉儲,工作者D負責物流,分工明確,各司其職。
在實際應用中,作業有可能不會沿著單一流水線進行。由於大多數系統可以執行多個作業,作業從一個工作者流向另一個工作者取決於作業需要做的工作。在實際中可能會有多個不同的虛擬流水線同時運行。
作業甚至也有可能被轉寄到超過一個工作者上並發處理。比如說,作業有可能被同時轉寄到作業執行器和作業日誌器。說明了三條流水線是如何通過將作業轉寄給同一個工作者(中間流水線的最後一個工作者)來完成作業:
優點
無需共用的狀態
工作者之間無需共用狀態,意味著實現的時候無需考慮所有因並發訪問共用對象而產生的並發性問題
較好的硬體整合
單線程代碼在整合底層硬體的時候往往具有更好的優勢。首先,當能確定代碼只在單線程模式下執行的時候,通常能夠建立更最佳化的資料結構和演算法。
合理的作業順序
基於流水線並行存取模型實現的並發系統,在某種程度上是有可能保證作業的順序的。作業的有序性使得它更容易地推出系統在某個特定時間點的狀態
缺點
編寫難度大
好在有一些平台架構可以直接使用,如Akka,Node.JS等
跟蹤困難
流水線並行存取模型最大的缺點是作業的執行往往分布到多個工作者上,並因此分布到項目中的多個類上。這樣導致在追蹤某個作業到底被什麼代碼執行時變得困難。
函數式並行
函數式並行的基本思想是採用函數調用實現程式。函數可以看作是代理人agents或者actor,函數之間可以像流水線模型(反應器或者事件驅動系統)那樣互相發送訊息。
函數都是通過拷貝來傳遞參數的,所以除了接收函數外沒有實體可以操作資料。這對於避免共用資料的競態來說是很有必要的。同樣也使得函數的執行類似於原子操作。每個函數調用的執行獨立於任何其他函數的調用。
Runnable、Callable、Future、Thread、FutureTask
Java並發中主要以Runnable、Callable、Future三個介面作為基礎。
Runnable
執行個體想要被線程執行,可以通過實現Runnable介面。
。通過執行個體化某個 Thread 執行個體並將自身作為運行目標,就可以運行實現 Runnable 的類而無需建立 Thread 的子類。大多數情況下,如果只想重寫 run() 方法,而不重寫其他 Thread 方法,那麼應使用 Runnable 介面。這很重要,因為除非程式員打算修改或增強類的基本行為,否則不應為該類建立子類。
Callable
Callable 介面類似於 Runnable,兩者都是為那些其執行個體可能被另一個線程執行的類設計的。但是 Runnable不會返回結果,並且無法拋出經過檢查的異常。
Future
Future 表示非同步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並擷取計算的結果。計算完成後只能使用 get 方法來擷取結果,如有必要,計算完成前可以阻塞此方法。取消則由 cancel 方法來執行。還提供了其他方法,以確定任務是正常完成還是被取消了。一旦計算完成,就不能再取消計算。如果為了可取消性而使用 Future 但又不提供可用的結果,則可以聲明 Future<?> 形式類型、並返回 null 作為底層任務的結果。
主要方法如下:
cancel(boolean mayInterruptIfRunning)
試圖取消對此任務的執行。
get()
如有必要,等待計算完成,然後擷取其結果。
get(long timeout, TimeUnit unit)
如有必要,最多等待為使計算完成所給定的時間之後,擷取其結果(如果結果可用)。
isCancelled()
如果在任務正常完成前將其取消,則返回 true。
isDone()
如果任務已完成,則返回 true。
Thread線程的建立
在Java中,我們有2個方式建立線程:
- 通過直接繼承
thread類,然後覆蓋run()方法。
構建一個實現Runnable介面的類, 然後建立一個thread類對象並傳遞Runnable對象作為構造參數
線程的運行流程我們在主線程中建立5個子線程,每個子線程通過建構函式初始化number的值,來實現1-5內的乘法表:
package com.molyeo.java.concurrent; public class ThreadTest { public static void main(String[] args) { System.out.println("main thread start"); for (int i = 1; i <= 5; i++) { Calculator calculator = new Calculator(i); Thread thread = new Thread(calculator); thread.start(); } System.out.println("main thread end"); } } class Calculator implements Runnable { private int number; public Calculator(int number) { this.number = number; } @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.printf("%s: %d * %d = %d \n", Thread.currentThread().getName(), number, i, i * number); } } }
程式輸出如下:
main thread startThread-0: 1 * 1 = 1 Thread-0: 1 * 2 = 2 Thread-0: 1 * 3 = 3 Thread-0: 1 * 4 = 4 Thread-0: 1 * 5 = 5 Thread-4: 5 * 1 = 5 Thread-4: 5 * 2 = 10 Thread-4: 5 * 3 = 15 Thread-4: 5 * 4 = 20 Thread-4: 5 * 5 = 25 Thread-3: 4 * 1 = 4 Thread-3: 4 * 2 = 8 Thread-2: 3 * 1 = 3 Thread-2: 3 * 2 = 6 Thread-2: 3 * 3 = 9 Thread-2: 3 * 4 = 12 Thread-1: 2 * 1 = 2 Thread-1: 2 * 2 = 4 Thread-1: 2 * 3 = 6 main thread endThread-1: 2 * 4 = 8 Thread-3: 4 * 3 = 12 Thread-3: 4 * 4 = 16 Thread-3: 4 * 5 = 20 Thread-2: 3 * 5 = 15 Thread-1: 2 * 5 = 10
在Java中,每個應用程式最少有一個執行線程,運行程式時,JVM負責調用main()方法的執行線程。
當全部的非守護線程執行結束時,Java程式才算結束。從輸出中也可以看到,主程式輸出main thread end後,其他程式還是繼續執行,直到執行結束。
需要注意的是,如果某個線程調用System.exit()指示終結程式,那麼全部的線程都會結束執行。
線程中斷、睡眠、設定優先權
下面的樣本中,NumberGenerator中首先建立numberGenetorThread線程,並設定優先權,啟動線程後,一直迴圈運行,列印出number的值,直到5毫秒後主線程調用interrupt()方法讓其中斷,numberGenetorThread線程其跳出while迴圈。首次調用方法isInterrupted()傳回值為true,表示線程已中止。
需要注意的是,interrupt()方法測試當前線程是否已經中斷,線程的中斷狀態也由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用將返回 false。大家可以開啟下面的注釋去測試。
package com.molyeo.java.concurrent;/** * Created by zhangkh on 2018/8/23. */public class ThreadTest2 { public static void main(String[] args) throws InterruptedException { Thread numberGenetorThread = new NumberGenerator(0); numberGenetorThread.setPriority(Thread.MAX_PRIORITY); numberGenetorThread.start(); Thread.sleep(5); numberGenetorThread.interrupt(); System.out.println("first interrupt,isInterrupted=" + numberGenetorThread.isInterrupted());// Thread.sleep(5);// numberGenetorThread.interrupt();// System.out.println("second interrupt,isInterrupted=" + numberGenetorThread.isInterrupted()); }}class NumberGenerator extends Thread { private int number; public NumberGenerator(int number) { this.number = number; } @Override public void run() { while (!isInterrupted()) { System.out.println("number is " + number); number++; } System.out.println("NumberGenerator thread,isInterrupted= " + this.isInterrupted()); }}
程式部分輸出如下:
number is 96number is 97NumberGenerator thread,isInterrupted= truefirst interrupt,isInterrupted=true
ThreadLocal
定義和作用
ThreadLocal稱執行緒區域變數,並不是為瞭解決共用對象的多線程訪問的問題的,因為如果ThreadLocal.set()放進去的本來就是多線程共用的同一個對象的話,線程通過ThreadLocal.get()方法得到的還是共用對象本身,依舊存在並發訪問的問題。其是每個線程所單獨持有的,主要是提供了保持對象的方法和避免參數傳遞,以方便對象的訪問。
- 每個線程中都有一個自己的
ThreadLocalMap類對象,可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象。
- 將一個共用的
ThreadLocal靜態執行個體作為key,將不同對象的引用儲存到不同線程的ThreadLocalMap中,然後線上程執行的各處通過這個靜態ThreadLocal執行個體的get()方法取得自己線程儲存的那個對象,避免了將這個對象作為參數傳遞的麻煩。
程式運行時,每個線程都保持對其線程局部變數副本的隱式引用,只要線程是活動的並且 ThreadLocal 執行個體是可訪問的;線上程消失之後,其線程局部執行個體的所有副本都會被記憶體回收(除非存在對這些副本的其他引用)。
使用樣本
如下我們建立ThreadLocal的執行個體stringLocal,分別在主線程和子線程中設定其值為當前線程名字。查看輸出的結果可以看到線程間彼此不干擾,各自輸出自己設定的值。
package com.molyeo.java.concurrent;/** * Created by zhangkh on 2018/8/24. */public class ThreadLocalDemo { public static void main(String[] args) throws InterruptedException { ThreadLocal<String> stringLocal = new ThreadLocal<String>(); stringLocal.set(Thread.currentThread().getName()); System.out.println(String.format("threadName=%10s,threadLocal valaue=%10s",Thread.currentThread().getName(),stringLocal.get()) ); Thread thread1 = new Thread() { public void run() { stringLocal.set(Thread.currentThread().getName()); System.out.println(String.format("threadName=%10s,threadLocal valaue=%10s",Thread.currentThread().getName(),stringLocal.get()) ); } }; thread1.start(); thread1.join(); System.out.println(String.format("threadName=%10s,threadLocal valaue=%10s",Thread.currentThread().getName(),stringLocal.get()) ); }}
程式輸出如下:
threadName= main,threadLocal valaue= mainthreadName= Thread-0,threadLocal valaue= Thread-0threadName= main,threadLocal valaue= main
源碼實現
ThreadLocal有3個成員變數
private final int threadLocalHashCode = nextHashCode();private static AtomicInteger nextHashCode = new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);}
nextHashCode是ThreadLocal的靜態變數,HASH_INCREMENT是靜態常量,只有threadLocalHashCode是ThreadLocal執行個體的變數。
在建立ThreadLocal類執行個體的時候,將ThreadLocal類的下一個hashCode值即nextHashCode的值賦給執行個體的threadLocalHashCode,然後nextHashCode的值增加HASH_INCREMENT這個值。而執行個體變數threadLocalHashCode是final的,用來區分不同的ThreadLocal執行個體。
ThreadLocal執行個體stringLocal建立完成後,調用set()方法時,
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value);}
先擷取當前線程,即main線程,然後根據線程執行個體調用getMap()方法擷取ThreadLocalMap,
其中getMap()方法如下:
ThreadLocalMap getMap(Thread t) { return t.threadLocals;}
getMap()方法直接返回線程的成員變數threadLocals,其中threadLocals變數是ThreadLocalMap類的執行個體,而ThreadLocalMap是ThreadLocal的內部類。
如果map(當前線程的成員變數threadLocals)存在,則將資料寫入到ThreadLoclMap用於儲存資料的Entry中。
ThreadLocalMap的set方法如下:
private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();}
其中Entry定義如下
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }}
key為ThreadLocal執行個體,值是使用者定義的具體對象值。
如果map(當前線程的成員變數threadLocals)不存在,則建立一個ThreadLocalMap執行個體,並和線程的成員變數threadLocals關聯起來。其中ThreadLocalMap執行個體的key為this,即ThreadLocal執行個體stringLocal,值是使用者定義的具體對象值。
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);}
總的來說,ThreadLocal的作用是提供線程內的局部變數,這種變數線上程的生命週期內起作用。作用:提供一個線程內公開變數(比如本次請求的使用者資訊),減少同一個線程內多個函數或者組件之間一些公開變數的傳遞的複雜度,或者為線程提供一個私人的變數副本,這樣每一個線程都可以隨意修改自己的變數副本,而不會對其他線程產生影響。
其他內容待續......
本文參考
Java 7 Concurrency Cookbook
http://ifeve.com/concurrency-modle-seven-week-1/
http://tutorials.jenkov.com/java-concurrency/concurrency-models.html
Java基礎-concurrent