多執行緒學習(二)--整體簡介
多執行緒整體理解
一、關於多執行緒整體的理解
多執行緒複雜的地方在 物件及變數的併發訪問。指多個執行緒併發的訪問同一個物件或者變數。為了保證執行緒安全,有兩個思路。都有自己的使用場景。執行緒同步和ThreadLocal兩種方式。其中執行緒同步的思路是用“時間換空間”。訪問序列化。確保對共享資料的訪問同一時刻只有一個執行緒進行訪問。整體序列操作。而ThreadLocal採用“空間換時間”,訪問並行化,物件獨享化
前者僅有一份資料,執行緒排隊進行訪問。後者為每個執行緒提供一份資料,同時訪問互不影響。
(1) 執行緒同步
主要的方式是互斥。即通過互斥達到同步的目的。實現互斥的有三種方式。Synchronized、volatile和lock。
1) Synchronized
2) Volatile
3) Lock
(2) ThreadLocal
Thread Local解決思路,也稱之為執行緒區域性變數。它不是一個執行緒,而是儲存執行緒本地化物件的容器。當執行在多執行緒環境下,某個物件使用Thread Local進行維護的時候,Thread Local為每個使用該變數的執行緒分配一個獨立的變數副本。每個執行緒都可以修改自己的變數副本,從而真正的併發訪問,相互不影響。
1) Thread Local 的介面方法
Thread Local 本質是一個執行緒安全的Map。其中key為當前執行緒,即currentThread型別為Thread。所以,不論set、個體、或者remove操作map的key都是當前執行緒。
1.Set
設定當前執行緒的私有變數副本。即map的put方法。map . put( current Thread,value)
2.Get
獲取當前執行緒的私有副本。即map的get方法。map.get( current Thread )
3.Remove
移除當前執行緒的變數。
4.Initiative
對私有變數的初始化。即在第一次set之前,變數的初始化。
2) Spring 使用 Thread Local解決執行緒安全問題
Spring中大多數的bean是有狀態的,比如:connection。其中有很多設定,比如字符集、自動提交或手動提交等。有狀態的bean在多執行緒的環境下有執行緒安全問題。而spring的解決思路就是通過ThreadLocal來解決的。
一般情況下,從接收訪問到返回響應都在同一個執行緒中。而將非執行緒安全的變數儲存在ThreadLocal中。Bean就可以是無狀態的。
二、執行緒的特徵
執行緒的三個特徵。可以對比事務的4個特徵(原子性、一致性、隔離性和永續性)對比記憶。
(1) 原子性
多個操作,要麼都執行,要麼都不執行。
(2) 可見性
多個執行緒共同訪問同一個變數。一個執行緒修改了這個變數,其它執行緒立即就能觀測到改變。
可見性主要牽扯到JAVA的記憶體模型。多個執行緒共享的物件儲存在堆或者方法區中。這部分稱之為主記憶體。而各個執行緒都有自己的執行緒記憶體。及方法區或者執行緒記憶體。執行緒在執行的過程中會從主記憶體備份資料到執行緒內部,執行過程中對執行緒內部變數進行修改。如果需要被其它執行緒觀測到。需要及時的寫入主記憶體。
這部分主要的知識點在JVM的java記憶體模型部分。
而volatile主要是通過確保可見性,來完成輕量級的執行緒安全。
(3) 有序性
瞭解有序性,需要了解背景知識。Java位元組碼的執行是通過編譯器或者執行器來對位元組碼執行進行執行的,將位元組碼轉成機器碼,呼叫執行執行引擎。在此過程中,會發生 指令重排序。
1) 指令重排序
允許編譯器和處理器對執行進行重排序。重排序的過程不會影響單執行緒程式的執行,但是會影響到多執行緒併發執行的正確性。
2) 指令重排序的原則
嚴格遵循指令資料之間的依賴關係。從單執行緒的角度來看,不影響執行的正確性。
3) 多執行緒的有序性
遵循先行發生生原則。即第一個執行緒優先於第二個執行緒發生。則第二個執行緒能觀測到第一個執行緒的修改結果。這個稱之為先行發生原則。
4) JMM(JAVA記憶體模型)怎樣指定規則滿足先行發生原則
要說明這個問題,需要說明JVM的JAVA記憶體模型。
1.Java記憶體模型
Java記憶體模型分為主記憶體和工作記憶體兩種。此處的記憶體劃分和JVM記憶體【JVM記憶體的劃分為5部分,依次是:堆,方法區、java虛擬機器棧、本地方法棧和程式計數器】的劃分是在不同層次上的劃分。如果需要將兩者對應起來。主記憶體對應java堆的例項物件部分。工作記憶體對應棧中的部分割槽域(區域性變量表)。
JVM記憶體執行流程如下圖:
JVM在設計的時候考慮到,如果工作記憶體每次修改都去修改主記憶體,會對效能影響較大。所以,每個執行緒擁有自己的工作記憶體。在執行的過程中,修改工作記憶體,而不是直接修改主記憶體。
這樣造成了一個執行緒對變數進行修改,只修改了工作記憶體中的變數,而沒有及時的修改主記憶體變數的值。即這次操作對其餘執行緒不可見。會導致執行緒不安全的問題。因為JMM(JAVA記憶體模型)制定了一套標準,確保在多執行緒的情況下,能夠控制什麼時候記憶體會被同步給其它執行緒。
2.記憶體互動操作
記憶體模型的操作有8種。JVM虛擬機器保證每個操作都是原子性的(double和long在某些平臺除外)。
記憶體互動操作的執行流程個人理解
為了方便理解記憶體的操作。其實可以理解為三個層面。主記憶體層面資料,工作記憶體層面的資料和JVM執行子系統層面的資料。根據資料的流向來分析這8中操作。
準備從主記憶體讀取資料。首先使用lock對主記憶體的變數進行加鎖。即lock操作。其次,使用read將主記憶體資料載入到工作記憶體。同時使用load將read的變數值賦值給工作記憶體的變數。以上,就完成了資料從主記憶體層面到工作記憶體層面。同時,read和load必須配置使用。不可拆開。
工作記憶體的資料在執行指令的時候,執行子系統會呼叫作業系統API,將工作記憶體資料傳遞給執行子系統資料。即將工作記憶體變數賦值給執行層面使用。這個使用使用use。
執行子系統執行完成後得到的變數需要傳遞給工作記憶體。即將執行子系統層面的資料傳遞給工作記憶體。這個時候使用assign。
工作記憶體的變數想同步給主記憶體。通過store將工作記憶體變數值傳遞給主記憶體,之後用write將store的數值賦值給主記憶體變數。這樣完成了工作記憶體變數賦值給主記憶體變數的操作。同時,store和write必須配置合適,不能拆開。
以上的描述過程可見下圖
對主記憶體物件取消加鎖操作。即unlock。
(1)Lock 鎖定
作用於主記憶體的變數。把一個變數標識成執行緒獨佔狀態。
(2)Read
作用於主記憶體變數,將一個主記憶體變數的值賦值並傳遞給工作記憶體中,供load使用。
(3)Load
作用於工作記憶體變數。對於load過來的變數,賦值給工作記憶體變數。
(4)Use
作用於工作記憶體變數。將工作記憶體中的變數傳遞給執行引擎。
(5)Assign
作用於工作記憶體變數。將執行引擎傳遞回來的資料賦值給工作記憶體變數。
(6)Store
作用於工作記憶體變數。將工作記憶體的變數值傳遞給主記憶體模型中,供後續的write使用。
(7)Write
作用於主記憶體變數。將store來的變數賦值給主記憶體變數。
(8)Unlock
作用於主記憶體變數。把一個鎖定狀態的變數釋放出來。
3.JMM提供了三種保證有序性的方法
JMM通過指定了8給規則,讓操作滿足有序性。比如,read和load必須配合使用。Store和write必須配合使用。
- 使用volatile保證有序性
- 使用synchronized保證有序性
- 使用顯示鎖lock來保證有序性。
三、執行緒的建立
執行緒的建立按照定義由兩種,繼承thread類或者實現runnable介面。實際使用中還有使用執行緒池的方式。
(1) 繼承Thread
通過繼承Thread執行緒類來實現執行緒。完成執行緒體程式碼。Run方法。
Class MyThread extends Thread{
Run(){
}
}
啟動執行緒 MyThread.start();
(2) 實現runnable介面
MyThread implement runnable{
Run(){
}
}
啟動執行緒 MyThread.start()
(3) 使用Executor框架建立執行緒池
Executors.newXXX ;newFixedThreadPool(int) 、newCacheThreadPool()、newScheduleThreadPool(int)、 newSingleThreadExecutor
通過Executors的四個靜態放法獲取ExecutorService例項。然後執行runnable任務或者callable任務。
1) Executor執行runnable任務
通過Executor的newXXX方法獲取ExecutorService例項。然後呼叫該例項的executor(Runnable command)方法即可。一旦runnable方法傳遞到execute方法上,則該方法將會加入到任務佇列中。等待執行緒呼叫。
- public class TestCachedThreadPool{
- public static void main(String[] args){
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < 5; i++){
- executorService.execute(new TestRunnable());
- System.out.println("************* a" + i + " *************");
- }
- executorService.shutdown();
- }
- }
- class TestRunnable implements Runnable{ //重寫run方法
- public void run(){
- System.out.println(Thread.currentThread().getName() + "執行緒被呼叫了。");
- }
2) Executor執行callable任務
將callable方法傳遞給ExecutorService的submit方法。則call方法將自動提交到任務佇列中。根據執行緒池執行緒的使用情況。分配執行緒給該任務。
Submit會返回一個Feature物件。
- public class CallableDemo{
- public static void main(String[] args){
- ExecutorService executorService = Executors.newCachedThreadPool();
- List<Future<String>> resultList = new ArrayList<Future<String>>();
- //建立10個任務並執行
- for (int i = 0; i < 10; i++){
- //使用ExecutorService執行Callable型別的任務,並將結果儲存在future變數中
- Future<String> future = executorService.submit(new TaskWithResult(i));
- //將任務執行結果儲存到List中
- resultList.add(future);
- }
- //遍歷任務的結果
- for (Future<String> fs : resultList){
- try{
- while(!fs.isDone);//Future返回如果沒有完成,則一直迴圈等待,直到Future返回完成
- System.out.println(fs.get()); //列印各個執行緒(任務)執行的結果
- }catch(InterruptedException e){
- e.printStackTrace();
- }catch(ExecutionException e){
- e.printStackTrace();
- }finally{
- //啟動一次順序關閉,執行以前提交的任務,但不接受新任務
- executorService.shutdown();
- }
- }
- }
- }
- class TaskWithResult implements Callable<String>{
- private int id;
- public TaskWithResult(int id){
- this.id = id;
- }
- // 重寫call()方法
- public String call() throws Exception {
- System.out.println("call()方法被自動呼叫!!! " + Thread.currentThread().getName());
- //該返回結果將被Future的get方法得到
- return "call()方法被自動呼叫,任務返回的結果是:" + id + " " + Thread.currentThread().getName();
- }
- }
四、Synchronized
Synchronized 是java關鍵字。能保證被他修飾的方法或者程式碼塊的執行緒同步。即任意時刻只能被一個執行緒訪問。它是多執行緒中重要的執行緒同步方式。
(1) Synchronized使用方式
Synchronized主要的三種使用方式。修飾例項方法,修飾靜態方法、修飾程式碼塊
1) 修飾例項方法
鎖是當前例項物件。
2) 修飾靜態方法
鎖是當前類的class物件。【由JVM可知,是class在JVM的java.lang.class 型別的記憶體物件】,也稱之為全域性鎖。
3) 修飾程式碼塊
鎖是synchronized()括號中的物件。
當一個執行緒試圖訪問同步程式碼的時候,必須先獲得鎖。退出或者丟擲異常時,必須釋放鎖。
(2) Synchronized 鎖物件存在哪裡
synchronized用到的鎖是存在Java物件頭裡的。synchronized關鍵字實現的鎖是依賴於JVM的,底層呼叫的是作業系統的指令集實現。
對比LOCK介面。Lock介面實現的鎖不一樣,例如ReentrantLock鎖是基於JDK實現的,有Java原生程式碼來實現的。
即synchronized的鎖物件是哪個物件。則在哪個物件的物件頭中儲存佔用當前鎖的執行緒資訊。
(3) Synchronized在JVM中的實現
synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步程式碼塊的開始位置,monitorexit 指令則指明同步程式碼塊的結束位置。
當執行 monitorenter 指令時,執行緒試圖獲取鎖也就是獲取 monitor的持有權。當計數器為0則可以成功獲取,獲取後將鎖計數器設為1也就是加1。相應的在執行 monitorexit 指令後,將鎖計數器設為0,表明鎖被釋放。如果獲取物件鎖失敗,那當前執行緒就要阻塞等待,直到鎖被另外一個執行緒釋放為止。
monitor物件存在於每個Java物件的物件頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什麼Java中任意物件可以作為鎖的原因。
五、Volatile
Volatile主要的作用是使變數在多個執行緒間可見。Volatile是多執行緒的輕量級實現。
(1) Volatile的可見性
Volatile修飾的變數,線上程記憶體中使用時候,必須先從主記憶體進行同步過來,然後在使用,確保只要主記憶體資料被修改,每次使用的時候必須拿到的是最新的主記憶體變數值。
其次,執行緒每次對執行緒記憶體變數修改的使用,主動地同步修改主記憶體的變數資料。確保執行緒內變數修改,及時的同步到主記憶體。確保執行緒修改,立馬對其它執行緒可見。
由以上兩個操作,確保了volatile修飾的變數對其它執行緒可見。
(2) Volatile 的有序性
JMM(Java 記憶體模型)有三個方案保證多執行緒的有序性。Volatile、synchronized和lock。能保證多執行緒的有序性。即先行發生原則。則後執行的執行緒能觀測到先執行執行緒的修改。
如果一個變數被宣告volatile的話,那麼這個變數不會被進行重排序。
(3) Volatile不能保證原子性
Volatile 只能保證修飾的變數的可見性和有序性。不能保證原子性。所以, 在變數修改和自身資料無關的情況下,相當於原子操作的。因為不存在多個執行緒同時對volatile修飾變數的同時訪問。這種情況下,是執行緒安全的。具體情況如下。
1) 運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒修改變數的值。
2) 變數不需要與其他狀態變數共同參與不變約束。
在以上兩種情況下,volatile修飾的變數能保證原子性。因為其本身能保證可見性和有序性。所以,以上兩種情況下能保證執行緒安全。
六、Lock
Lock是介面。具體的實現類有兩種。ReentrantLock和ReentrantReadWriteLock兩個子類。和synchronized相比較。Synchronized是通過在編譯的過程中,在位元組碼中新增monitorrenter和monitorexit位元組碼命令。在執行位元組碼的過程中,呼叫系統API,確保執行緒同步的。而lock是jdk中的包提供的功能,實現的執行緒同步。
(1) Lock實現同步
根據synchronized關鍵字可以瞭解。它實現執行緒同步是使用一個鎖物件。然後在需要執行緒同步的地方新增獲取鎖。在使用完成之後釋放鎖。其實,lock的實現思路也是一樣的。Lock的子類就是一個鎖物件而已。
在使用之前先建立一個鎖物件。比如 Lock lock = new ReentrantLock();然後在使用的開始地方獲取鎖。即lock.lock();在使用完成的地方新增一個釋放鎖。即lock.unlock()操作。即通過lock完成執行緒的同步。
(2) 使用condition實現等待/通知
個人理解的執行緒之間的等待/通知模式和執行緒的並行操作操作模式其實是多執行緒使用的兩個場景。
1) 多執行緒並行操作模式的理解
多執行緒並行操作模式,其實是多個執行緒針對共享資料的不同部分,做相同的流程的操作。
2) 等待通知模式的理解
等待/通知模式 其實是不同種類執行緒(有不同的操作流程)之間的配合協作使用用場景。
3) Lock的condition使用
關鍵字synchronized和wait/notify配合使用,可以實現等待通知模式。而lock中提供了condition,能提供多路通知的功能。即可以選擇性通知。
1.使用方法
通過Condition condition = lock.newCondition()獲取condition物件。在需要等待的地方呼叫condition.wait().則對當前執行緒處於等待狀態。
2.使用需要注意
建立condition的時候,在此之前需要對lock加鎖。即在之前需要呼叫lock.lock()。否則會報錯。
七、執行緒池相關
(1) 執行緒池的好處
1) 降低資源消耗
通過重複利用已建立的執行緒,降低執行緒建立和銷燬造成的消耗。
2) 提高響應速度
任務準備好就可以立即執行,不需要因為等待建立執行緒的時間。
3) 提高執行緒的可管理型
執行緒是稀缺資源,使用執行緒池可以統一分配,監控。
(2) 常見的執行緒池及使用的場景
常見的執行緒池ThreadPoolExecutor有FixedThreadPool、CacheThreadPool、SingleThreadExecutor。
1.FixedThreadPool 可重用固定執行緒數的執行緒池
適用場景:適用於負載比較重的伺服器
- FixedThreadPool使用無界佇列LinkedBlockingQueue作為執行緒池的工作佇列。
- 該執行緒池的執行緒數始終保持不變。當一個新的任務提交時,執行緒池若有空閒執行緒,則立即執行。若沒有空閒執行緒,則新任務被暫存在任務佇列中。待有執行緒空閒時,從佇列中取出需要處理的任務開始執行。
2.CacheThreadPool 根據需要調節執行緒數量的執行緒池
適用場景:大小無界,適用於執行需要短期非同步的小程式。或者負載較輕的伺服器。
- CachedThreadPool使用沒有容量的SynchronousQueue作為執行緒池的工作佇列,但CachedThreadPool的maximumPool是無界的。
- 執行緒池的數量不確定,但是,如果有空閒執行緒,則優先複用空閒的執行緒。若所有執行緒都在工作,此時,有新任務提交,則建立新執行緒處理任務。
3.SingleThreadExecutor 只建立一個執行緒執行任務
適用場景:需要保持順序執行任務。任意時間點,沒有多執行緒活動的場景。
- SingleThreadExecutor使用無界佇列LinkedBlockingQueue 作為執行緒池的工作佇列。
- 若有新任務提交,則儲存到任務佇列。待執行緒空閒,按照佇列的先後順序,執行任務。
(3) 執行緒池有哪幾種工作佇列
1) ArrayBlockingQueue
基於資料結構的有界阻塞佇列。使用元素遵循佇列屬性,FIFO先進先出。
2) LinkedBlockingQueue
基於連結串列的無界阻塞佇列(連結串列結構確定了無界)。吞吐量高於ArrayBlockingQueue。靜態方法Executor. newFixedThreadPool()使用這個佇列。
3) SynchronousQueue
是一個不儲存元素的佇列。每個插入佇列必須等待另一個執行緒呼叫移除操作。否則插入操作一直處於阻塞狀態。
4) PriorityBlockingQueue
一個具有優先順序的無阻塞佇列。
(4) 執行緒池引數
1) CorePoolSize 執行緒池基本大小
即使執行緒池中沒有任務,也會有corePoolSize個執行緒等待任務。
2) MaximumPoolSize 最大執行緒數
執行緒池最多的執行緒數量。
3) KeepAliveTime執行緒存活時間
如果執行緒池中的執行緒數大於CorePoolSize,且等待時間大於KeepAliveTime,仍然沒有任務執行,則執行緒退出。
4) Unit 執行緒存活時間的單位
比如:TimeUnit.SECONDS。
5) WorkQueue 工作佇列
用於保持執行任務的阻塞佇列。
6) ThreadFactory 執行緒工廠
主要是為了給執行緒起名字。
7) Handler 拒絕策略
當執行緒和佇列已經滿的時候,應該採用什麼樣的策略才處理新提交的任務。比如 報錯,丟棄等
(5) 執行緒池執行流程
任務提交到執行緒池,判斷當前執行緒數和基本執行緒數和最大執行緒數之間的關係。
1) 當前執行緒數小於基本執行緒數
建立執行緒來提交任務。
2) 當前執行緒數大於基本執行緒數小於最大執行緒數
1.工作佇列未滿
放入任務佇列中,等待執行緒池安排執行緒執行佇列中任務。
2.工作佇列已滿
呼叫拒絕策略,進行拒絕任務。
八、synchronized和volatile區別
所以,一定情況下,synchronized和volatile都能解決執行緒資料同步的問題。但是,各有特點。
- Synchronized 修飾的是方法,程式碼塊。Volatile修飾的是共享變數。
- Synchronized 是通過同步阻塞的方式完成變數的同步,體現的是原子性。Volatile是非阻塞的,能保證可見性,不能保證原子性。第一時間回去修改資料到主記憶體。
- 什麼時候下可以使用volatile?任何時候都可以使用synchronized,都能起到資料同步的作用。如果寫入的變數值不依賴當前的變數值的情況下可以使用。