目標
本篇博文作為多線程技術的讀書筆記,主要學習了以下知識點:
- synchronized對象監視器為Object時的使用
- synchronized對象監視器為Class時的使用
- 非線程安全是如何出現的
- 關鍵字volatile的主要作用
- 關鍵字volatile與synchronized的區別及使用
方法內的變量為線程安全
“非線程安全”問題存在於“實例變量”中,如果是方法內部的私有變量,則不存在“非線程安全”問題,這是方法內部的變量都是線程私有的特性造成的。
實例變量非線程安全
如果多個線程共同訪問一個對象中的實例變量,則有可能出現“非線程安全”問題。
synchronized鎖重入
關鍵字synchronized擁有鎖重入功能,即當使用synchronized時,當一個線程得到一個對象鎖后,再次請求此對象鎖時是可以再次得到該對象的鎖的,這也證明在一個synchronized方法/塊的內部調用本來的其他synchronized方法/塊時,是永遠可以得到鎖的。
可重入鎖也同樣適用於父子集成關系中,子類是完全可以通過“可重入鎖”調用父類的同步方法的。
出現異常,鎖自動釋放
當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。
同步不具有繼承性
如果父類某個方法是同步的,子類某個方法是非同步的,子類的非同步方法調用了父類中的同步方法,在多線程環境下調用子類的該同步方法,是無法同步的,必須子類的這個方法也實現同步。
synchronized同步語句塊
用關鍵字synchronized聲明方法在某些情況下是有弊端的,比如線程A調用同步方法執行一個長時間的任務,那么線程B線程則必須等待比較長時間的,即這個方法鎖住的資源太多,鎖粒度太大,不利於業務高效運行,這個時候我們就可以鎖同步塊。
synchronized代碼塊間的同步性
在使用同步synchronized(this)代碼塊時需要注意的是,當一個線程訪問Object的一個synchronized(this)同步代碼塊時,其他線程對同一個Object中所有其他synchronzied(this)同步代碼塊的訪問將被阻塞,這說明此時synchronized使用的是同一個對象監視器(同一個對象鎖)。
synchronized(this)同步方法持有的是對象鎖。
將任意對象作為對象監視器
多個線程調用同一個對象中的不同名稱的synchronized同步方法或者synchronized(this)同步代碼塊時,調用的效果就是按順序執行,也就是同步的,阻塞的。
在多個線程持有的對象監視器為同一個對象的前提下,同一時間只喲局一個線程可以執行synchronized(對象)同步方法/代碼塊中的內容。
鎖非this對象具有一定的優先:如果在一個類中有很多個synchronized方法,這時雖然能實現同步,但會收到阻塞,所以影響運行效率;但如果使用代碼塊鎖非this對象,則synchronized(對象)代碼塊中的程序與同步方法是異步的,不予其他鎖this同步方法爭搶this鎖,則可以大大提高運行效率。
靜態同步synchronized方法與synchronized(class)代碼塊
關鍵字synchronized應用在static靜態方法上,那就是對當前*.java文件對應的Class類進行持鎖。
synchronized public static void pringA(){}
synchronized關鍵字加到static靜態方法上是給Class類上鎖,而synchronized關鍵字加到非static靜態方法上是給對象上鎖。
數據類型String的常量池特性
在JVM中具有String常量池緩存的功能,比如以下:
1 String a = "a"; 2 String b = "a"; 3 System.out.println(a == b); 4 // true
即兩個不同的String對象當時如果值相同,兩個對象也是相等的,這就是String的常量池緩存特性。
當我們的synchronized(string)同步塊與String聯合使用時,就可能會應為這個常量池特性而帶來不受控的一些影響了。因為如果兩個不相干的線程在調用此次方法時,使用了相同的String值,那么原本不希望阻塞的異步方法此時也阻塞了。
1 public static void print(String str) { 2 synchronized (str) { 3 System.out.println(Thread.currentThread().getName()); 4 } 5 }
因此大多數情況下,同步synchronized代碼塊都不使用String作為鎖對象,而改用其他如Object對象。
多線程的死鎖
多線程的鎖使用不當,很容易引起線程間的死鎖,下面通過一個簡單的例子來說明死鎖。
1 public class MyThread09 extends Thread { 2 3 public String flag; 4 public Object lock1 = null; 5 public Object lock2 = null; 6 7 public MyThread09(String flag, Object lock1, Object lock2) { 8 this.flag = flag; 9 this.lock1 = lock1; 10 this.lock2 = lock2; 11 } 12 13 @Override 14 public void run() { 15 if(flag.equals("a")) { 16 synchronized (lock1) { 17 System.out.println("=== print A"); 18 try { 19 Thread.sleep(3000L); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 synchronized (lock2) { 24 System.out.println("=== print A again"); 25 } 26 } 27 } 28 if(flag.equals("b")) { 29 synchronized (lock2) { 30 System.out.println("=== print B"); 31 try { 32 Thread.sleep(5000L); 33 } catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 synchronized (lock1) { 37 System.out.println("=== print B again"); 38 } 39 } 40 } 41 } 42 43 public static void main(String[] args) { 44 // lock1,lock2作為鎖,或者說臨界區資源 45 Object lock1 = new Object(); 46 Object lock2 = new Object(); 47 Thread threadA = new MyThread09("a", lock1, lock2); 48 Thread threadB = new MyThread09("b", lock1, lock2); 49 threadA.start(); 50 threadB.start(); 51 } 52 } 53 54 運行結果: 55 === print A 56 === print B 57 (程序無法停止)
當程序正在運行時,我們打開JDK自帶的jps.exe來查看當前正在運行程序的ID值。
PS C:\Program Files\Java\jdk1.8.0_131\bin> .\jps.exe 25856 MyThread09 29152 Appliaction 34688 Jps 33972 Launcher 35780 Launcher
然后再執行jstack命令,查看具體的堆棧細節。
Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x000000001c30dc48 (object 0x000000076b39f1a8, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x000000001c310638 (object 0x000000076b39f1b8, a java.lang.Object), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at com.captainad.MyThread09.run(MyThread09.java:46) - waiting to lock <0x000000076b39f1a8> (a java.lang.Object) - locked <0x000000076b39f1b8> (a java.lang.Object) "Thread-0": at com.captainad.MyThread09.run(MyThread09.java:33) - waiting to lock <0x000000076b39f1b8> (a java.lang.Object) - locked <0x000000076b39f1a8> (a java.lang.Object) Found 1 deadlock.
從堆棧中可以看到程序已經發生了死鎖,兩個線程在持有不同的鎖的情況下,還繼續等待着獲取對方持有的還沒有釋放的鎖,形成了相互等待,最終造成程序死鎖。
鎖對象的改變
如果鎖是String類型的,這個類型值后續改變了,鎖也會隨之改變,即爭奪的資源就改變了。
如果鎖的是對象,對象的屬性在某個線程中改變了,線程間相互競爭的鎖還是同一個對象的,不會因為對象的值改變而變更。
volatile關鍵字
關鍵字volatile的主要作用是使變量的變更在多線程間可見。
通過使用volatile關鍵字,強制線程從公共內存中讀取變量的值,volatile關鍵字保證了變量在多線程間的可見性,但是該關鍵字不支持原子性。
下面將關鍵字synchronized和volatile做一個簡單的比較:
- 關鍵字volatile是線程同步的輕量級實現,所以volatile性能肯定比synchronized要好,並且volatile只能修飾於變量,而synchronized可以修飾方法以及代碼塊。隨着JDK版本的升級,synchronized關鍵字在執行效率上得到了很大的提升,在后續使用synchronized關鍵字還是可以考慮的。
- 多線程訪問volatile不會發生阻塞,而synchronized會發生阻塞。
- volatile能保證數據的可見性,但不能保證原子性;synchronized可以保證原子性,也可以間接保證可見性,因為他會將私有內存和公共內存中的數據做同步。
- 關鍵字volatile解決的是變量在多個線程之間的可見性,而synchronized關鍵字解決的是多個線程之間訪問資源的同步性。
volatile非原子特性
(Java多線程編程核心技術 截圖)
1 volatile int i = 0; 2 Thread: i++;
在多線程環境下,即使給變量i加了volatile修飾,也是無法保證其計算的原子性的,上面的截圖給出了解釋,那么要做到一個多線程情況下使用的計數器該如何處理呢?
- 使用synchronized關鍵字來做同步
- 使用AtomicInteger原子類進行實現
總結
多線程的使用會產生線程安全問題,處理好線程安全問題也是我們學習多線程知識中需要掌握的一個技能。
關鍵字synchronized在處理同步塊時,可能會出現多種情況,下面做個簡單總結:
- 指定加鎖對象:對給定對象加鎖,進入同步代碼前要獲得給定對象的鎖。
- 直接作用於實例方法:相當於對當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。
- 直接作用於靜態方法:相當於對當前類加鎖,進入同步代碼前要獲得當前類的鎖。