Java併發程式設計3 —— 物件鎖和類鎖
synchronized關鍵字作用在同步程式碼塊上,給共享變數“上鎖”可以解決執行緒安全問題。這把“鎖”可以作用在某個物件上,也可以作用在某個類上。
舉個栗子,有個自助銀行,裡面有兩臺ATM機,工作人員可以看到每次存取款之後機器裡鈔票的總金額數。現在有兩個人來存錢,各存50元。
沒有鎖
在下面的程式碼中,兩個執行緒t1、t2相當於兩個人,每個Service物件相當於一臺ATM機。這裡先只建立了一個Service物件,也就是兩個人向同一臺ATM機裡存錢。顯然,第一個人存完後機器裡應當有150元,第二個人存完後有200元。
public class Bank { public static void main(String[] args) { Service s1 = new Service(); //Service s2 = new Service(); Thread t1 = new Thread(s1, "t1"); Thread t2 = new Thread(s1, "t2"); t1.start(); t2.start(); } } class Service implements Runnable{ private int total = 100; @Override public void run() { total += 50; System.out.println(Thread.currentThread().getName() + " --- total = " + total); } }
輸出:
t1 --- total = 200
t2 --- total = 200
顯然是兩個人同時操作“餘額”這個共享變數時出現了問題,第一個人存完50元后即讀到了第二個人存完50元后的餘額200元。
物件鎖
我們給run方法加上synchronized關鍵字,相當於給存錢和修改餘額的操作原子化,加上一把鎖。
public class Bank { public static void main(String[] args) { Service s1 = new Service(); //Service s2 = new Service(); Thread t1 = new Thread(s1, "t1"); Thread t2 = new Thread(s1, "t2"); t1.start(); t2.start(); } } class Service implements Runnable{ private int total = 100; @Override public synchronized void run() { total += 50; System.out.println(Thread.currentThread().getName() + " --- total = " + total); } }
輸出:
t1 --- total = 150
t2 --- total = 200
這裡的結果就是正確的了。第一個人存完後看到餘額150元,第二個人存完後看到餘額200元。
如果執行緒t2使用另一個物件s2會怎麼樣?
public class Bank { public static void main(String[] args) { Service s1 = new Service(); Service s2 = new Service(); Thread t1 = new Thread(s1, "t1"); Thread t2 = new Thread(s2, "t2"); t1.start(); t2.start(); } } class Service implements Runnable{ private int total = 100; @Override public synchronized void run() { total += 50; System.out.println(Thread.currentThread().getName() + " --- total = " + total); } }
輸出
t1 --- total = 150
t2 --- total = 150
這裡的邏輯相當於兩個人分別在兩臺ATM機上存錢,而這兩臺機器裡鈔票的餘額是互相獨立、互不干擾的。因為s1和s2每個物件都有它自己的“total”變數,分別給了t1和t2去操作。所以在這種邏輯下也就不會產生執行緒安全問題,這兩個變數並不是共享的。程式碼也證實了這一點,如果把此時run方法的synchronized關鍵字去掉,得到的是同樣的結果。
這也就是物件鎖的概念。物件鎖作用在某個(非靜態)方法上,這個方法為每個物件所獨享。只有當不同的執行緒處理同一個物件時,相當於走了同一個方法處理同一個共享變數,才會產生執行緒安全問題。當一個執行緒處理這個物件或物件的方法時,其他執行緒必須等待,直到這個執行緒處理完畢,釋放物件鎖,其他執行緒才重新競爭並獲得物件鎖。
類鎖
繼續上面存錢的例子。每個ATM機內鈔票的金額是每臺機器獨享的,但是整個銀行擁有的資金總額是各個機器所共享的,在每臺機器上存錢取錢後都會修改這同一個銀行資金總額。
靜態變數、靜態方法不再屬於某個具體的物件例項,而是屬於某個類的。所以我們把total變數定義為static。
public class Bank {
public static void main(String[] args) {
Service s1 = new Service();
Service s2 = new Service();
Thread t1 = new Thread(s1, "t1");
Thread t2 = new Thread(s2, "t2");
t1.start();
t2.start();
}
}
class Service implements Runnable{
private static int total = 100;
@Override
public synchronized void run() {
total += 50;
System.out.println(Thread.currentThread().getName() + " --- total = " + total);
}
}
輸出
t1 --- total = 150
t2 --- total = 200
兩個執行緒去操作了這個類的不同物件例項,但static變數total屬於Service這個類,所以兩個執行緒操作了相同的共享變數。如果不加synchronized關鍵字,就有點類似本文第一段程式碼的情形,產生了執行緒安全問題。
這就是類鎖的概念。靜態方法、靜態變數只會存在一份,給它加上鎖,不同執行緒使用不同物件操作這個變數時,也就使用的相同的一把鎖。
總結
物件鎖即“一個物件一把鎖”,當多個執行緒使用同一個物件時會有執行緒安全問題,多個物件之間的鎖互不影響。
類鎖即“多個物件一把鎖”,當多個執行緒使用多個物件操作同一個靜態共享變數時存線上程安全問題。