無狀態對象一定是線程安全的
競態條件
當某個計算的正確性取決于多個線程的交替執行時序時,那么久會發生競態條件。換句話說,就是正確的結果要取決于運氣。最常見的競態條件類型就是“先檢查后執行”操作,即通過一個可能失效的觀測結果來決定下一步的動作。
當把變量聲明為volatile類型后,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值
理解volatile變量的一種有效方法是,將volatile變量的讀操作和寫操作分別替換成get和set方法。然而,在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比synchronized關鍵字更輕量級的同步機制
加鎖機制既可以確保可見性又可以確保原子性,而volatile變量只能確保可見性
當且僅當滿足以下所有條件時,才應該使用volatile變量
- 對變量的寫入操作不依賴變量當前值,或者能確保只有單個線程更新變量的值
- 該變量不會與其他狀態變量一起納入不變性條件中
- 在訪問變量時不需要加鎖
線程安全及不可變性
當多個線程同時訪問同一個資源,并且其中的一個或者多個線程對這個資源進行了寫操作,才會產生競態條件。多個線程同時讀同一個資源不會產生競態條件。
我們可以通過創建不可變的共享對象來保證對象在線程間共享時不會被修改,從而實現線程安全。
“不變”(immutable)和“只讀”(Read Only)是不同的,當一個變量是“只讀”時,變量的值不能直接改變,但是可以在其他變量發生改變的時候發生改變。
引用不是線程安全的
即使一個對象是線程安全的不可變對象,指向這個對象的引用也可能不是線程安全的
線程的創建方式
Java 提供了三種創建線程的方法:
- 通過實現 Runnable 接口;
- 通過繼承 Thread 類本身;
- 通過 Callable 和 Future 創建線程。
Java多線程編程
Java線程(七):Callable和Future
Java并發編程:Callable、Future和FutureTask
線程池實現
-
使用wait/notify/notifyAll實現線程間通信的幾點重要說明
在Java中,可以通過配合調用Object對象的wait()方法和notify()方法或notifyAll()方法來實現線程間的通信。在線程中調用wait()方法,將阻塞等待其他線程的通知(其他線程調用notify()方法或notifyAll()方法),在線程中調用notify()方法或notifyAll()方法,將通知其他線程從wait()方法處返回。
如果線程調用了對象的wait()方法,那么線程便會處于該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。
當有線程調用了對象的notifyAll()方法(喚醒所有wait線程)或notify()方法(只隨機喚醒一個wait線程),被喚醒的線程變回進入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。
優先級高的線程競爭到對象鎖的概率大,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中,唯有線程再次調用wait()方法,它才會重新回到等待池中。而競爭到對象鎖的線程則繼續往下執行,直到執行完了synchronized代碼塊,它會釋放該對象鎖,這時鎖池中的線程會繼續競爭該對象鎖。
-
線程間通信中notify通知的遺漏(含代碼)
notify通知的遺漏很容易理解,即threadA還沒開始wait的時候,threadB已經notify了,這樣,threadB通知是沒有任何響應的,當threadB退出synchronized代碼塊后,threadA再開始wait,便會一直阻塞等待,直到被別的線程打斷。
在使用線程的等待/通知機制時,一般都要配合一個boolean變量值(或者其他能夠判斷真假的條件),在notify之前改變該boolean變量的值,讓wait返回后能夠退出while循環(一般都要在wait方法外圍加一層while循環,以防止早期通知),或在通知被遺漏后,不會被阻塞在wait方法處。這樣便保證了程序的正確性。
-
線程間通信中notifyAll造成的早期通知問題(含代碼)
如果線程在等待時接到通知,但線程等待的條件還不滿足,此時,線程接到的就是早期通知,如果條件滿足的時間很短,但很快又改變了,而變得不再滿足,這時也將發生早期通知。wait的線程被notif之后從wait之后的代碼開始執行,所以進入wait的條件有可能發生了改變,需要用while來判斷
在使用線程的等待/通知機制時,一般都要在while循環中調用wait()方法,滿足條件時,才讓while循環退出,這樣一般也要配合使用一個boolean變量(或其他能判斷真假的條件,如本文中的list.isEmpty()),滿足while循環的條件時,進入while循環,執行wait()方法,不滿足while循環的條件時,跳出循環,執行后面的代碼。
-
并發新特性—Lock鎖和條件變量(含代碼)
- 簡單實用Lock鎖
Java 5中引入了新的鎖機制--java.util.concurrent.locks中的顯式的互斥鎖:Lock接口,它提供了比synchronized更加廣泛的鎖機制。
Lock接口有3個實現類:ReentrantLock、ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock,即重入鎖,讀鎖和寫鎖
Lock必須被顯式的創建、鎖定和釋放
- ReentrantLock和synchronized比較
ReentrantLock相對synchronized增加了一些高級功能,如下:
- 等待可中斷:當持有鎖的線程長期不釋放鎖時,正在等待的線程可以選擇放棄等待,改為處理其他事情。而在等待由synchronized產生的互斥鎖,會一直阻塞,是不能被中斷
- 可實現公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序排隊等待,而非公平鎖則不保證這點,在鎖釋放時,任何一個等待鎖的線程都有機會獲得鎖。synchronized中的鎖是非公平鎖,ReentrantLock默認情況下也是非公平鎖,但可以通過構造方法ReentrantLock(true)來要求使用公平鎖
- 鎖可以綁定多個條件:ReentrantLock對象可以同時綁定多個Condition對象(條件變量或者條件隊列)
- 讀寫鎖
synchronized獲得互斥鎖不僅互斥讀寫操作、寫寫操作,還互斥讀讀操作,而讀讀操作時不會帶來數據競爭,因此對讀讀操作也互斥的話會降低性能。Java 5中提供了讀寫鎖,它將讀鎖和寫鎖分離,使得讀讀操作不互斥,獲取讀鎖和寫鎖的一般形式如下:
ReadWriteLock rwl = new ReentrantReadWriteLock();
rwl.writeLock().lock(); //獲取寫鎖
rwl.readLock().lock(); //獲取讀鎖
- 使用ReentrantLock的最佳時機:當你需要以下高級特性時,才應該使用:可定時的、可輪詢的與可中斷的鎖獲取操作,公平隊列或者非塊結構的鎖。否則,請使用synchronized
-
可重入鎖是一種特殊的互斥鎖,它可以被同一個線程多次獲取,而不會產生死鎖
- 首先它是互斥鎖:任意時刻,只有一個線程鎖。即假如A線程已經獲取了鎖,在A線程釋放這個鎖之前,B線程是無法獲取這個鎖的,B要獲取這個鎖就要進入阻塞狀態
- 其次,它可以被同一個線程多次持有。即,假如A線程已經獲取了這個鎖,如果A線程在釋放鎖之前又一次請求獲取這個鎖,那么是能夠成功的
-
Java中的可重入鎖
1. synchronized
synchronized是Java提供的內置鎖,是互斥鎖,又是可重入鎖
synchronized關鍵字有三種用法:
* 加在對象方法上:鎖住的是當前對象,不同的對象可以被同時訪問
* 加在類方法上:鎖住的是當前類對應在JVM中的Class對象
* 加在代碼塊上:鎖住的是synchronized(lockobj)中的lockobj
2. ReentrantLock
ReentrantLock是java提供的顯示鎖,它也是互斥鎖,也是可重入鎖
- 總結
- 可重入鎖:即某個線程獲得了鎖之后,在鎖釋放前,它可以多次重新獲取該鎖
- 可重入鎖解決了重入鎖死的問題
- Java的內置鎖synchronized和ReentrantLock都是可重入鎖
ReentrantLock實現了Lock接口,并提供了與synchronized相同的互斥性和內存可見性。