Java併發程式設計系列(二)-synchronized同步鎖
synchronized基本用法
從一個簡單的例子入手
1 public class C1 { 2 private int count = 10; 3 4 public void foo() { 5 count--; 6 System.out.println(Thread.currentThread().getName() + " count=" + count); 7 } 8 9 public static void main(String[] args) { 10 C1 c1 = new C1();11 for (int i = 0; i < 5; i++) { 12 new Thread(c1::foo).start(); 13 } 14 } 15 }
類裡有個count變數初始是10.foo方法執行一次,count減1,打印出當前執行緒名和count.
程式入口方法new出一個C1的例項,開啟5個執行緒同時去執行foo方法.
執行結果:
執行兩次結果不一樣,第一次符合預期,每次減1.第二次出了狀況,9沒了,出現了兩次8.
原因:Thread-0執行緒執行了count--,count變成了9,還沒有列印結果.這時Thread-1執行緒也進來了,執行count--,count變成了8.然後Thread-0開始列印結果,這時count是8,輸出的是8.然後Thread-1開始列印結果,這時count也是8,所以輸出了兩次8.
多執行幾次,還會出現不同的結果.單執行緒執行沒問題的程式碼,到了多執行緒環境下.執行結果就會變得不可預期.
synchronized關鍵字就是用來解決這種問題的.
1 public class C1 { 2 private int count = 10; 3 private Object o = new Object(); 4 5 public void foo1() { 6 synchronized (o) { 7 count--; 8 System.out.println(Thread.currentThread().getName() + " count=" + count);9 } 10 } 11 12 public static void main(String[] args) { 13 C1 c1 = new C1(); 14 for (int i = 0; i < 5; i++) { 15 new Thread(c1::foo1).start(); 16 } 17 }
執行結果:
無論執行多少次,結果都是符合預期的.
將count--和列印結果放在synchronized的大括號裡,表示這是一個原子操作,不能分割.執行到一半的時候,別的執行緒是進不來的,只能阻塞.等到這個執行緒執行完大括號的程式碼時,會釋放鎖.這時候別的執行緒才能執行這段程式碼.
像這種簡單的場景每次想加鎖時還要new一個物件,感覺有點多此一舉啊.所以還有別的寫法,可以鎖定當前物件this.靜態方法沒有this物件,所以靜態方法鎖定的是類物件.下面幾種方法,對於這個場景,效果是一樣的.
1 public class C1 { 2 private int count = 10; 3 private Object o = new Object(); 4 5 public void foo() { 6 count--; 7 System.out.println(Thread.currentThread().getName() + " count=" + count); 8 } 9 10 public void foo1() { 11 synchronized (o) { 12 count--; 13 System.out.println(Thread.currentThread().getName() + " count=" + count); 14 } 15 } 16 17 //鎖定this 18 public void foo2() { 19 synchronized (this) { 20 count--; 21 System.out.println(Thread.currentThread().getName() + " count=" + count); 22 } 23 } 24 25 //將synchronized加到方法宣告上 同樣鎖定的是this 26 public synchronized void foo3() { 27 count--; 28 System.out.println(Thread.currentThread().getName() + " count=" + count); 29 } 30 31 private static int sCount = 10; 32 private static Object so = new Object(); 33 34 public static void bar() { 35 sCount--; 36 System.out.println(Thread.currentThread().getName() + " sCount=" + sCount); 37 } 38 39 public static void bar1() { 40 synchronized (so) { 41 sCount--; 42 System.out.println(Thread.currentThread().getName() + " sCount=" + sCount); 43 } 44 } 45 46 //靜態方法鎖定類物件 47 public static void bar2() { 48 synchronized (C1.class) { 49 sCount--; 50 System.out.println(Thread.currentThread().getName() + " sCount=" + sCount); 51 } 52 } 53 54 //將synchronized加到靜態方法的方法宣告上 同樣鎖定的是類物件 55 public synchronized static void bar3() { 56 sCount--; 57 System.out.println(Thread.currentThread().getName() + " sCount=" + sCount); 58 } 59 }
synchronized鎖定的物件
1 public class C2 { 2 private int count = 10; 3 private int count1 = 10; 4 private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); 5 6 public synchronized void foo() { 7 System.out.println("foo() " + simpleDateFormat.format(new Date())); 8 try { 9 Thread.sleep(100); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 count--; 14 System.out.println(Thread.currentThread().getName() + " count=" + count); 15 System.out.println("foo() end " + simpleDateFormat.format(new Date())); 16 } 17 18 public synchronized void foo1() { 19 System.out.println("foo1() " + simpleDateFormat.format(new Date())); 20 try { 21 Thread.sleep(100); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 count1--; 26 System.out.println(Thread.currentThread().getName() + " count1=" + count1); 27 System.out.println("foo1() end " + simpleDateFormat.format(new Date())); 28 } 29 30 public static void main(String[] args) { 31 C2 c2 = new C2(); 32 new Thread(c2::foo).start(); 33 new Thread(c2::foo1).start(); 34 } 35 }
執行結果:
可以看到,兩個方法修改兩個變數,他們之間應該是沒有影響的.但現在第二個方法要等到第一個方法執行完才執行.
出現這種情況是因為,synchronized雖然加到了各自的方法上,但最終鎖是加到了this物件上.
synchronized同步鎖可以這麼來理解:當一個執行緒要執行synchronized大括號內的程式碼時,這會有一個看門的(synchronized)告訴執行緒,想要執行這段程式碼,你得去看看那邊的那個物件(this)上有沒有鎖,沒鎖你才能執行.同時你得在那個物件上掛個鎖,這樣別的執行緒就進不來了.
這樣,Thread-0進入foo方法時,就在this上掛了個鎖.這時Thread-1來執行foo1方法,雖然和Thread-0執行的不是同一塊程式碼甚至不是同一個方法.但是也被synchronized攔了下來,讓他去看this物件上有沒有鎖.這時this上有鎖.所以只能阻塞,等Thread-0執行完才能執行.
所以,修改兩個變數想互不干涉.只能鎖定兩個物件.
1 public class C2 { 2 private int count = 10; 3 private int count1 = 10; 4 private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); 5 6 private Object o = new Object(); 7 private Object o1 = new Object(); 8 9 public void bar() { 10 synchronized (o) { 11 System.out.println("bar() " + simpleDateFormat.format(new Date())); 12 try { 13 Thread.sleep(100); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 count--; 18 System.out.println(Thread.currentThread().getName() + " count=" + count); 19 System.out.println("bar() end " + simpleDateFormat.format(new Date())); 20 } 21 } 22 23 public void bar1() { 24 synchronized (o1) { 25 System.out.println("bar1() " + simpleDateFormat.format(new Date())); 26 try { 27 Thread.sleep(100); 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 } 31 count1--; 32 System.out.println(Thread.currentThread().getName() + " count1=" + count1); 33 System.out.println("bar1() end " + simpleDateFormat.format(new Date())); 34 } 35 } 36 37 public static void main(String[] args) { 38 C2 c2 = new C2(); 39 new Thread(c2::bar).start(); 40 new Thread(c2::bar1).start(); 41 } 42 }
執行結果:
可以看到bar1沒有等到bar執行完才進入方法.
synchronized鎖定在堆記憶體
1 public class C3 { 2 private int count = 10; 3 private Object o = new Object(); 4 private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); 5 6 public void foo() { 7 synchronized(o){ 8 System.out.println("foo() " + simpleDateFormat.format(new Date())); 9 //此處高能 10 o=new Object(); 11 try { 12 Thread.sleep(100); 13 } catch (InterruptedException e) { 14 e.printStackTrace(); 15 } 16 count--; 17 System.out.println(Thread.currentThread().getName() + " count=" + count); 18 System.out.println("foo() end " + simpleDateFormat.format(new Date())); 19 } 20 } 21 22 public void bar(){ 23 synchronized (o){ 24 System.out.println("bar() " + simpleDateFormat.format(new Date())); 25 //此處高能 26 o=new Object(); 27 try { 28 Thread.sleep(100); 29 } catch (InterruptedException e) { 30 e.printStackTrace(); 31 } 32 count--; 33 System.out.println(Thread.currentThread().getName() + " count=" + count); 34 System.out.println("bar() end " + simpleDateFormat.format(new Date())); 35 } 36 } 37 38 public static void main(String[] args) { 39 C3 c3=new C3(); 40 new Thread(c3::foo).start(); 41 new Thread(c3::bar).start(); 42 } 43 }
執行結果:
上面兩個方法同樣的邏輯:記錄開始和結束時間,count--,列印執行緒名和數量,synchronized鎖定的都是o.
從執行結果看,哇完全是同時執行的嘛,根本就沒有阻塞,根本就沒有原子.
注意註釋高能處o=new Object().就是上完鎖之後,他把地址給換了,指向了一個沒有鎖的記憶體推.
所以重新理解下這個過程:
當一個執行緒要執行synchronized程式碼塊的程式碼時,先去棧裡找o這個物件的引用地址,然後根據地址去堆裡找new Object()分配的記憶體,有鎖的話阻塞等待,直到解鎖.沒鎖的話就在這個區域的推記憶體上加一個鎖.這樣別的執行緒就不能執行這塊程式碼了.但上面的例子裡,執行緒把鎖加上之後,又new了一個Object,在堆裡新分配了一塊記憶體,把o處的引用地址改成了這裡.所以第二個執行緒通過o來找堆記憶體的時候,找到的是這個新分配的堆記憶體,自然是沒有加鎖的.這樣就不會阻塞等待.直接去執行了
髒讀問題
通過下面程式碼瞭解髒讀是怎麼產生的,當然髒讀不一定就是問題,允不允許髒讀要看業務.
1 public class C4 { 2 3 private int balance = 0; 4 5 public synchronized void addBalance(int money) { 6 try { 7 Thread.sleep(100); 8 } catch (InterruptedException e) { 9 e.printStackTrace(); 10 } 11 balance += money; 12 } 13 14 public void showBalance() { 15 System.out.println(balance); 16 } 17 18 public synchronized void showBalanceSync() { 19 System.out.println(balance); 20 } 21 22 public static void main(String[] args) { 23 C4 c4 = new C4(); 24 new Thread(() -> c4.addBalance(2)).start(); 25 new Thread(c4::showBalance).start(); 26 try { 27 Thread.sleep(100); 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 } 31 new Thread(c4::showBalance).start(); 32 33 try { 34 Thread.sleep(1000); 35 } catch (InterruptedException e) { 36 e.printStackTrace(); 37 } 38 System.out.println("---分割線---"); 39 new Thread(() -> c4.addBalance(3)).start(); 40 new Thread(c4::showBalanceSync).start(); 41 } 42 }
執行結果:
先執行餘額加2,馬上去讀,讀書來是0.睡100毫秒再去讀,讀出來的才是2.因為寫餘額的方法加鎖了,但是讀的方法沒加鎖.所以寫入餘額還沒執行完的時候去讀,讀的是以前的.
分割線下面的部分呼叫的是加同步鎖的讀餘額,寫入餘額加3沒執行完的時候,讀方法時阻塞的.等寫完後才開始讀,所以讀出來的是5.
synchronized重入
1 public class C5 { 2 3 private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); 4 5 public synchronized void foo(){ 6 System.out.println("foo() " + simpleDateFormat.format(new Date())); 7 try { 8 Thread.sleep(100); 9 } catch (InterruptedException e) { 10 e.printStackTrace(); 11 } 12 System.out.println("foo() end " + simpleDateFormat.format(new Date())); 13 } 14 15 public synchronized void bar(){ 16 System.out.println("bar() " + simpleDateFormat.format(new Date())); 17 try { 18 Thread.sleep(100); 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } 22 //當前執行緒內執行foo() 23 foo(); 24 System.out.println("bar() end " + simpleDateFormat.format(new Date())); 25 } 26 27 public static void main(String[] args) { 28 C5 c5=new C5(); 29 new Thread(c5::bar).start(); 30 } 31 }
執行結果:
類裡有兩個同步方法,都鎖定的是當前物件this.執行bar的時候加了鎖.在bar方法中呼叫了foo,而foo也是同步方法,也需要判斷this上有沒有鎖.那這時foo方法會阻塞嗎.從執行結果上看,並沒有阻塞.因為bar和foo方法在同一個執行緒內執行,當執行緒執行foo方法時去記憶體堆裡看有沒有鎖時,這時有鎖,但這個鎖是剛才自己掛上去的,所以就繼續往下執行了.如果不這樣做,方法就永遠執行不了,foo方法需要bar釋放鎖才能執行,而bar方法沒有執行完foo不會釋放鎖.所以可以理解成鎖的使用權在當前執行緒.
為了說明這個問題,看下面例子
public class C5 { private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); public synchronized void foo(){ System.out.println("foo() " + simpleDateFormat.format(new Date())); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("foo() end " + simpleDateFormat.format(new Date())); } public synchronized void bar1(){ System.out.println("bar1() " + simpleDateFormat.format(new Date())); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //新開一個執行緒執行foo() new Thread(this::foo).start(); System.out.println("bar1() end " + simpleDateFormat.format(new Date())); } public static void main(String[] args) { C5 c5=new C5(); new Thread(c5::bar1).start(); } }
執行結果:
和上面唯一的區別就是bar1方法呼叫foo的時候,是開了一個執行緒呼叫的.如結果所示,執行foo的時候阻塞了,bar1執行完釋放鎖之後,foo才開始執行.
synchronized死鎖
最簡單的死鎖情況如下例所示
1 public class C6 { 2 3 private Object o = new Object(); 4 private Object o1 = new Object(); 5 6 public void foo() { 7 System.out.println("foo()"); 8 synchronized (o) { 9 try { 10 Thread.sleep(100); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 System.out.println("foo() lock o"); 15 synchronized (o1){ 16 System.out.println("foo() lock o1"); 17 } 18 } 19 } 20 21 public void bar(){ 22 System.out.println("bar()"); 23 synchronized (o1){ 24 System.out.println("bar() lock o1"); 25 synchronized (o){ 26 System.out.println("bar() lock o"); 27 } 28 } 29 } 30 31 public static void main(String[] args){ 32 C6 c6=new C6(); 33 new Thread(c6::foo).start(); 34 new Thread(c6::bar).start(); 35 }
執行結果:
這個結果是永遠也執行不完的.因為死鎖了.o等著o1釋放,o1等著o釋放.天長地久海枯石爛.
synchronized鎖會在丟擲異常時被釋放
1 public class C7 { 2 3 private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); 4 5 public synchronized void foo(){ 6 System.out.println("foo() "+simpleDateFormat.format(new Date())); 7 //處理邏輯1耗時100毫秒 8 try { 9 Thread.sleep(100); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 foo1(); 14 //處理邏輯2耗時100毫秒 15 try { 16 Thread.sleep(100); 17 } catch (InterruptedException e) { 18 e.printStackTrace(); 19 } 20 } 21 22 private void foo1(){ 23 throw new RuntimeException("假設處理foo1邏輯的時候出了異常"); 24 } 25 26 public synchronized void bar(){ 27 System.out.println("bar() "+simpleDateFormat.format(new Date())); 28 } 29 30 public static void main(String[] args) { 31 C7 c7=new C7(); 32 new Thread(c7::foo).start(); 33 /** 34 * 正常foo執行需要200毫秒 200毫秒後釋放鎖 bar執行 35 * 現在foo執行到100毫秒時 foo1執行 foo1執行時丟擲了異常 鎖被釋放 36 * 所以bar實際在foo執行100毫秒後執行 37 */ 38 new Thread(c7::bar).start(); 39 } 40 }
執行結果:
注意執行時間,相差101毫秒,遠不到200毫秒.