Java並發編程從入門到精通 - 第3章:Thread安全
Java內存模型與多線程:
線程不安全與線程安全:
線程安全問題闡述:
多條語句操作多個線程共享的資源時,一個線程只執行了部分語句,還沒執行完,另一個線程又進來操作共享數據(執行語句),導致共享數據最終結果出現誤差;所以就是看一個線程能否每次在沒有其他線程進入的情況下操作完包含共享資源的語句塊,如果能就沒有安全問題,不能就有安全問題;
如何模擬多線程的安全問題:
用Thread.sleep()方法模擬; 放在哪:放在多線程操作共享數據的語句塊之間(使正在運行的線程休息一會,讓其他線程執行,就會出現共享數據錯誤的問題);
如何解決線程安全問題:
解決思想:
只有當一個線程執行完所有語句之後,才能讓另外一個線程進來再進行操作;
具體操作:
加鎖,對操作共享數據的代碼塊加鎖,實現在一個線程操作共享數據時,其它線程不能再進來操作,直到本線程執行完之後其它線程才能進來執行;
哪些代碼塊需要加鎖(同步):
明確每個線程都會操作的代碼塊;
明確共享資源;
明確代碼塊中操作共享資源語句塊,這些語句塊就是需要加鎖的代碼塊;
具體解決方式:(以synchronized為例)
同步代碼塊:
synchronized(對象)
{
需要被同步的代碼塊;
}
同步方法:
就是把需要同步的代碼塊放到一個函數裏面,代碼塊原來所在的函數裏面可能還有其他不需要同步的代碼塊(所以不能每次直接同步原來所在的方法),需要仔細分析;
確保沒有線程安全問題的兩個前提:
至少有兩個及兩個以上的線程操作共享資源;
所有線程使用的鎖是同一個鎖;
註意:加了鎖之後還出現線程安全問題的話,說明上面兩個前提肯定沒有全部滿足;
想實現線程安全大致有三種方法:
多實例,也就是不使用單例模式了(單例模式在多線程下是不安全的);
使用java.util.concurrent下面的類庫;
使用鎖機制synchronized、lock方式;
為什麽單例在多線程下是不安全的:
因為在多線程下可能會創建多個實例,不能保證原子性,違背設計單例模式的初衷;
synchronized:
詳解:
隱式鎖,同步鎖,內置鎖,監視器鎖,可重入鎖;
為了解決線程同步問題而生;
當用它來修飾一個代碼塊或一個方法時,能夠保證在同一時刻最多只有一個線程執行該段代碼(或方法);
采用synchronized修飾符實現的同步機制叫做互斥鎖機制,它所獲得的鎖(對象,鎖必須是對象,就是引用類型,不能是基本數據類型)叫做互斥鎖;每個對象都有一個monitor(鎖標記),當線程擁有這個鎖標記時才能訪問這個資源,沒有鎖標記便進入鎖池(誰進入鎖池);對於任何一個對象,系統都會為其創建一個互斥鎖,這個鎖是為了分配給線程的,防止打斷原子操作;每個對象的鎖只能分配給一個線程,因此叫做互斥鎖;
是可重入鎖:一個線程可以多次獲得同一個對象的互斥鎖;
使用同步機制獲取互斥鎖的規則說明:
如果同一個方法內同時有兩個或更多線程,則每個線程有自己的局部變量拷貝;(不解)
類的每個實例都有自己的對象級別鎖(一個實例對象就是一個互斥鎖,同一個類的兩個實例對象對應的互斥鎖是不一樣的);當一個線程訪問實例對象中的synchronized同步代碼塊或同步方法時,該線程便獲取了該實例的對象級別鎖(就是當前對象的意思);
持有一個對象級別鎖不會阻止該線程被交換出來(不解),也不會阻塞其他線程訪問同一實例對象中的非synchronized代碼;
持有對象級別鎖的線程會讓其他線程阻塞在所有的synchronized代碼外;
使用synchronized(obj)同步語句塊,可以獲取指定對象上的對象級別鎖;
類級別鎖被特定類的所有實例共享,它用於控制對static成員變量以及static方法的並發訪問;具體用法與對象級別鎖相似;
synchronized的不同寫法對於性能和執行效率的優劣程度排序:
同步方法體 < 同步方法塊(鎖不是最小的鎖) < 同步方法塊(鎖是最小的鎖);
顯式鎖:
詳解:
為什麽叫顯式鎖,因為所有加鎖和解鎖的方法都是顯示的,即必須手動加鎖和釋放鎖;
為了保證鎖最終一定會被釋放(可能會有異常發生),要把互斥區放在try語句塊內,並在finally語句塊中釋放鎖;尤其當有return語句時,return語句必須放在try字句中,以確保unlock()不會過早發生,從而將數據暴露給第二個任務;
采用lock加鎖和釋放鎖的一般形式如下:
接口:Lock ReadWriteLock
實現類:ReentrantLock ReentrantReadWriteLock
ReentrantLock是Lock接口的實現類, ReentrantReadWriteLock是ReadWriteLock接口的實現類;
ReadWriteLock並不是Lock的子接口,只是ReadWriteLock借助Lock來實現讀寫兩個鎖的並存、互斥的機制;
Lock:是一個接口,提供了無條件的、可輪詢的、定時的、可中斷的鎖獲取操作,所有加鎖和解鎖的方法都是顯示的;
ReentrantLock:在競爭條件下,ReentrantLock的實現要比現在的synchronized實現更具有可伸縮性;(有可能在JVM的將來版本中改進synchronized的競爭性能)這意味著當許多線程都競爭相同鎖定時,使用ReentrantLock的吞吐量通常要比synchronized好;換句話說,當許多線程試圖訪問ReentrantLock保護的共享資源時,JVM將花費較少的時間來調度線程,而用更多時間執行線程;ReentrantLock是在工作中對方法塊加鎖使用頻率最高的;但ReentrantLock也有一個主要缺點:它可能忘記釋放鎖定;
Lock和synchronized的比較:
Lock使用起來比較靈活,但是必須有釋放鎖的動作配合;
Lock必須手動釋放和開啟鎖,而synchronized不需要手動釋放和開啟鎖;
Lock只適用於代碼塊鎖,而synchronized對象之間是互斥關系;
ReadWriteLock接口:
提供了readLock和writeLock兩種鎖的操作機制;
一個資源能夠被多個讀線程訪問,或者被一個寫線程訪問,但是不能同時存在讀寫線程;也就是說讀寫鎖適用的場合是一個共享資源被大量讀取操作,而只有少量的寫操作;
在ReadWriteLock中,每次讀取共享數據就需要讀取鎖,當需要修改共享數據時就需要寫入鎖;看起來好像是兩個鎖,其實不是;
ReentrantReadWriteLock類:
ReadWriteLock接口唯一的實現類;
主要應用場景是:當有很多線程都從某個數據結構讀取數據,而很少有線程對其進行修改時,在這種情況下,允許讀取器線程共享訪問是合適的,寫入器線程依然必須是互斥訪問的;
主要特性:
公平性:
重入性:
鎖降級:
鎖升級:
鎖獲取中斷:
條件變量:
重入數:
以上概括起來就是讀寫鎖的機制:讀-讀不互斥、讀-寫互斥、寫-寫互斥;
ReentrantReadWriteLock與ReentrantLock的比較:
相同點:都是顯式鎖,需要手動加鎖解鎖,都很適合高並發場景;
不同點:
ReentrantReadWriteLock是對ReentrantLock的復雜擴展,能適合更加復雜的業務;
ReentrantReadWriteLock能實現一個方法中讀寫分離的鎖的機制,而ReentrantLock加鎖解鎖只有一種機制;
顯式鎖(Lock)和隱式鎖(synchronized)的比較:
ReentrantLock主要增了如下的高級功能:
1、等待可中斷:
當持有鎖的線程長期不釋放鎖時,正在等待的線程可以選擇放棄等待,改為處理其他事情,它對處理執行時間上的同步塊很有幫助;而在等待由synchronized產生的互斥鎖時,會一直阻塞,是不能被中斷的;
2、可實現公平鎖:
多個線程在等待同一個鎖時,必須按照申請鎖的時間順序排隊等待;而非公平鎖則不保證這點,在鎖釋放時,任何一個等待鎖的線程都有機會獲得鎖; synchronized中的鎖是非公平鎖,ReentrantLock默認情況下也是非公平鎖,但可以通過構造方法ReentrantLock(ture)來要求使用公平鎖;
3、鎖可以綁定多個條件:
ReentrantLock對象可以同時綁定多個Condition對象(名曰:條件變量或條件隊列);而在synchronized中,鎖對象的wait()和notify()或notifyAll()方法可以實現一個隱含條件,但如果要和多於一個的條件關聯的時候,就不得不額外地添加一個鎖;而ReentrantLock則無需這麽做,只需要多次調用newCondition()方法即可;而且我們還可以通過綁定Condition對象來判斷當前線程通知的是哪些線程(即與Condition對象綁定在一起的其他線程);
其他方面的比較:
synchronized:讀寫互斥、寫寫互斥、讀讀互斥(讀讀操作不會引發安全問題);ReentrantReadWriteLock(讀寫鎖):讀寫互斥、寫寫互斥、讀寫不互斥;
悲觀鎖 與 樂觀鎖:
悲觀鎖:顧名思義,就是很悲觀,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖;傳統的關系型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖;
樂觀鎖:
樂觀鎖:顧名思義,就是很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制;樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫如果提供類似於write_condition機制的其實都是提供的樂觀鎖;
兩種鎖各有優缺點,不可認為一種好於另一種,像樂觀鎖適用於寫比較少的情況下,即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量;但如果經常產生沖突,上層應用會不斷的進行retry,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適;
顯示鎖StampedLock:
該類是一個讀寫鎖的改進,它的思想是讀寫鎖中讀不僅不阻塞讀,同時也不應該阻塞寫;
讀不阻塞寫的實現思路:
在讀的時候如果發生了寫,則應當重讀而不是在讀的時候直接阻塞寫!
因為在讀線程非常多而寫線程比較少的情況下,寫線程可能發生饑餓現象,也就是因為大量的讀線程存在並且讀線程都阻塞寫線程,因此寫線程可能幾乎很少被調度成功!當讀執行的時候另一個線程執行了寫,則讀線程發現數據不一致則執行重讀即可。所以讀寫都存在的情況下,使用StampedLock就可以實現一種無障礙操作,即讀寫之間不會阻塞對方,但是寫和寫之間還是阻塞的!
死鎖:
在兩段不同的邏輯都在等待對方的鎖釋放才能繼續往下工作時,這個時候就會產生死鎖;
1 /** 2 * 死鎖 3 */ 4 package thread02; 5 6 class Count11 7 { 8 private byte[] lock1 = new byte[1]; 9 private byte[] lock2 = new byte[2]; 10 11 private int num = 0; 12 13 public void add1() 14 { 15 synchronized (lock1) 16 { 17 try 18 { 19 Thread.sleep(1001); // 模擬幹活 20 } 21 catch (InterruptedException e) 22 { 23 e.printStackTrace(); 24 } 25 26 synchronized (lock2) // 產生死鎖,一直在等待lock2對象釋放鎖 27 { 28 num += 1; 29 } 30 31 System.out.println(Thread.currentThread().getName() + " - " + num); 32 } 33 } 34 35 public void add2() 36 { 37 synchronized (lock2) 38 { 39 try 40 { 41 Thread.sleep(1001); 42 } 43 catch (InterruptedException e) 44 { 45 e.printStackTrace(); 46 } 47 48 synchronized (lock1) // 產生死鎖,一直等待lock1對象釋放鎖 49 { 50 num += 1; 51 } 52 53 System.out.println(Thread.currentThread().getName() + " - " + num); 54 } 55 } 56 } 57 58 public class DeadLockTest01 59 { 60 public static void main(String[] args) 61 { 62 Count11 count = new Count11(); 63 64 ThreadA threadA = new ThreadA(count); 65 Thread t1 = new Thread(threadA); 66 t1.setName("線程A"); 67 68 ThreadB threadB = new ThreadB(count); 69 Thread t2 = new Thread(threadB); 70 t2.setName("線程B"); 71 } 72 73 } 74 75 class ThreadA implements Runnable 76 { 77 private Count11 count; 78 79 public ThreadA(Count11 count) 80 { 81 this.count = count; 82 } 83 84 @Override 85 public void run() 86 { 87 count.add1(); 88 } 89 } 90 91 class ThreadB implements Runnable 92 { 93 private Count11 count; 94 95 public ThreadB(Count11 count) 96 { 97 this.count = count; 98 } 99 100 @Override 101 public void run() 102 { 103 count.add2(); 104 } 105 }死鎖
volatile:
表面意思:易變的,不穩定的;
作用:修飾變量;告訴編譯器,凡是被該關鍵字聲明的變量都是易變的,不穩定的;所以不要試圖對該變量使用緩存等優化機制,而應當每次都從它的內存地址中去讀取值;
特性:
使用volatile標記的變量在讀取或寫入時不需要使用鎖,這將減少產生死鎖的概率,使代碼保持整潔;
每次讀取volatile的變量都要從它的內存地址中讀取,但並不是每次修改完volatile的變量後都要立刻將它的值寫回內存;也就是說volatile只提供內存可見性,而沒有提供原子性;所以使用這個關鍵字做高並發的安全機制是不可靠的;
適用場景:
最好是那種只有一個線程修改變量,多個線程讀取該變量的地方;也就是對內存可見性要求高,而對原子性要求低的地方;
volatile與加鎖機制的主要區別:
加鎖機制既可以保證可見性又可以確保原子性;而volatile變量只能保證可見性;
atomic:
詳述:
atomic是不會阻塞線程(或者說只是在硬件級別上阻塞了),線程安全的加強版的volatile原子操作;
java.util.concurrent.atomic包裏,多了一批原子操作,主要用於高並發環境下的高效程序處理;
1 /** 2 * 原子操作 3 */ 4 package thread02; 5 6 import java.util.concurrent.atomic.AtomicInteger; 7 8 public class AtomicIntegerTest01 9 { 10 public static void main(String[] args) 11 { 12 AtomicInteger ai = new AtomicInteger(0); 13 14 // 獲取當前的值 15 System.out.println(ai.get()); 16 System.out.println("--------------"); 17 18 // 取當前的值,並設置新的值 19 System.out.println(ai.getAndSet(5)); 20 System.out.println(ai.get()); // 設置新值之後的當前值 21 System.out.println("--------------"); 22 23 // 獲取當前的值,並自增 24 System.out.println(ai.getAndIncrement()); 25 System.out.println(ai.get()); // 自增之後的當前值 26 System.out.println("--------------"); 27 28 // 獲取當前的值,並自減 29 System.out.println(ai.getAndDecrement()); 30 System.out.println(ai.get()); // 自減之後的當前值 31 System.out.println("--------------"); 32 33 // 獲取當前的值,並加上預期的值 34 System.out.println(ai.getAndAdd(3)); 35 System.out.println(ai.get()); // 加上預期值之後的當前值 36 System.out.println("--------------"); 37 38 } 39 }原子操作
單例:
1 /** 2 * 單例模式第一種寫法:線程不安全的,不正確的寫法 3 */ 4 package thread02.singleton; 5 6 public class Singleton01 7 { 8 private static Singleton01 instance; 9 10 private Singleton01() 11 { 12 13 } 14 15 public static Singleton01 getSingleton() 16 { 17 if(instance == null) 18 { 19 instance = new Singleton01(); 20 } 21 22 return instance; 23 } 24 }單例模式第一種寫法:線程不安全的,不正確的寫法
1 /** 2 * 單例模式第二種寫法:線程安全,但是高並發性能不是很高 3 */ 4 package thread02.singleton; 5 6 public class Singleton02 7 { 8 private static Singleton02 instance; 9 10 private Singleton02() 11 { 12 13 } 14 15 public static synchronized Singleton02 getSingleton() 16 { 17 if(instance == null) 18 { 19 instance = new Singleton02(); 20 } 21 22 return instance; 23 } 24 }單例模式第二種寫法:線程安全,但是高並發性能不是很高
1 /** 2 * 單例模式第三種寫法:線程安全,性能又高,這種寫法最為常用 3 */ 4 package thread02.singleton; 5 6 public class Singleton03 7 { 8 private static Singleton03 instance; 9 private static byte[] lock = new byte[0]; 10 11 private Singleton03() 12 { 13 14 } 15 16 public static Singleton03 getSingleton() 17 { 18 if(instance == null) 19 { 20 synchronized (lock) 21 { 22 if(instance == null) 23 { 24 instance = new Singleton03(); 25 } 26 } 27 } 28 29 return instance; 30 } 31 }單例模式第三種寫法:線程安全,性能又高,這種寫法最為常用
1 /** 2 * 單例模式第四種寫法:線程安全,性能又高,也是最為常用的 3 */ 4 package thread02.singleton; 5 6 import java.util.concurrent.locks.ReentrantLock; 7 8 public class Singleton04 9 { 10 private static Singleton04 instance; 11 private static ReentrantLock lock = new ReentrantLock(); 12 13 private Singleton04() 14 { 15 16 } 17 18 public static Singleton04 getSingleton() 19 { 20 if(instance == null) 21 { 22 lock.lock(); 23 24 if(instance == null) 25 { 26 instance = new Singleton04(); 27 } 28 29 lock.unlock(); 30 } 31 32 return instance; 33 } 34 }單例模式第四種寫法:線程安全,性能又高,也是最為常用的
Java並發編程從入門到精通 - 第3章:Thread安全