多執行緒(2):synchronized關鍵字
多執行緒操作相同資源的時候,會出現執行緒安全的問題,導致結果與預期的不一致。
如下例子,設計四個執行緒,其中兩個對執行緒對變數加1操作,兩個執行緒對變數減1操作。理想狀態是,執行緒順序執行,相同次數的加減操作,最後變數的值不變。
1.執行緒不安全的操作
public class FourThreadAddMinus { private int i = 0; public static void main(String[] args) { FourThreadAddMinus ft = new FourThreadAddMinus(); for(int i = 0; i < 2; i++){ new Thread(ft.new Add(), "add執行緒-" + i).start(); new Thread(ft.new Minus(),"minus執行緒-" + i).start(); } } // 內部類,加1操作(不用內部類也行,這裡偷懶,把程式碼都放到一個class檔案下) class Add implements Runnable{ @Override public void run() { for(int i = 0; i < 5; i++) { add(); } } } // 內部類,減1操作 class Minus implements Runnable{ @Override public void run() { for(int i = 0; i < 5; i++){ minus(); } } } private void add(){ i++; System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i); } private void minus(){ i--; System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i); } }
列印輸出結果:
1542793183193 - add執行緒-0 執行 >>> 1 1542793183193 - minus執行緒-1 執行 >>> 0 1542793183193 - add執行緒-0 執行 >>> 1 1542793183193 - minus執行緒-1 執行 >>> 0 1542793183193 - add執行緒-0 執行 >>> 1 1542793183193 - minus執行緒-0 執行 >>> -1 1542793183193 - add執行緒-0 執行 >>> 0 1542793183193 - minus執行緒-1 執行 >>> 0 1542793183194 - add執行緒-0 執行 >>> 0 1542793183194 - minus執行緒-0 執行 >>> -1 1542793183194 - minus執行緒-1 執行 >>> -1 1542793183194 - minus執行緒-0 執行 >>> -2 1542793183194 - minus執行緒-1 執行 >>> -3 1542793183194 - add執行緒-1 執行 >>> -3 1542793183195 - add執行緒-1 執行 >>> -2 1542793183195 - add執行緒-1 執行 >>> -1 1542793183195 - add執行緒-1 執行 >>> 0 1542793183194 - minus執行緒-0 執行 >>> -4 1542793183195 - minus執行緒-0 執行 >>> 0 1542793183195 - add執行緒-1 執行 >>> 1
從執行的結果看,同一時刻,有多個執行緒同時執行且順序不定,會出現某個執行緒拿到的值是另一個執行緒未儲存的,也可以理解成資料庫裡面的髒讀,造成的結果就是共享資源變數i的值不可控,每次執行,結果可能都不一樣。這不是我們想要的結果。
這種情況,我們需要控制執行緒的併發狀態,即同一時刻,只能有一個執行緒對變數i進行操作,其他執行緒阻塞。等該執行緒執行結束,i的值進行變更後,其他執行緒拿到最新的i值再進行操作。
2.執行緒安全的操作(多執行緒搶佔同一例項的鎖)
java中提供了一個關鍵字,synchronized,可以控制執行緒的併發。
對程式碼進行如果修改add()和minuss()方法加上,synchronized關鍵字
public class FourThreadAddMinus {
private int i = 0;
public static void main(String[] args) {
FourThreadAddMinus ft = new FourThreadAddMinus();
for(int i = 0; i < 2; i++){
new Thread(ft.new Add(), "add執行緒-" + i).start();
new Thread(ft.new Minus(),"minus執行緒-" + i).start();
}
}
// 內部類,加1操作(不用內部類也行,這裡偷懶,把程式碼都放到一個class檔案下)
class Add implements Runnable{
@Override
public void run() {
for(int i = 0; i < 5; i++) {
add();
}
}
}
// 內部類,減1操作
class Minus implements Runnable{
@Override
public void run() {
for(int i = 0; i < 5; i++){
minus();
}
}
}
private synchronized void add(){
i++;
System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);
}
private synchronized void minus(){
i--;
System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);
}
}
列印輸出結果:
1542793702921 - add執行緒-0 執行 >>> 1
1542793702922 - add執行緒-0 執行 >>> 2
1542793702922 - add執行緒-0 執行 >>> 3
1542793702922 - add執行緒-0 執行 >>> 4
1542793702922 - add執行緒-0 執行 >>> 5
1542793702923 - minus執行緒-0 執行 >>> 4
1542793702923 - minus執行緒-0 執行 >>> 3
1542793702924 - minus執行緒-0 執行 >>> 2
1542793702924 - minus執行緒-0 執行 >>> 1
1542793702924 - add執行緒-1 執行 >>> 2
1542793702924 - add執行緒-1 執行 >>> 3
1542793702924 - add執行緒-1 執行 >>> 4
1542793702924 - add執行緒-1 執行 >>> 5
1542793702924 - add執行緒-1 執行 >>> 6
1542793702924 - minus執行緒-0 執行 >>> 5
1542793702925 - minus執行緒-1 執行 >>> 4
1542793702925 - minus執行緒-1 執行 >>> 3
1542793702925 - minus執行緒-1 執行 >>> 2
1542793702925 - minus執行緒-1 執行 >>> 1
1542793702926 - minus執行緒-1 執行 >>> 0
從執行的結果看,由於add()和minus()方法被synchronized修飾,執行緒執行該方法,需要獲取當前例項的鎖。而當前例項只有一個FourThreadAddMinus ft = new FourThreadAddMinus()。所以,假設add執行緒-0獲取鎖以後,minus執行緒-0,minus執行緒-1,add執行緒-1,則處於阻塞狀態,。待add執行緒-0執行完,鎖釋放後,其他執行緒搶佔資源,獲取鎖。此時執行緒one by one執行,最後i的值變為初識狀態0。
3.執行緒不安全的操作(多執行緒搶佔多例項的鎖)
程式碼再進一步修改,new兩個FourThreadAddMinus例項。
public class FourThreadAddMinus {
private int i = 0;
public static void main(String[] args) {
FourThreadAddMinus ft1 = new FourThreadAddMinus();
FourThreadAddMinus ft2 = new FourThreadAddMinus();
for(int i = 0; i < 2; i++){
new Thread(ft1.new Add(), "add執行緒-" + i).start();
new Thread(ft2.new Minus(),"minus執行緒-" + i).start();
}
}
// 內部類,加1操作(不用內部類也行,這裡偷懶,把程式碼都放到一個class檔案下)
class Add implements Runnable{
@Override
public void run() {
for(int i = 0; i < 5; i++) {
add();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 內部類,減1操作
class Minus implements Runnable{
@Override
public void run() {
for(int i = 0; i < 5; i++){
minus();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private synchronized void add(){
i++;
System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);
}
private synchronized void minus(){
i--;
System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);
}
}
列印輸出結果:
1542795420885 - add執行緒-0 執行 >>> 1
1542795420886 - minus執行緒-0 執行 >>> -1
1542795420886 - add執行緒-1 執行 >>> 2
1542795420886 - minus執行緒-1 執行 >>> -2
1542795421386 - minus執行緒-1 執行 >>> -3
1542795421386 - add執行緒-1 執行 >>> 3
1542795421386 - minus執行緒-0 執行 >>> -4
1542795421386 - add執行緒-0 執行 >>> 4
1542795421887 - add執行緒-1 執行 >>> 5
1542795421887 - add執行緒-0 執行 >>> 6
1542795421887 - minus執行緒-0 執行 >>> -5
1542795421887 - minus執行緒-1 執行 >>> -6
1542795422388 - minus執行緒-1 執行 >>> -7
1542795422388 - add執行緒-0 執行 >>> 7
1542795422388 - minus執行緒-0 執行 >>> -8
1542795422388 - add執行緒-1 執行 >>> 8
1542795422888 - add執行緒-0 執行 >>> 9
1542795422888 - minus執行緒-1 執行 >>> -9
1542795422888 - add執行緒-1 執行 >>> 10
1542795422888 - minus執行緒-0 執行 >>> -10
從結果看,add兩個執行緒順序執行,將i從0加到10;minus兩個執行緒順序執行,將i從0減到-10。但這裡,add執行緒操作的i和minus操作的i不是同一個,因為是兩個例項。鎖也只是保證了兩個add執行緒(或兩個minus的執行緒)的順序。其實我是想寫,四個執行緒,搶兩個例項鎖,操作同一個資源。不刪了,就當複習一下多執行緒(1)的內容。程式碼再改一下就好了,將 private int i = 0,改成 private static int i = 0。加一個static關鍵字,變成所有例項共享的變數。完整程式碼就不放了,跟上面的一樣,就是多個static關鍵字。
列印輸出結果:
1542796917053 - add執行緒-0 執行 >>> 1
1542796917054 - minus執行緒-0 執行 >>> 0
1542796917054 - add執行緒-1 執行 >>> 1
1542796917054 - minus執行緒-1 執行 >>> 0
1542796917554 - add執行緒-0 執行 >>> 0
1542796917554 - minus執行緒-1 執行 >>> 0
1542796917554 - add執行緒-1 執行 >>> 1
1542796917554 - minus執行緒-0 執行 >>> 0
1542796918054 - minus執行緒-0 執行 >>> 1
1542796918054 - add執行緒-0 執行 >>> 1
1542796918054 - minus執行緒-1 執行 >>> 0
1542796918054 - add執行緒-1 執行 >>> 1
1542796918555 - add執行緒-1 執行 >>> 1
1542796918555 - minus執行緒-1 執行 >>> 1
1542796918555 - minus執行緒-0 執行 >>> 1
1542796918555 - add執行緒-0 執行 >>> 2
1542796919056 - add執行緒-1 執行 >>> 2
1542796919056 - minus執行緒-1 執行 >>> 2
1542796919056 - add執行緒-0 執行 >>> 3
1542796919056 - minus執行緒-0 執行 >>> 2
從結果看,由於add執行緒和minus執行緒,搶佔的不是同一個例項的鎖,所以add和minus之前互不影響,同一時刻,可能add和minus都在執行,而且由於操作同一個資源i,就會出現髒讀的情況,最後結果也不可控。
4.synchronized關鍵字的使用:
如果一個方法被synchronized修飾了,當一個執行緒獲取了對應的鎖,並執行該程式碼塊時,其他執行緒便只能一直等待,等待獲取鎖的執行緒釋放鎖,而這裡獲取鎖的執行緒釋放鎖只會有兩種情況:
- 獲取鎖的執行緒執行完了該程式碼塊,然後執行緒釋放對鎖的佔有;
- 執行緒執行發生異常,此時JVM會讓執行緒自動釋放鎖。
synchronized可保證一個執行緒的變化(主要是共享資料的變化)被其他執行緒所看到(保證可見性,完全可以替代Volatile功能),這點確實也是很重要的。
synchronized關鍵字最主要有以下3種應用方式
- 修飾例項方法,作用於當前例項加鎖,進入同步程式碼前要獲得當前例項的鎖(上面講的都是這種用法)
- 修飾靜態方法,作用於當前類物件加鎖,進入同步程式碼前要獲得當前類物件的鎖
- 修飾程式碼塊,指定加鎖物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖。
修飾靜態方法,也好說,把程式碼改一下。
private synchronized static void add(){
i++;
System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);
}
private synchronized static void minus(){
i--;
System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 執行 >>> " + i);
}
列印輸出結果:
1542797628413 - add執行緒-0 執行 >>> 1
1542797628413 - minus執行緒-0 執行 >>> 0
1542797628413 - minus執行緒-1 執行 >>> -1
1542797628413 - add執行緒-1 執行 >>> 0
1542797628914 - minus執行緒-1 執行 >>> -1
1542797628914 - minus執行緒-0 執行 >>> -2
1542797628914 - add執行緒-0 執行 >>> -1
1542797628914 - add執行緒-1 執行 >>> 0
1542797629414 - add執行緒-0 執行 >>> 1
1542797629414 - add執行緒-1 執行 >>> 2
1542797629414 - minus執行緒-1 執行 >>> 1
1542797629414 - minus執行緒-0 執行 >>> 0
1542797629915 - add執行緒-0 執行 >>> 1
1542797629915 - add執行緒-1 執行 >>> 2
1542797629915 - minus執行緒-1 執行 >>> 1
1542797629915 - minus執行緒-0 執行 >>> 0
1542797630415 - add執行緒-1 執行 >>> 1
1542797630415 - add執行緒-0 執行 >>> 2
1542797630415 - minus執行緒-0 執行 >>> 1
1542797630415 - minus執行緒-1 執行 >>> 0
從輸出結果看,執行緒之間還是 one by one執行,因為add()和minus()方法都是靜態方法,為當前類物件所有,不屬於某一個例項。不管建立多少例項,類的物件都是那一個。多執行緒搶佔的是類物件的鎖,只有一把鎖,所以執行緒之間互斥,誰搶到鎖,誰執行。