Java高併發程式設計之synchronized關鍵字(一)
首先看一段簡單的程式碼:
public class T001 { private int count = 0; private Object o = new Object(); public void m() { //任何執行緒要執行下面這段程式碼,必須先拿到o的鎖 synchronized (o) { count++; System.out.println(Thread.currentThread().getName() + " count = " + count); } } }
這段程式碼非常簡單,o指向了堆記憶體中的一個物件,synchronized(o)起到的作用是,當有執行緒要執行其作用域中的程式碼的時候,需要先獲取到o所指向的物件的鎖,注意o是物件的引用,物件存在於堆記憶體中,鎖的資訊也是記錄在堆記憶體中。只有拿到了鎖的執行緒,才能執行,否則只有等待其他執行緒釋放鎖,所以這把鎖叫做互斥鎖。注意,synchronized鎖定的不是一個程式碼塊,而是一個物件。
將上面程式碼稍加修改,如下:
public class T002 { private int count = 0; public void m() { //任何執行緒要執行下面這段程式碼,必須先拿到this的鎖 synchronized (this) { count++; System.out.println(Thread.currentThread().getName() + " count = " + count); } } }
synchronized (this)鎖定的是自身這個物件,如果一段程式,開始的時候就要鎖定自身,結束時才釋放,那麼有一種簡單的寫法:
public class T003 {
private int count = 0;
//任何執行緒要執行下面這段程式碼,必須先拿到this的鎖
public synchronized void m() {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
synchronized關鍵字直接寫在方法的宣告中,它跟synchronized (this)是等價的,鎖定的都是this物件(不是鎖定一段程式碼哦
那麼如果synchronized關鍵字寫在靜態方法的宣告中,情況又會怎樣呢?看如下程式碼:
public class T004 {
private static int count = 0;
//這裡等同於 synchronized(T004.class)
public synchronized static void m() {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void mm() {
synchronized (T004.class) {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
實際上ynchronized關鍵字寫在靜態方法的宣告中,等同於鎖定的是T004.class這個物件,T004.class是Class類的物件。靜態的方法,不需要建立物件來訪問,所以這時候是不需要this的,所以m方法等同於mm方法。
synchronized宣告的方法程式碼塊,相當於一個原子操作:
public class T005 implements Runnable {
private static int count = 10;
@Override
public synchronized void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
T005 t005 = new T005();
for(int i = 0; i < 5; i++) {
new Thread(t005, "thread_" + i).start();
}
}
}
上面程式碼中,因為run方法由synchronized關鍵字,所以該方法的程式碼塊,就相當於一個原子操作,儘管在main方法中啟動了5個執行緒都對t005物件的count進行了修改,但列印的結果順序完好。
假如同時存在同步方法(方法宣告中有synchronized關鍵字)和非同步方法,那麼在執行同步方法的過程中,非同步方法能否被其他執行緒執行呢?看下面這段程式碼:
public class T006 {
private static int count = 10;
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " m1 start...");
try {
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m1 end.");
}
public void m2() {
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m2");
}
public static void main(String[] args) {
T006 t006 = new T006();
new Thread(()->t006.m1()).start();
new Thread(()->t006.m2()).start();
}
}
上面程式碼啟動了兩個執行緒,執行緒1執行m1方法,執行緒2執行m2方法,在m1執行的過程中睡眠了10秒,在此期間,m2被執行了,執行結果如下:
Thread-0 m1 start...
Thread-1 m2
Thread-0 m1 end.
這說明,在同步方法在被執行的過程中,非同步方法是可以被其他執行緒執行的。有一個經典的問題,那就是寫方法同步,讀方法非同步的時候,容易產生髒讀,下面是一個例子:
public class T007 {
private String name;
private double balance;
public synchronized void set(String name, double balance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
this.balance = balance;
}
public double getBalance() {
return this.balance;
}
public static void main(String[] args) {
T007 t007 = new T007();
new Thread(()->t007.set("zhangsan", 100.0)).start();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("balance:" + t007.getBalance());
}
}
顯然打印出來的並不會是set進去的100.0,而是0,這就是髒讀問題。業務程式碼只對寫加鎖,而讀不加鎖,寫方法在被執行的時候,讀方法可以被其他執行緒執行。因此,在做業務的時候要考慮清楚,是否允許髒讀。
synchronized獲得的鎖,是可重入的。即一個同步方法,可以呼叫另一個同步方法,一個執行緒已經擁有某個物件的鎖,再次申請時,仍然會得到該物件的鎖。下面是一個小例子:
public class T008 {
public synchronized void m1() {
System.out.println("m1 start...");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
m2();
System.out.println("m1 end.");
}
public synchronized void m2() {
System.out.println("m2 start...");
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("m2 end.");
}
public static void main(String[] args) {
T008 t008 = new T008();
new Thread(t008::m1).start();
}
}
列印結果如下:
m1 start...
m2 start...
m2 end.
m1 end.
用一句話歸納,就是,同一個執行緒,同一把鎖,則可重入。另一種情形與此相似,子類呼叫父類的方法:
public class T009 {
public synchronized void m() {
System.out.println("m start...");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("m end.");
}
public static void main(String[] args) {
TT009 tt009 = new TT009();
new Thread(tt009::m).start();
}
}
class TT009 extends T009 {
@Override
public synchronized void m() {
System.out.println("child m start...");
super.m();
System.out.println("child m start...");
}
}
列印結果如下:
child m start...
m start...
m end.
child m start...
在程式執行過程中,如果出現異常,預設情況鎖會被釋放,所以,在併發處理過程中,有異常要多加小心,不然可能發生不一致的情況。比如,在一個web app處理過程中,多個servlet執行緒共同訪問同一個資源,這是如果異常處理不合適,在第一個執行緒丟擲異常,其他執行緒就會進入同步程式碼區,有可能會訪問到異常產生時的資料。因此要非常小心地處理同步業務邏輯中的異常。
public class T010 {
private int count = 0;
public synchronized void m() {
System.out.println(Thread.currentThread().getName() + " start...");
while (true) {
count++;
System.out.println("m start...");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
if(5 == count) {
//此處必定會丟擲異常,要想鎖不被釋放,需要進行catch,讓迴圈繼續
int i = 1/0;
}
}
}
public static void main(String[] args) {
T010 t010 = new T010();
Runnable r = new Runnable() {
@Override
public void run() {
t010.m();
}
};
new Thread(r, "thread_1").start();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
new Thread(r, "thread_2").start();
}
}
在上面這段程式碼中,thread_1丟擲異常時,thread_2馬上開始執行,這說明,thread_1丟擲異常的時候,也釋放了鎖。