java多執行緒-初探(三)
java多執行緒-初探(二)
本章主要闡述synchronized同步關鍵字,以及未做同步將出現的問題。
未做同步引發的問題舉例
本文舉例:1萬個人一起吃1萬碗飯,兩個人隨便吃。吃完為止。
正常想要的結果是:吃到第0碗的時候,程式執行結束,打印出剩餘的飯為0碗。
未加同步可能出現的結果是:吃到第0碗的時候,有一個人以為還有飯可以吃,接著吃。結果就把剩餘的飯吃出負數來了。
未加同步的問題不是每次執行都能出現的,這個要看cpu的資源執行效果,如果都是很和諧的狀態下則不會出現,建議多執行幾次。
class Main
package com.thread.three; public class Main { // 一萬碗飯 public static int riceNum = 10000 ; /** * 吃飯 * @param args */ public static void main(String[] args) { System.out.println("開吃!"); // 一萬個執行緒同時吃飯 int peopleNum = 10000 ; Thread[] threads = new Thread[peopleNum]; for (int i=0; i<peopleNum; i++){ Thread thread = new Thread(){ public void run(){ while (true){ if (Main.riceNum <= 0) break; System.out.println("吃了一碗,剩餘:" + Main.riceNum--); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }; thread.start(); threads[i] = thread; } // 加入主執行緒 for (Thread thread : threads){ try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("吃完啦,剩餘:" + riceNum + "碗飯"); } }
執行結果
問題原因
上述試了幾遍出現的同步問題,說到底就是有一個執行緒以為還有飯可以吃,吃的時候卻已經沒了。
關鍵性問題程式碼:
if (Main.riceNum <= 0) break;
System.out.println("吃了一碗,剩餘:" + Main.riceNum--);
舉例說明
此時飯剩下最後1碗的時候,執行緒1在第一行判斷通過,還未執行第二行減少飯的程式碼邏輯時,cpu切換到執行緒2,此時執行緒2以為飯還有1碗,執行緒2的判斷也通過,當執行緒2要執行第二行時cpu又切換到執行緒3。
以此類推,假設有3個執行緒在第二行等待,執行緒1執行了第二行程式碼,現在飯變成0碗了,輸出剩餘0碗飯。cpu又切換到執行緒2,此時輸出剩餘-1碗飯,cpu再次切換到執行緒3,則輸出剩餘-2碗飯。
當cpu切換到某一個執行緒時,從上次切換走的那行繼續執行,所以切換到執行緒2跟執行緒3時不需要判斷飯是不是小於0碗了,因為cpu切走之前已經判斷通過了。
解決思路
線上程1執行判斷以及減少飯的操作時,其他執行緒等執行緒1吃完再吃呢?
我們要把判斷是否還有飯以及吃飯的整個過程,不允許其他執行緒在我們吃的時候也來一起吃。
synchronized關鍵字
synchronized之內的程式碼塊可以只讓一個執行緒執行,執行完了才能輪到其他執行緒進入這個程式碼塊,否則其他執行緒就在synchronized處等待。
synchronized可修飾方法或者方法內的程式碼塊。
修飾方法的鎖則是呼叫該方法的物件,如果同時new多個例項,則不是同一把鎖。
synchronized的鎖
實現其他執行緒等待,歸根需要一把鎖。這個鎖建議要是一個唯一不可變的例項物件。
當執行緒1執行到synchronized時,獲取了synchronized的鎖,當執行緒1在執行程式碼塊的時,cpu切換到其他執行緒,其他執行緒要獲取鎖的時候,發現鎖已經被執行緒1佔用了,無法獲取,其他執行緒則在synchronized程式碼塊之外等待執行緒1把鎖釋放了,才能獲取鎖從而執行。
大白話解釋就是:當一個人拿了一把鑰匙開門進房間了,其他人要進來時沒有鑰匙,只能等裡面的人出來把鑰匙給其他人,其他人才能進去。
Java中的同步有三種:
- 同步程式碼塊。鎖是由開發者自己定義
- 非靜態方法上加同步。鎖物件是當前呼叫方法的那個物件使用this 表示
- 靜態方法上加同步。鎖物件是當前方法所在的class檔案,使用類名.class 表示
同步方法的執行:
當任何的執行緒要進入到當前這個方法時,就必須先判斷能不能獲取到鎖,如果可以獲取到,才能進入方法執行,獲取不到,只能在方法外面等待。
synchronized程式碼格式
synchronized( 任意物件(鎖) ){
需要被同步的程式碼
}
釋放鎖
上述為例,執行緒1釋放鎖有兩種方式。
1:synchronized程式碼塊內執行完成。
2:synchronized程式碼塊內發生異常且程式碼塊內無捕獲。
新增synchronized鎖的關鍵性程式碼示例
while (true){
synchronized (Main.class){
if (Main.riceNum <= 0) break;
System.out.println("吃了一碗,剩餘:" + Main.riceNum--);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述的鎖就是:Main.class
這個class物件在類載入時產生,唯一且不可變。
錯誤示例
while (true){
synchronized (new Object()){
if (Main.riceNum <= 0) break;
System.out.println("吃了一碗,剩餘:" + Main.riceNum--);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上文的鎖是一個Object物件,造成的結果就是鎖直接在門上插著,誰都能進。
沒new一個物件都不一樣,兩個執行緒的鎖不是同一個,則無法實現同步。
執行緒安全的類
執行緒安全:效率低。
執行緒不安全:效率高。
在一個類裡,如果所有的方法都被synchronized修飾,則稱這個類為執行緒安全的類。
能保證同一時間內,有且只有一個執行緒可以進入這個類裡修改操作資料。從而保證資料的安全性,避免因為同步而導致出現的髒資料(如上文的剩餘-1碗飯)
舉例說明:StringBuffer和StringBuilder
StringBuffer:執行緒安全的類
StringBuilder:執行緒不安全的類
StringBuffer的方法中,所有方法都被synchronized修飾。因為無法多個執行緒一起操作,所以效率相對StringBuffer比較低。
舉例說明:HashMap和Hashtable
Hashtable:執行緒安全的類,效率低
HashMap:執行緒不安全的類,效率高
舉例說明:ArrayList和Vector
Vector:執行緒安全的類,效率低
ArrayList:執行緒不安全的類,效率高
說明
執行緒安全的類,表示不可以多個執行緒同時訪問一個執行緒安全的物件。
如上文的集合,不安全的類,則可以兩個執行緒同時新增資料。安全的類則必須等第一個執行緒新增完成後第二個執行緒才能新增資料。(前提是兩個執行緒操作同一個集合物件)
注:執行緒不安全的集合也可以通過Collections工具類轉換成執行緒安全的。
如:List<Integer> list = Collections.synchronizedList(new ArrayList());