Java中的鎖——Lock和synchronized

上一篇Java中的隊列同步器AQShtml

1、Lock接口

一、Lock接口和synchronized內置鎖

a)synchronized:Java提供的內置鎖機制,Java中的每一個對象均可以用做一個實現同步的鎖(內置鎖或者監視器Monitor),線程在進入同步代碼塊以前須要或者這把鎖,在退出同步代碼塊會釋放鎖。而synchronized這種內置鎖其實是互斥的,即沒把鎖最多隻能由一個線程持有。java

b)Lock接口:Lock接口提供了與synchronized類似的同步功能,和synchronized(隱式的獲取和釋放鎖,主要體如今線程進入同步代碼塊以前須要獲取鎖退出同步代碼塊須要釋放鎖)不一樣的是,Lock在使用的時候是顯示的獲取和釋放鎖。雖然Lock接口缺乏了synchronized隱式獲取釋放鎖的便捷性,可是對於鎖的操做具備更強的可操做性、可控制性以及提供可中斷操做和超時獲取鎖等機制。數組

二、lock接口使用的通常形式

1 Lock lock = new ReentrantLock(); //這裏能夠是本身實現Lock接口的實現類,也能夠是jdk提供的同步組件
2 lock.lock();//通常不將鎖的獲取放在try語句塊中,由於若是發生異常,在拋出異常的同時,也會致使鎖的無端釋放
3 try {
4 }finally {
5     lock.unlock(); //放在finally代碼塊中,保證鎖必定會被釋放
6 }

三、Lock接口的方法

 1 public interface Lock {
 2 
 3     /**
 4      * 獲取鎖,調用該方法的線程會獲取鎖,當獲取到鎖以後會從該方法但會
 5      */
 6     void lock();
 7 
 8     /**
 9      * 可響應中斷。即在獲取鎖的過程當中能夠中斷當前線程
10      */
11     void lockInterruptibly() throws InterruptedException;
12 
13     /**
14      * 嘗試非阻塞的獲取鎖,調用該方法以後會當即返回,若是獲取到鎖就返回true不然返回false
15      */
16     boolean tryLock();
17 
18     /**
19      * 超時的獲取鎖,下面的三種狀況會返回
20      * ①當前線程在超時時間內獲取到了鎖
21      * ②當前線程在超時時間內被中斷
22      * ③超時時間結束,返回false
23      */
24     boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
25 
26     /**
27      * 釋放鎖
28      */
29     void unlock();
30 
31     /**
32      * 獲取等待通知組件,該組件和當前鎖綁定,當前線程只有獲取到了鎖才能調用組件的wait方法,調用該方法以後會釋放鎖
33      */
34     Condition newCondition();
35 }

四、相比於synchronized,Lock接口所具有的其餘特性

①嘗試非阻塞的獲取鎖tryLock():當前線程嘗試獲取鎖,若是該時刻鎖沒有被其餘線程獲取到,就能成功獲取並持有鎖多線程

②能被中斷的獲取鎖lockInterruptibly():獲取到鎖的線程可以響應中斷,當獲取到鎖的線程被中斷的時候,會拋出中斷異常同時釋放持有的鎖併發

③超時的獲取鎖tryLock(long time, TimeUnit unit):在指定的截止時間獲取鎖,若是沒有獲取到鎖返回falseide

2、重入鎖

一、重入鎖的概念

  當某個線程請求一個被其餘線程所持有的鎖的時候,該線程會被阻塞(後面的讀寫鎖先不考慮在內),可是像synchronized這樣的內置鎖是可重入的,即一個線程試圖獲取一個已經被該線程所持有的鎖,這個請求會成功。重入覺得這鎖的操做粒度是線程級別而不是調用級別。咱們下面說到的ReentrantLock也是可重入的,而除了支持鎖的重入以外,該同步組件也支持公平的和非公平的選擇。源碼分析

二、ReentrantLock

a)ReentrantLock實現的可重入性

對於鎖的可重入性,須要解決的兩個問題就是:post

①線程再次獲取鎖的識別問題(鎖須要識別當前要獲取鎖的線程是否爲當前佔有鎖的線程);學習

②鎖的釋放(同一個線程屢次獲取同一把鎖,那麼鎖的記錄也會不一樣。通常來講,當同一個線程重複n次獲取鎖以後,只有在以後的釋放n次鎖以後,其餘的線程才能去競爭這把鎖)測試

③ReentrantLock的可重入測試

 1 import java.util.concurrent.locks.Lock;
 2 import java.util.concurrent.locks.ReentrantLock;
 3 
 4 public class TestCR {
 5     Lock lock = new ReentrantLock();
 6     
 7     void m1(){
 8         try{
 9             lock.lock(); // 加鎖
10             for(int i = 0; i < 4; i++){
11                 TimeUnit.SECONDS.sleep(1);
12                 System.out.println("m1() method " + i);
13             }
14             m2(); //在釋放鎖以前,調用m2方法
15         }catch(InterruptedException e){
16             e.printStackTrace();
17         }finally{
18             lock.unlock(); // 解鎖
19         }
20     }
21     
22     void m2(){
23         lock.lock();
24         System.out.println("m2() method");
25         lock.unlock();
26     }
27     
28     public static void main(String[] args) {
29         final TestCR t = new TestCR();
30         new Thread(new Runnable() {
31             @Override
32             public void run() {
33                 t.m1();
34             }
35         }).start();
36 
37         new Thread(new Runnable() {
38             @Override
39             public void run() {
40                 t.m2();
41             }
42         }).start();
43     }
44 }
ReentrantLock的可重入測試

b)下面分析ReentrantLock的部分源碼來學習這個同步組件(默認的非公平鎖實現)

①首先能夠知道ReentrantLock實現Lock接口public class ReentrantLock implements Lock 

 1 abstract static class Sync extends AbstractQueuedSynchronizer {
 2     /**
 3      * 建立非公平鎖的方法
 4      */
 5     abstract void lock();
 6 
 7     /**
 8      * 執行非公平的tryLock。 tryAcquire實現於
 9      * 子類,但二者都須要tryf方法的非公平嘗試。
10      */
11     final boolean nonfairTryAcquire(int acquires) {
12         final Thread current = Thread.currentThread();//獲取當前線程
13         int c = getState(); //獲取當前同步狀態的值
14         if (c == 0) { //若是當前的同步狀態尚未被任何線程獲取
15             if (compareAndSetState(0, acquires)) { //就更新同步狀態的值,由於已經有線程獲取到同步裝填
16                 setExclusiveOwnerThread(current);//設置同步狀態的線程擁有者爲當前獲取的線程
17                 return true;
18             }
19         }
20         else if (current == getExclusiveOwnerThread()) {//增長再次獲取同步狀態的處理邏輯
21             int nextc = c + acquires; //若是再次嘗試獲取同步狀態的線程就是當前已經佔有同步狀態的線程,那麼就更新同步狀態的值(進行增長操做)
22             if (nextc < 0) // 對同步狀態的值進行非法判斷
23                 throw new Error("Maximum lock count exceeded");
24             setState(nextc); //更新state的值
25             return true;
26         }
27         return false;
28     }
29 
30     /**
31      * 釋放同步狀態的處理邏輯
32      */
33     protected final boolean tryRelease(int releases) {
34         int c = getState() - releases; //對同一線程而言,就是減去相應的獲取次數
35         if (Thread.currentThread() != getExclusiveOwnerThread())
36             throw new IllegalMonitorStateException();
37         boolean free = false; //返回值
38         if (c == 0) { //只有該線程將獲取的次數所有釋放以後,纔會返回true,而且將當前同步狀態的持有者設置爲null
39             free = true;
40             setExclusiveOwnerThread(null);
41         }
42         setState(c); //更新state
43         return free;
44     }
45 
46         /**
47          * 判斷當前同步狀態的持有者線程
48          */
49     protected final boolean isHeldExclusively() {
50         return getExclusiveOwnerThread() == Thread.currentThread();
51     }
52 
53     final ConditionObject newCondition() {
54         return new ConditionObject();
55     }
56 
57         /**
58          * 返回當前持有者線程
59          */      
60     final Thread getOwner() {
61         return getState() == 0 ? null : getExclusiveOwnerThread();
62     }
63 
64         /**
65          * 返回持有同步狀態的線程獲取次數
66          */
67     final int getHoldCount() {
68         return isHeldExclusively() ? getState() : 0;
69     }
70 
71         /**
72          * 判斷當前是否有線程獲取到同步狀態(根據state值進行判斷)
73          */        
74     final boolean isLocked() {
75         return getState() != 0;
76     }
77 
78     private void readObject(java.io.ObjectInputStream s)
79         throws java.io.IOException, ClassNotFoundException {
80         s.defaultReadObject();
81         setState(0); // reset to unlocked state
82     }
83 } 

②經過上面的非公平鎖的實現源碼能夠看到,ReentrantLock實現可重入的邏輯大概上是這樣的:

  獲取邏輯:首先經過nonfairTryAcquire方法增長了對於同一線程再次獲取同步狀態的邏輯處理(經過判斷當前線程是否爲已經同步狀態的持有者,來決定是否可以再次獲取同步狀態,若是當前線程是已經獲取到同步狀態的那個線程,那麼就可以獲取成功,而且同時以CAS的方式修改state的值)

  釋放邏輯:對於成功獲取到同步狀態的線程,在釋放鎖的時候,經過tryRelease方法的實現能夠看出,若是該鎖被線程獲取到了n次,那麼前(n-1)次釋放的操做都會返回false,只有將同步狀態徹底釋放纔會返回true。最終獲取到同步狀態的線程在徹底釋放掉以後,state值爲0而且持有鎖的線程爲null。

c)關於ReentrantLock的公平和非公平實現

①非公平鎖

  公平和非公平是針對於獲取鎖而言的,對於公平鎖而言獲取鎖應該遵循FIFO原則,上面咱們經過源碼分析了非公平鎖的實現(對於非公平鎖而言,tryAcquire方法直接使用的是ReentrantLock靜態內部類Sync的nofairTryAcquire方法)

 1 //非公平鎖實現
 2 static final class NonfairSync extends Sync {
 3 
 4     /**
 5      * 以CAS方式原子的更新state的值
 6      */
 7     final void lock() {
 8         if (compareAndSetState(0, 1))
 9             setExclusiveOwnerThread(Thread.currentThread());
10         else
11             acquire(1);
12     }
13 
14     /**
15      * 非公平鎖的實現是直接調用Sync的nonfairTryAcquire方法
16      */
17     protected final boolean tryAcquire(int acquires) {
18         return nonfairTryAcquire(acquires);
19     }
20 }

②公平鎖實現

  公平鎖的實現和非公平實現的主要區別就是tryAcquire方法的實現

 1 static final class FairSync extends Sync {
 2 
 3     final void lock() {
 4         acquire(1); //調用AQS的模板方法實現鎖的獲取
 5     }
 6 
 7     /**
 8      * 公平鎖的處理邏輯
 9      */
10     protected final boolean tryAcquire(int acquires) {
11         final Thread current = Thread.currentThread(); //獲取當前線程
12         int c = getState(); //獲取當前同步狀態的值
13         if (c == 0) { //當前同步狀態沒有被任何線程獲取的時候
14             if (!hasQueuedPredecessors() &&
15                 compareAndSetState(0, acquires)) { //這個點的主要處理邏輯就是:hasQueuedPredecessors判斷當前線程所在的結點是否含有前驅結點,
                                  若是返回值爲true表示有前驅結點,那麼當前線程須要等待前驅結點中的線程獲取並釋放鎖以後才能獲取鎖,保證了FIFO
16 setExclusiveOwnerThread(current); 17 return true; 18 } 19 } 20 else if (current == getExclusiveOwnerThread()) { //支持重入的邏輯,和非公平鎖的實現原理相同 21 int nextc = c + acquires; 22 if (nextc < 0) 23 throw new Error("Maximum lock count exceeded"); 24 setState(nextc); 25 return true; 26 } 27 return false; 28 } 29 } 30 //hasQueuedPredecessors的處理邏輯 31 public final boolean hasQueuedPredecessors() { 32 // 簡單而言,就是判斷當前線程是否有前驅結點 33 // 當前結點含有前驅結點時候返回true;當前結點爲頭結點揮着隊列爲空的時候返回false 34 Node t = tail; // Read fields in reverse initialization order 35 Node h = head; 36 Node s; 37 return h != t && 38 ((s = h.next) == null || s.thread != Thread.currentThread()); 39 }

d)公平鎖和非公平鎖的測試

  ①測試目的

  驗證上面經過源碼分析的,非公平鎖在獲取鎖的時候會首先進行搶鎖,在獲取鎖失敗後纔會將當前線程加入同步隊列隊尾中,而公平鎖則是符合請求的絕對順序,也就是會按照先來後到FIFO。在下面的代碼中咱們使用一個靜態內部類繼承了ReentrantLock並重寫等待隊列的方法,做爲測試的ReentrantLock。而後建立5個線程,每一個線程連續兩次去獲取鎖,分別測試公平鎖和非公平鎖的測試結果

 1 import java.util.ArrayList;
 2 import java.util.Collection;
 3 import java.util.Collections;
 4 import java.util.List;
 5 import java.util.concurrent.locks.Lock;
 6 import java.util.concurrent.locks.ReentrantLock;
 7 
 8 import org.junit.Test;
 9 
10 public class TestReentrantLock {
11     /**
12      * ReentrantLock的構造方法
13      * public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
14      */
15     private Lock fairLock = new ReentrantLock2(true);
16     private Lock unFairLock = new ReentrantLock2(false);
17     
18     @Test
19     public void testFair() throws InterruptedException {
20         testLock(fairLock); //測試公平鎖
21     }
22     
23     @Test
24     public void testUnFair() throws InterruptedException {
25         testLock(unFairLock); //測試非公平鎖
26     }
27     
28     private void testLock(Lock lock) throws InterruptedException {
29         for (int i = 0; i < 5; i++) {
30             Thread thread = new Thread(new Job(lock)) {
31                 public String toString() {
32                         return getName();
33                 }
34             };
35             thread.setName(i+"");
36             thread.start();
37         }
38         Thread.sleep(12000);
39     }
40     
41     private static class Job extends Thread {
42         private Lock lock;
43         public Job(Lock lock) {
44             this.lock = lock;
45         }
46         @Override
47         public void run() {
48             //兩次打印當前線程和等待隊列中的Threads
49             for (int i = 0; i < 2; i++) {
50                 lock.lock(); //獲取鎖
51                 try {
52                     Thread.sleep(1000);
53                     System.out.println("當前線程=>" + Thread.currentThread().getName() + " " +
54                             "等待隊列中的線程=>" + ((ReentrantLock2)lock).getQueuedThreads());
55                 } catch (InterruptedException e) {
56                     e.printStackTrace();
57                 } finally {
58                     lock.unlock(); //釋放鎖
59                 }
60             }
61         }
62         
63     }
64     
65     private static class ReentrantLock2 extends ReentrantLock {
66         public ReentrantLock2(boolean fair) {
67             super(fair);
68         }
69         public Collection<Thread> getQueuedThreads() { //逆序打印等待隊列中的線程
70             List<Thread> list = new ArrayList<Thread>(super.getQueuedThreads());
71             Collections.reverse(list);
72             return list;
73         }
74     }
75     
76     
77 }

  ②測試非公平鎖

  由上面的測試結果簡單的獲得關於非公平鎖的一個結論:經過nofairTryAcquire方法能夠獲得這樣一個前提,當一個線程請求一個鎖時,判斷獲取成功的條件就是這個線程獲取到同步狀態就能夠,那麼某個剛剛釋放鎖的線程再次獲取到同步狀態的概率就會更大一些(固然實驗中也出現並不是連續兩次獲取這把鎖的狀況,好比下面的測試結果)

  ③測試公平鎖

  經過分析下面的測試結果,對於使用公平鎖而言,即使是同一個線程連續兩次獲取鎖釋放鎖,在第一次釋放鎖以後仍是會被放在隊尾並從隊列頭部拿出線程進行執行。並無出現像非公平鎖那樣連續兩次獲取鎖的那種狀況

   ④由上面的測試能夠看出:非公平鎖可能致使在隊尾的線程飢餓,可是又由於同一個線程在釋放鎖的時候有更大的機率再次獲取到這把鎖,那麼這樣的話線程的切換次數就會更少(這帶來的就是更大的吞吐量和開銷的減少)。而雖然公平鎖的獲取嚴格按照FIFO的規則,可是線程切換的次數就會更多。

3、Synchronized

一、Synchronized做用對象

①對於普通方法,鎖的是當前實例對象

②對於靜態同步方法,鎖的是類的Class對象

③對於同步代碼塊,鎖的是Synchronized括號中的對象

以下所示的三種狀況

 1 package cn.source.sync;
 2 
 3 public class TestSync01 {
 4     private static int count = 0;
 5     private Object object = new Object();
 6 
 7     public void testSyn1() {
 8         //同步代碼塊(這裏面是鎖臨界資源,即括號中的對象)
 9         synchronized (object) {
10             System.out.println(Thread.currentThread().getName()
11                 +" count =" + count++);
12         }
13     }
14 
15     public void testSyn2() {
16         //鎖當前對象(至關於普通同步方法)
17         synchronized (this) {
18             System.out.println(Thread.currentThread().getName()
19                     +" count =" + count++);
20         }
21     }
22 
23     //普通同步方法:鎖當前對象
24     public synchronized void testSyn3() {
25         System.out.println(Thread.currentThread().getName()
26                 +" count =" + count++);
27     }
28 
29     //靜態同步方法,鎖的是當前類型的類對象(即TestSync01.class)
30     public static synchronized void testSyn4() {
31         System.out.println(Thread.currentThread().getName()
32                 +" count =" + count++);
33     }
34 
35     //下面的這種方式也是鎖當前類型的類對象
36     public static void testSyn5() {
37         synchronized (TestSync01.class) {
38             System.out.println(Thread.currentThread().getName()
39                     +" count =" + count ++);
40         }
41     }
42 }

二、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、重量級鎖十、偏向鎖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 自動執行若干次空循環,再次申請鎖,而不是進入阻塞狀態的狀況。稱爲自旋鎖。自旋鎖提升效率就是避免線程狀態的變動

③重量級鎖

  在自旋過程當中,爲了不無用的自旋(好比得到鎖的線程被阻塞住了),鎖就會被升級爲重量級鎖。在重量級鎖的狀態下,其餘線程視圖獲取鎖的時候都會被阻塞住,只有持有鎖的線程釋放鎖以後纔會喚醒那些阻塞的線程,這些線程就開始競爭鎖。

四、關於synchronized的其餘說明

a)關於同步方法和非同步方法

同步方法隻影響 鎖定同一個鎖對象的同步方法,不影響非同步方法被其餘線程調用,也不影響其餘所資源的同步方法(簡單理解就是鎖的不是同一個資源,就不會影響);

b)synchronized是可重入的

同一個線程,屢次調用同步代碼,鎖定同一個對象,是可重入的;

c)關於同步的繼承問題

同一個線程中,子類同步方法覆蓋父類的同步方法,能夠指定調用父類的同步方法(至關於鎖的重入)

d)鎖與異常

當同步方法出現異常的時候會自動釋放鎖,不會影響其餘線程的執行

e)synchronized鎖的是對象,而不是引用

同步代碼一旦加鎖以後會有一個臨時鎖引用執行鎖對象,和真實的引用無直接關聯,在鎖釋放以前,修改鎖對象引用不會影響同步代碼塊的執行

f)synchronized中的常量問題

在定義同步代碼塊的時候,不要使用常量對象做爲鎖對象

相關文章
相關標籤/搜索