synchronized,ReentrantLock解决锁冲突,脏读的问题
最常見的秒殺系統,解決思路就是從前端、后臺服務、數據庫層層去掉負載,以達到平衡
鎖作為并發共享數據,保證一致性的工具,在JAVA平臺有多種實現(如 synchronized 和 ReentrantLock等等 ) 。這些已經寫好提供的鎖為我們開發提供了便利,但是鎖的具體性質以及類型卻很少被提及。本系列文章將分析JAVA下常見的鎖名稱以及特性,為大家答疑解惑。
public class Thread1 implements Runnable {private String flag = "start";private String control = "";public void run() {// TODO Auto-generated method stubint i = 0;while (true) {if (flag.equals("start")) {i++;System.out.println("The thread1 is running" + i);} else if (flag.equals("wait")) {try {System.out.println("===wait===");synchronized (control) {control.wait();}} catch (InterruptedException e) {// TODO Auto-generated catch block e.printStackTrace();}}}}public void wait1() {this.flag = "wait";}public void start1() {this.flag = "start";if (flag.equals("start")) {synchronized (control) {control.notifyAll();}}}}看調用
public static void main(String[] args) {// TODO Auto-generated method stubThread1 th1 = new Thread1();Thread t1 = new Thread(th1);t1.start();try {Thread.sleep(20);} catch (InterruptedException e) {// TODO Auto-generated catch block e.printStackTrace();}//thread線程暫停 th1.wait1();try {Thread.sleep(2000);} catch (InterruptedException e) {// TODO Auto-generated catch block e.printStackTrace();}//thread線程繼續運行 th1.start1();//th1.wait1();//th1.start1();}ReentrantLock和synchronized都是可重入鎖
可重入鎖,也叫做遞歸鎖,指的是同一線程 外層函數獲得鎖之后 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。
廣義上的可重入鎖指的是可重復可遞歸調用的鎖,在外層使用鎖之后,在內層仍然可以使用,并且不發生死鎖(前提得是同一個對象或者class),這樣的鎖就叫做可重入鎖。
public class ReentrantTest implements Runnable {
? ? public synchronized void get() {
? ? ? ? System.out.println(Thread.currentThread().getName());
? ? ? ? set();
? ? }
? ? public synchronized void set() {
? ? ? ? System.out.println(Thread.currentThread().getName());
? ? }
? ? public void run() {
? ? ? ? get();
? ? }
? ? public static void main(String[] args) {
? ? ? ? ReentrantTest rt = new ReentrantTest();
? ? ? ? for(;;){
? ? ? ? ? ? new Thread(rt).start();
? ? ? ? }
輸出
Thread-8492
Thread-8492
Thread-8494
Thread-8494
Thread-8495
Thread-8495
Thread-8493
Thread-8493
?set()和get()同時輸出了線程名稱,表明即使遞歸使用synchronized也沒有發生死鎖,證明其是可重入的。
?不可重入鎖,與可重入鎖相反,不可遞歸調用,遞歸調用就發生死鎖。
package com.thread;import java.util.concurrent.atomic.AtomicReference;public class UnreentrantLock {private AtomicReference<Thread> owner = new AtomicReference<Thread>();//記錄當前鎖的持有線程對象public void lock() {//加鎖Thread current = Thread.currentThread();//獲取當前線程對象for (; ; ) {//自旋(被當前線程或其他線程持有鎖,就會循環) for 的三種用法,class、增強型循環、無線循環if (owner.compareAndSet(null, current)) {//只有鎖可用即為null,才能設置當前線程為鎖持有對象,并返回truereturn;}}}public void unlock() {//解鎖Thread current = Thread.currentThread();//獲取當前線程對象owner.compareAndSet(current, null);//設置鎖的持有對象為null } }使用原子引用來存放線程,同一線程兩次調用lock()方法,如果不執行unlock()釋放鎖的話,第二次調用自旋的時候就會產生死鎖,這個鎖就不是可重入的。
? ? 實際上同一個線程不必每次都去釋放鎖再來獲取鎖,這樣的調度切換是很耗資源的。稍微改一下,把它變成一個可重入鎖:
package com.thread;import java.util.concurrent.atomic.AtomicReference;public class UnreentrantLock {private AtomicReference<Thread> owner = new AtomicReference<Thread>();//記錄當前鎖的持有線程對象private int state = 0;//記錄重入次數public void lock() {//加鎖Thread current = Thread.currentThread();//獲取當前線程對象if (owner.compareAndSet(null, current)) {//當前鎖可用state = 1;//狀態置為1return;} else {if (current == owner.get()) {//如果當前線程持有鎖state++;//重入次數加1return;}for (; ; ) {//被其他線程持有就會繼續循環if (owner.compareAndSet(null, current)) {//只有鎖可用即為null,才能設置當前線程為鎖持有對象,并返回truereturn;}}}}public void unlock() {//解鎖Thread current = Thread.currentThread();//獲取當前線程對象if (current == owner.get()) {//如果當前線程持有鎖if (state > 0) {//重入次數大于0state--;//重入次數減1} else {owner.compareAndSet(current, null);//設置鎖的持有對象為null }}} }? 在執行每次操作之前,判斷當前鎖持有者是否是當前對象,采用state計數,不用每次去釋放鎖。
?
ReentrantLock原理
- 原子狀態:原子狀態有 CAS(compareAndSetState) 操作來存儲當前鎖的狀態,判斷鎖是否有其他線程持有。
- 等待隊列:所有沒有請求到鎖的線程,會進入等待隊列進行等待。待有線程釋放鎖后,系統才能夠從等待隊列中喚醒一個線程,繼續工作。詳見:隊列同步器——AQS
- 阻塞原語 park() 和 unpark(),用來掛起和恢復線程。沒有得到鎖的線程將會被掛起。關于阻塞原語,詳見:線程阻塞工具類——LockSupport
ReentrantLock的幾個重要方法整理如下:
- lock():獲得鎖,如果鎖被占用,進入等待。
- lockInterruptibly():獲得鎖,但優先響應中斷。
- tryLock():嘗試獲得鎖,如果成功,立即放回 true,反之失敗返回 false。該方法不會進行等待,立即返回。
- tryLock(long time, TimeUnit unit):在給定的時間內嘗試獲得鎖。
unLock():釋放鎖。
一、何為重進入(重入)?
重進入是指任意線程在獲取到鎖之后能夠再次獲取該鎖而不會被鎖阻塞,該特性的實現需要解決以下兩個問題:
-
- 線程再次獲取鎖:鎖需要去識別獲取鎖的線程是否為當前占據鎖的線程,如果是,則再次成功獲取。
- 鎖的最終釋放。線程重復 n 次獲取了鎖,隨后在第 n 次釋放該鎖后,其它線程能夠獲取到該鎖。鎖的最終釋放要求鎖對于獲取進行計數自增,計數表示當前鎖被重復獲取的次數,而鎖被釋放時,計數自減,當計數等于 0 時表示鎖已經成功釋放。
acquireQueued 方法增加了再次獲取同步狀態的處理邏輯:通過判斷當前線程是否為獲取鎖的線程,來決定獲取操作是否成功,如果獲取鎖的線程再次請求,則將同步狀態值進行增加并返回 true,表示獲取同步狀態成功。
成功獲取鎖的線程再次獲取鎖,只是增加了同步狀態值,也就是要求 ReentrantLock 在釋放同步狀態時減少同步狀態值,釋放鎖源碼如下:
如果鎖被獲取 n 次,那么前 (n-1) 次 tryRelease(int releases) 方法必須返回 false,只有同步狀態完全釋放了,才能返回 true。該方法將同步狀態是否為 0 作為最終釋放的條件,當同步狀態為 0 時,將占有線程設置為 null,并返回 true,表示釋放成功。
通過對獲取與釋放的分析,就可以解釋,以上兩個例子中出現的兩個問題:為什么 ReentrantLock 鎖能夠支持一個線程對資源的重復加鎖?為什么公平鎖例子中出現,公平鎖線程是不斷切換的,而非公平鎖出現同一線程連續獲取鎖的情況?
- 為什么支持重復加鎖?因為源碼中用變量 c 來保存當前鎖被獲取了多少次,故在釋放時,對 c 變量進行減操作,只有 c 變量為 0 時,才算鎖的最終釋放。所以可以 lock 多次,同時 unlock 也必須與 lock 同樣的次數。
- 為什么非公平鎖出現同一線程連續獲取鎖的情況?tryAcquire 方法中增加了再次獲取同步狀態的處理邏輯;
二、為什么使用可重入鎖?
ReentrantLock 是一個可重入的互斥(/獨占)鎖,又稱為“獨占鎖”。
ReentrantLock通過自定義隊列同步器(AQS-AbstractQueuedSychronized,是實現鎖的關鍵)來實現鎖的獲取與釋放。
其可以完全替代 synchronized 關鍵字。JDK 5.0 早期版本,其性能遠好于 synchronized,但 JDK 6.0 開始,JDK 對 synchronized 做了大量的優化,使得兩者差距并不大。
“獨占”,就是在同一時刻只能有一個線程獲取到鎖,而其它獲取鎖的線程只能處于同步隊列中等待,只有獲取鎖的線程釋放了鎖,后繼的線程才能夠獲取鎖。
“可重入”,就是支持重進入的鎖,它表示該鎖能夠支持一個線程對資源的重復加鎖。
該鎖還支持獲取鎖時的公平和非公平性選擇。“公平”是指“不同的線程獲取鎖的機制是公平的”,而“不公平”是指“不同的線程獲取鎖的機制是非公平的”。
1?中斷響應(lockInterruptibly)
對于 synchronized 來說,如果一個線程在等待鎖,那么結果只有兩種情況,獲得這把鎖繼續執行,或者線程就保持等待。
而使用重入鎖,提供了另一種可能,這就是線程可以被中斷。也就是在等待鎖的過程中,程序可以根據需要取消對鎖的需求。
下面的例子中,產生了死鎖,但得益于鎖中斷,最終解決了這個死鎖:
public class IntLock implements Runnable{public static ReentrantLock lock1 = new ReentrantLock();public static ReentrantLock lock2 = new ReentrantLock();int lock;/*** 控制加鎖順序,產生死鎖*/public IntLock(int lock) {this.lock = lock;}public void run() {try {if (lock == 1) {lock1.lockInterruptibly(); // 如果當前線程未被 中斷,則獲取鎖。try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}lock2.lockInterruptibly();System.out.println(Thread.currentThread().getName()+",執行完畢!");} else {lock2.lockInterruptibly();try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}lock1.lockInterruptibly();System.out.println(Thread.currentThread().getName()+",執行完畢!");}} catch (InterruptedException e) {e.printStackTrace();} finally {// 查詢當前線程是否保持此鎖。if (lock1.isHeldByCurrentThread()) {lock1.unlock();}if (lock2.isHeldByCurrentThread()) {lock2.unlock();}System.out.println(Thread.currentThread().getName() + ",退出。");}}public static void main(String[] args) throws InterruptedException {IntLock intLock1 = new IntLock(1);IntLock intLock2 = new IntLock(2);Thread thread1 = new Thread(intLock1, "線程1");Thread thread2 = new Thread(intLock2, "線程2");thread1.start();thread2.start();Thread.sleep(1000);thread2.interrupt(); // 中斷線程2 } }上述例子中,線程 thread1 和 thread2 啟動后,thread1 先占用 lock1,再占用 lock2;thread2 反之,先占 lock2,后占 lock1。這便形成 thread1 和 thread2 之間的相互等待。
代碼 56 行,main 線程處于休眠(sleep)狀態,兩線程此時處于死鎖的狀態,代碼 57 行 thread2 被中斷(interrupt),故 thread2 會放棄對 lock1 的申請,同時釋放已獲得的 lock2。這個操作導致 thread1 順利獲得 lock2,從而繼續執行下去。
執行代碼,輸出如下:
2鎖申請等待限時(tryLock)
除了等待外部通知(中斷操作 interrupt )之外,限時等待也可以做到避免死鎖。
通常,無法判斷為什么一個線程遲遲拿不到鎖。也許是因為產生了死鎖,也許是產生了饑餓。但如果給定一個等待時間,讓線程自動放棄,那么對系統來說是有意義的??梢允褂?tryLock() 方法進行一次限時的等待。
?
public class TimeLock implements Runnable{public static ReentrantLock lock = new ReentrantLock();public void run() {try {if (lock.tryLock(5, TimeUnit.SECONDS)) {Thread.sleep(6 * 1000);}else {System.out.println(Thread.currentThread().getName()+" get Lock Failed");}} catch (InterruptedException e) {e.printStackTrace();}finally {// 查詢當前線程是否保持此鎖。if (lock.isHeldByCurrentThread()) {System.out.println(Thread.currentThread().getName()+" release lock");lock.unlock();}}}/*** 在本例中,由于占用鎖的線程會持有鎖長達6秒,故另一個線程無法再5秒的等待時間內獲得鎖,因此請求鎖會失敗。*/public static void main(String[] args) {TimeLock timeLock = new TimeLock();Thread t1 = new Thread(timeLock, "線程1");Thread t2 = new Thread(timeLock, "線程2");t1.start();t2.start();} }上述例子中,由于占用鎖的線程會持有鎖長達 6 秒,故另一個線程無法在 5 秒的等待時間內獲得鎖,因此,請求鎖失敗。
ReentrantLock.tryLock()方法也可以不帶參數直接運行。這種情況下,當前線程會嘗試獲得鎖,如果鎖并未被其他線程占用,則申請鎖成功,立即返回 true。否則,申請失敗,立即返回 false,當前線程不會進行等待。這種模式不會引起線程等待,因此也不會產生死鎖。
3?公平鎖
·默認情況下,鎖的申請都是非公平的。也就是說,如果線程 1 與線程 2,都申請獲得鎖 A,那么誰獲得鎖不是一定的,是由系統在等待隊列中隨機挑選的。這就好比,買票的人不排隊,售票姐姐只能隨機挑一個人賣給他,這顯然是不公平的。而公平鎖,它會按照時間的先后順序,保證先到先得。公平鎖的特點是:不會產生饑餓現象。
重入鎖允許對其公平性進行設置。構造函數如下:
public ReentrantLock(boolean fair) public class FairLock implements Runnable{public static ReentrantLock fairLock = new ReentrantLock(true);public void run() {while (true) {try {fairLock.lock();System.out.println(Thread.currentThread().getName()+",獲得鎖!");}finally {fairLock.unlock();}}}public static void main(String[] args) {FairLock fairLock = new FairLock();Thread t1 = new Thread(fairLock, "線程1");Thread t2 = new Thread(fairLock, "線程2");t1.start();t2.start();} }測試結果:
1.當參數設置為 true 時:線程1 和 線程2 交替進行 公平競爭 交替打印
線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖!2.當參數設置為 false 時: 此時可以看到線程1 可以持續拿到鎖 等線程1 執行完后 線程2 才可以拿到線程 然后多次執行 ; 這就是使用 可重入鎖后 是非公平機制 線程可以優先多次拿到執行權 線程1,獲得鎖! 線程1,獲得鎖! 線程1,獲得鎖! 線程1,獲得鎖! 線程1,獲得鎖! 線程1,獲得鎖! 線程1,獲得鎖! 線程1,獲得鎖! 線程1,獲得鎖! 線程1,獲得鎖! 線程1,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程2,獲得鎖! 線程2,獲得鎖! 線程2,獲得鎖!
修改重入鎖是否公平,觀察輸出結果,如果公平,輸出結果始終為兩個線程交替的獲得鎖,如果是非公平,輸出結果為一個線程占用鎖很長時間,然后才會釋放鎖,另個線程才能執行。
ReenTrantLock可重入鎖(和synchronized的區別)總結
可重入性:
從名字上理解,ReenTrantLock的字面意思就是再進入的鎖,其實synchronized關鍵字所使用的鎖也是可重入的,兩者關于這個的區別不大。兩者都是同一個線程沒進入一次,鎖的計數器都自增1,所以要等到鎖的計數器下降為0時才能釋放鎖。
鎖的實現:
Synchronized是依賴于JVM實現的,而ReenTrantLock是JDK實現的,有什么區別,說白了就類似于操作系統來控制實現和用戶自己敲代碼實現的區別。前者的實現是比較難見到的,后者有直接的源碼可供閱讀。
性能的區別:
在Synchronized優化以前,synchronized的性能是比ReenTrantLock差很多的,但是自從Synchronized引入了偏向鎖,輕量級鎖(自旋鎖)后,兩者的性能就差不多了,在兩種方法都可用的情況下,官方甚至建議使用synchronized,其實synchronized的優化我感覺就借鑒了ReenTrantLock中的CAS技術。都是試圖在用戶態就把加鎖問題解決,避免進入內核態的線程阻塞。
功能區別:
便利性:很明顯Synchronized的使用比較方便簡潔,并且由編譯器去保證鎖的加鎖和釋放,而ReenTrantLock需要手工聲明來加鎖和釋放鎖,為了避免忘記手工釋放鎖造成死鎖,所以最好在finally中聲明釋放鎖。
鎖的細粒度和靈活度:很明顯ReenTrantLock優于Synchronized
ReenTrantLock獨有的能力:
1.??????ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。
2.??????ReenTrantLock提供了一個Condition(條件)類,用來實現分組喚醒需要喚醒的線程們,而不是像synchronized要么隨機喚醒一個線程要么喚醒全部線程。
3.??????ReenTrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實現這個機制。
ReenTrantLock實現的原理:
在網上看到相關的源碼分析,本來這塊應該是本文的核心,但是感覺比較復雜就不一一詳解了,簡單來說,ReenTrantLock的實現是一種自旋鎖,通過循環調用CAS操作來實現加鎖。它的性能比較好也是因為避免了使線程進入內核態的阻塞狀態。想盡辦法避免線程進入內核的阻塞狀態是我們去分析和理解鎖設計的關鍵鑰匙。
什么情況下使用ReenTrantLock:
答案是,如果你需要實現ReenTrantLock的三個獨有功能時。
?
公平與非公平唯一的區別是判斷條件中多了hasQueuedPredecessors()方法,即加入了同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回了true,則表示有線程比當前線程更早地請求獲取鎖,所以需要等待前驅線程獲取并釋放鎖后才能繼續獲取該鎖。
但是非公平鎖是默認實現:非公平性鎖可能使線程“饑餓”,但是極少的線程切換,可以保證其更大的吞吐量。而公平性鎖,保證了鎖的獲取按照FIFO原則,代價是進行大量的線程切換。
synchronized可重入性
同一線程在調用自己類中其他synchronized方法/塊或調用父類的synchronized方法/塊都不會阻礙該線程的執行,就是說同一線程對同一個對象鎖是可重入的,而且同一個線程可以獲取同一把鎖多次,也就是可以多次重入。
轉載于:https://www.cnblogs.com/ation/p/10905723.html
總結
以上是生活随笔為你收集整理的synchronized,ReentrantLock解决锁冲突,脏读的问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Set的常用实现类HashSet和Tre
- 下一篇: 用shell脚本实现MongoDB数据库