詳解Java中的鎖Lock和synchronized
a)synchronized:Java提供的內置鎖機制,Java中的每個對象都可以用作一個實現同步的鎖(內置鎖或者監視器Monitor),線程在進入同步代碼塊之前需要或者這把鎖,在退出同步代碼塊會釋放鎖。而synchronized這種內置鎖實際上是互斥的,即沒把鎖最多只能由一個線程持有。
b)Lock接口:Lock接口提供了與synchronized相似的同步功能,和synchronized(隱式的獲取和釋放鎖,主要體現在線程進入同步代碼塊之前需要獲取鎖退出同步代碼塊需要釋放鎖)不同的是,Lock在使用的時候是顯示的獲取和釋放鎖。雖然Lock接口缺少了synchronized隱式獲取釋放鎖的便捷性,但是對于鎖的操作具有更強的可操作性、可控制性以及提供可中斷操作和超時獲取鎖等機制。
2、lock接口使用的一般形式Lock lock = new ReentrantLock(); //這里可以是自己實現Lock接口的實現類,也可以是jdk提供的同步組件lock.lock();//一般不將鎖的獲取放在try語句塊中,因為如果發生異常,在拋出異常的同時,也會導致鎖的無故釋放try {}finally { lock.unlock(); //放在finally代碼塊中,保證鎖一定會被釋放}3、Lock接口的方法
public interface Lock { /** * 獲取鎖,調用該方法的線程會獲取鎖,當獲取到鎖之后會從該方法但會 */ void lock(); /** * 可響應中斷。即在獲取鎖的過程中可以中斷當前線程 */ void lockInterruptibly() throws InterruptedException; /** * 嘗試非阻塞的獲取鎖,調用該方法之后會立即返回,如果獲取到鎖就返回true否則返回false */ boolean tryLock(); /** * 超時的獲取鎖,下面的三種情況會返回 * ①當前線程在超時時間內獲取到了鎖 * ②當前線程在超時時間內被中斷 * ③超時時間結束,返回false */ boolean tryLock(long time, TimeUnit unit) throws InterruptedException; /** * 釋放鎖 */ void unlock(); /** * 獲取等待通知組件,該組件和當前鎖綁定,當前線程只有獲取到了鎖才能調用組件的wait方法,調用該方法之后會釋放鎖 */ Condition newCondition();}4、相比于synchronized,Lock接口所具備的其他特性
①嘗試非阻塞的獲取鎖tryLock():當前線程嘗試獲取鎖,如果該時刻鎖沒有被其他線程獲取到,就能成功獲取并持有鎖
②能被中斷的獲取鎖lockInterruptibly():獲取到鎖的線程能夠響應中斷,當獲取到鎖的線程被中斷的時候,會拋出中斷異常同時釋放持有的鎖
③超時的獲取鎖tryLock(long time, TimeUnit unit):在指定的截止時間獲取鎖,如果沒有獲取到鎖返回false
二、重入鎖1、重入鎖的概念當某個線程請求一個被其他線程所持有的鎖的時候,該線程會被阻塞(后面的讀寫鎖先不考慮在內),但是像synchronized這樣的內置鎖是可重入的,即一個線程試圖獲取一個已經被該線程所持有的鎖,這個請求會成功。重入以為這鎖的操作粒度是線程級別而不是調用級別。我們下面說到的ReentrantLock也是可重入的,而除了支持鎖的重入之外,該同步組件也支持公平的和非公平的選擇。
2、ReentrantLocka)ReentrantLock實現的可重入性
對于鎖的可重入性,需要解決的兩個問題就是:
①線程再次獲取鎖的識別問題(鎖需要識別當前要獲取鎖的線程是否為當前占有鎖的線程);
②鎖的釋放(同一個線程多次獲取同一把鎖,那么鎖的記錄也會不同。一般來說,當同一個線程重復n次獲取鎖之后,只有在之后的釋放n次鎖之后,其他的線程才能去競爭這把鎖)
③ReentrantLock的可重入測試
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TestCR { Lock lock = new ReentrantLock(); void m1(){ try{ lock.lock(); // 加鎖 for(int i = 0; i < 4; i++){ TimeUnit.SECONDS.sleep(1); System.out.println('m1() method ' + i); } m2(); //在釋放鎖之前,調用m2方法 }catch(InterruptedException e){ e.printStackTrace(); }finally{ lock.unlock(); // 解鎖 } } void m2(){ lock.lock(); System.out.println('m2() method'); lock.unlock(); } public static void main(String[] args) { final TestCR t = new TestCR(); new Thread(new Runnable() { @Override public void run() { t.m1(); } }).start(); new Thread(new Runnable() { @Override public void run() { t.m2(); } }).start(); } }
b)下面分析ReentrantLock的部分源碼來學習這個同步組件(默認的非公平鎖實現)
①首先可以知道ReentrantLock實現Lock接口public class ReentrantLock implements Lock
abstract static class Sync extends AbstractQueuedSynchronizer { /** * 創建非公平鎖的方法 */ abstract void lock(); /** * 執行非公平的tryLock。 tryAcquire實現于 * 子類,但兩者都需要tryf方法的非公平嘗試。 */ final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();//獲取當前線程int c = getState(); //獲取當前同步狀態的值if (c == 0) { //如果當前的同步狀態還沒有被任何線程獲取 if (compareAndSetState(0, acquires)) { //就更新同步狀態的值,因為已經有線程獲取到同步裝填setExclusiveOwnerThread(current);//設置同步狀態的線程擁有者為當前獲取的線程return true; }}else if (current == getExclusiveOwnerThread()) {//增加再次獲取同步狀態的處理邏輯 int nextc = c + acquires; //如果再次嘗試獲取同步狀態的線程就是當前已經占有同步狀態的線程,那么就更新同步狀態的值(進行增加操作) if (nextc < 0) // 對同步狀態的值進行非法判斷throw new Error('Maximum lock count exceeded'); setState(nextc); //更新state的值 return true;}return false; } /** * 釋放同步狀態的處理邏輯 */ protected final boolean tryRelease(int releases) {int c = getState() - releases; //對同一線程而言,就是減去相應的獲取次數if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException();boolean free = false; //返回值if (c == 0) { //只有該線程將獲取的次數全部釋放之后,才會返回true,并且將當前同步狀態的持有者設置為null free = true; setExclusiveOwnerThread(null);}setState(c); //更新statereturn free; }/** * 判斷當前同步狀態的持有者線程 */ protected final boolean isHeldExclusively() {return getExclusiveOwnerThread() == Thread.currentThread(); } final ConditionObject newCondition() {return new ConditionObject(); }/** * 返回當前持有者線程 */ final Thread getOwner() {return getState() == 0 ? null : getExclusiveOwnerThread(); }/** * 返回持有同步狀態的線程獲取次數 */ final int getHoldCount() {return isHeldExclusively() ? getState() : 0; }/** * 判斷當前是否有線程獲取到同步狀態(根據state值進行判斷) */ final boolean isLocked() {return getState() != 0; } private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {s.defaultReadObject();setState(0); // reset to unlocked state }}
②通過上面的非公平鎖的實現源碼可以看到,ReentrantLock實現可重入的邏輯大概上是這樣的:
獲取邏輯:首先通過nonfairTryAcquire方法增加了對于同一線程再次獲取同步狀態的邏輯處理(通過判斷當前線程是否為已經同步狀態的持有者,來決定是否能夠再次獲取同步狀態,如果當前線程是已經獲取到同步狀態的那個線程,那么就能夠獲取成功,并且同時以CAS的方式修改state的值)
釋放邏輯:對于成功獲取到同步狀態的線程,在釋放鎖的時候,通過tryRelease方法的實現可以看出,如果該鎖被線程獲取到了n次,那么前(n-1)次釋放的操作都會返回false,只有將同步狀態完全釋放才會返回true。最終獲取到同步狀態的線程在完全釋放掉之后,state值為0并且持有鎖的線程為null。
c)關于ReentrantLock的公平和非公平實現
①非公平鎖
公平和非公平是針對于獲取鎖而言的,對于公平鎖而言獲取鎖應該遵循FIFO原則,上面我們通過源碼分析了非公平鎖的實現(對于非公平鎖而言,tryAcquire方法直接使用的是ReentrantLock靜態內部類Sync的nofairTryAcquire方法)
//非公平鎖實現static final class NonfairSync extends Sync { /** * 以CAS方式原子的更新state的值 */ final void lock() {if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread());else acquire(1); } /** * 非公平鎖的實現是直接調用Sync的nonfairTryAcquire方法 */ protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires); }}
②公平鎖實現
公平鎖的實現和非公平實現的主要區別就是tryAcquire方法的實現
static final class FairSync extends Sync { final void lock() {acquire(1); //調用AQS的模板方法實現鎖的獲取 } /** * 公平鎖的處理邏輯 */ protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread(); //獲取當前線程int c = getState(); //獲取當前同步狀態的值if (c == 0) { //當前同步狀態沒有被任何線程獲取的時候 if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) { //這個點的主要處理邏輯就是:hasQueuedPredecessors判斷當前線程所在的結點是否含有前驅結點, 如果返回值為true表示有前驅結點,那么當前線程需要等待前驅結點中的線程獲取并釋放鎖之后才能獲取鎖,保證了FIFOsetExclusiveOwnerThread(current);return true; }}else if (current == getExclusiveOwnerThread()) { //支持重入的邏輯,和非公平鎖的實現原理相同 int nextc = c + acquires; if (nextc < 0)throw new Error('Maximum lock count exceeded'); setState(nextc); return true;}return false; }}//hasQueuedPredecessors的處理邏輯public final boolean hasQueuedPredecessors() { // 簡單而言,就是判斷當前線程是否有前驅結點 // 當前結點含有前驅結點時候返回true;當前結點為頭結點揮著隊列為空的時候返回false Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());}
d)公平鎖和非公平鎖的測試
①測試目的
驗證上面通過源碼分析的,非公平鎖在獲取鎖的時候會首先進行搶鎖,在獲取鎖失敗后才會將當前線程加入同步隊列隊尾中,而公平鎖則是符合請求的絕對順序,也就是會按照先來后到FIFO。在下面的代碼中我們使用一個靜態內部類繼承了ReentrantLock并重寫等待隊列的方法,作為測試的ReentrantLock。然后創建5個線程,每個線程連續兩次去獲取鎖,分別測試公平鎖和非公平鎖的測試結果
import java.util.ArrayList;import java.util.Collection;import java.util.Collections;import java.util.List;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;import org.junit.Test;public class TestReentrantLock { /** * ReentrantLock的構造方法 * public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();} */ private Lock fairLock = new ReentrantLock2(true); private Lock unFairLock = new ReentrantLock2(false); @Test public void testFair() throws InterruptedException {testLock(fairLock); //測試公平鎖 } @Test public void testUnFair() throws InterruptedException {testLock(unFairLock); //測試非公平鎖 } private void testLock(Lock lock) throws InterruptedException {for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Job(lock)) {public String toString() {return getName();} }; thread.setName(i+''); thread.start();}Thread.sleep(12000); } private static class Job extends Thread {private Lock lock;public Job(Lock lock) { this.lock = lock;}@Overridepublic void run() { //兩次打印當前線程和等待隊列中的Threads for (int i = 0; i < 2; i++) {lock.lock(); //獲取鎖try { Thread.sleep(1000); System.out.println('當前線程=>' + Thread.currentThread().getName() + ' ' + '等待隊列中的線程=>' + ((ReentrantLock2)lock).getQueuedThreads());} catch (InterruptedException e) { e.printStackTrace();} finally { lock.unlock(); //釋放鎖} }} } private static class ReentrantLock2 extends ReentrantLock {public ReentrantLock2(boolean fair) { super(fair);}public Collection<Thread> getQueuedThreads() { //逆序打印等待隊列中的線程 List<Thread> list = new ArrayList<Thread>(super.getQueuedThreads()); Collections.reverse(list); return list;} }}
②測試非公平鎖
由上面的測試結果簡單的得到關于非公平鎖的一個結論:通過nofairTryAcquire方法可以得到這樣一個前提,當一個線程請求一個鎖時,判斷獲取成功的條件就是這個線程獲取到同步狀態就可以,那么某個剛剛釋放鎖的線程再次獲取到同步狀態的幾率就會更大一些(當然實驗中也出現并非連續兩次獲取這把鎖的情況,比如下面的測試結果)
③測試公平鎖
通過分析下面的測試結果,對于使用公平鎖而言,即便是同一個線程連續兩次獲取鎖釋放鎖,在第一次釋放鎖之后還是會被放在隊尾并從隊列頭部拿出線程進行執行。并沒有出現像非公平鎖那樣連續兩次獲取鎖的那種情況
④由上面的測試可以看出:非公平鎖可能導致在隊尾的線程饑餓,但是又因為同一個線程在釋放鎖的時候有更大的概率再次獲取到這把鎖,那么這樣的話線程的切換次數就會更少(這帶來的就是更大的吞吐量和開銷的減小)。而雖然公平鎖的獲取嚴格按照FIFO的規則,但是線程切換的次數就會更多。
三、Synchronized1、Synchronized作用對象①對于普通方法,鎖的是當前實例對象
②對于靜態同步方法,鎖的是類的Class對象
③對于同步代碼塊,鎖的是Synchronized括號中的對象
如下所示的三種情況
package cn.source.sync;public class TestSync01 { private static int count = 0; private Object object = new Object(); public void testSyn1() {//同步代碼塊(這里面是鎖臨界資源,即括號中的對象)synchronized (object) { System.out.println(Thread.currentThread().getName()+' count =' + count++);} } public void testSyn2() {//鎖當前對象(相當于普通同步方法)synchronized (this) { System.out.println(Thread.currentThread().getName() +' count =' + count++);} } //普通同步方法:鎖當前對象 public synchronized void testSyn3() {System.out.println(Thread.currentThread().getName()+' count =' + count++); } //靜態同步方法,鎖的是當前類型的類對象(即TestSync01.class) public static synchronized void testSyn4() {System.out.println(Thread.currentThread().getName()+' count =' + count++); } //下面的這種方式也是鎖當前類型的類對象 public static void testSyn5() {synchronized (TestSync01.class) { System.out.println(Thread.currentThread().getName() +' count =' + count ++);} }}2、synchronized的實現原理
①Java 虛擬機中的同步(Synchronization)基于進入和退出管程(Monitor)對象實現。同步代碼塊是使用monitorenter和monitorexit來實現的,同步方法 并不是由 monitor enter 和 monitor exit 指令來實現同步的,而是由方法調用指令讀取運行時常量池中方法的 ACC_SYNCHRONIZED 標志來隱式實現的。monitorenter指令是在編譯后插入同步代碼塊的起始位置,而monitorexit指令是在方法結束處和異常處,每個對象都有一個monitor與之關聯,當一個monitor被持有后它就會處于鎖定狀態。
②synchronized用的鎖是存在Java對象頭(非數組類型包括Mark Word、類型指針,數組類型多了數組長度)里面的,對象頭中的Mark Word存儲對象的hashCode,分代年齡和鎖標記位,類型指針指向對象的元數據信息,JVM通過這個指針確定該對象是那個類的實例等信息。
③當在對象上加鎖的時候,數據是記錄在對象頭中,對象頭中的Mark Word里存儲的數據會隨著鎖標志位的變化而變化(無鎖、輕量級鎖00、重量級鎖10、偏向鎖01)。當執行synchronized的同步方法或者同步代碼塊時候會在對象頭中記錄鎖標記,鎖標記指向的是monitor對象(也稱為管程或者監視器鎖)的起始地址。由于每個對象都有一個monitor與之關聯,monitor和與關聯的對象一起創建(當線程試圖獲取鎖的時候)或銷毀,當monitor被某個線程持有之后,就處于鎖定狀態。
④Hotspot虛擬機中的實現,通過ObjectMonitor來實現的
如圖所示,ObjectMonitor中有兩個隊列(EntryList、WaitSet)以及鎖持有者Owner標記,其中WaitSet是哪些調用wait方法之后被阻塞等待的線程隊列,EntryList是ContentionList中能有資格獲取鎖的線程隊列。當多個線程并發訪問同一個同步代碼時候,首先會進入EntryList,當線程獲得鎖之后monitor中的Owner標記會記錄此線程,并在該monitor中的計數器執行遞增計算代表當前鎖被持有鎖定,而沒有獲取到的線程繼續在EntryList中阻塞等待。如果線程調用了wait方法,則monitor中的計數器執行賦0運算,并且將Owner標記賦值為null,代表當前沒有線程持有鎖,同時調用wait方法的線程進入WaitSet隊列中阻塞等待,直到持有鎖的執行線程調用notify/notifyAll方法喚醒WaitSet中的線程,喚醒的線程進入EntryList中等待鎖的獲取。除了使用wait方法可以將修改monitor的狀態之外,顯然持有鎖的線程的同步代碼塊執行結束也會釋放鎖標記,monitor中的Owner會被賦值為null,計數器賦值為0。如下圖所示
a)鎖的種類
Java 中鎖的種類大致分為偏向鎖,自旋鎖,輕量級鎖,重量級鎖。鎖的使用方式為:先提供偏向鎖,如果不滿足的時候,升級為輕量級鎖,再不滿足,升級為重量級鎖。自旋鎖是一個過渡的鎖狀態,不是一種實際的鎖類型。鎖只能升級,不能降級。
b)鎖的升級
①偏向鎖
如果代碼中基本不可能出現多線程并發爭搶同一個鎖的時候,JVM 編譯代碼,解釋執行的時候,會自動的放棄同步信息,消除 synchronized 的同步代碼結果,使用鎖標記的形式記錄鎖狀態。具體的實現方式大概就是:當一個線程訪問同步塊并獲取鎖的時候,會在對象頭和棧幀的鎖記錄中存儲偏向的線程ID,之后線程在進入和退出同步塊的時候不需要使用CAS進行加鎖和解鎖,只需要測試對象頭中的MarkWord中是否存儲著當前線程的偏向鎖;如果測試成功,就表示線程獲取鎖成功,如果測試失敗需要檢查對象頭中的MarkWord的偏向鎖表示是否設置為1,如果沒有設置就使用CAS競爭鎖,設置了就以CAS方式將偏向鎖設置為當前線程。在 Monitor 中有變量 ACC_SYNCHRONIZED。當變量值使用的時候,代表偏向鎖鎖定。使用偏向鎖可以避免鎖的爭搶和鎖池狀態的維護。提高效率。
②輕量級鎖
當偏向鎖不滿足,也就是有多線程并發訪問,鎖定同一個對象的時候,先提升為輕量級鎖。也是使用標記 ACC_SYNCHRONIZED 標記記錄的。ACC_UNSYNCHRONIZED 標記記錄未獲取到鎖信息的線程。就是只有兩個線程爭搶鎖標記的時候,優先使用輕量級鎖。(自旋鎖)當獲取鎖的過程中,未獲取到。為了提高效率,JVM 自動執行若干次空循環,再次申請鎖,而不是進入阻塞狀態的情況。稱為自旋鎖。自旋鎖提高效率就是避免線程狀態的變更
③重量級鎖
在自旋過程中,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),鎖就會被升級為重量級鎖。在重量級鎖的狀態下,其他線程視圖獲取鎖的時候都會被阻塞住,只有持有鎖的線程釋放鎖之后才會喚醒那些阻塞的線程,這些線程就開始競爭鎖。
4、關于synchronized的其他說明a)關于同步方法和非同步方法
同步方法只影響 鎖定同一個鎖對象的同步方法,不影響非同步方法被其他線程調用,也不影響其他所資源的同步方法(簡單理解就是鎖的不是同一個資源,就不會影響);
b)synchronized是可重入的
同一個線程,多次調用同步代碼,鎖定同一個對象,是可重入的;
c)關于同步的繼承問題
同一個線程中,子類同步方法覆蓋父類的同步方法,可以指定調用父類的同步方法(相當于鎖的重入)
d)鎖與異常
當同步方法出現異常的時候會自動釋放鎖,不會影響其他線程的執行
e)synchronized鎖的是對象,而不是引用
同步代碼一旦加鎖之后會有一個臨時鎖引用執行鎖對象,和真實的引用無直接關聯,在鎖釋放之前,修改鎖對象引用不會影響同步代碼塊的執行
f)synchronized中的常量問題
在定義同步代碼塊的時候,不要使用常量對象作為鎖對象
以上就是詳解Java中的鎖Lock和synchronized的詳細內容,更多關于Java Lock synchronized的資料請關注好吧啦網其它相關文章!
相關文章: