synchronized和ReentrantLock

一、線程同步問題的產生及解決方案

問題的產生:
Java允許多線程併發控制,當多個線程同時操作一個可共享的資源變量時(如數據的增刪改查),將會導致數據不準確,相互之間產生衝突。
如下例:假設有一個賣票系統,一共有100張票,有4個窗口同時賣。

public class Ticket implements Runnable {
    // 當前擁有的票數
    private int num = 100;

    public void run() {
        while (true) {
            if (num > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                }
                // 輸出賣票信息
                System.out.println(Thread.currentThread().getName() + ".....sale...." + num--);
            }
        }
    }
}
public static void main(String[] args) {
    Ticket t = new Ticket();//創建一個線程任務對象。   
        //創建4個線程同時賣票  
        Thread t1 = new Thread(t);  
        Thread t2 = new Thread(t);  
        Thread t3 = new Thread(t);  
        Thread t4 = new Thread(t);  
        //啓動線程  
        t1.start();  
        t2.start();  
        t3.start();  
        t4.start();  
    }

輸出部分結果:

Thread-1.....sale....2
Thread-0.....sale....3
Thread-2.....sale....1
Thread-0.....sale....0
Thread-1.....sale....0
Thread-3.....sale....1

顯然上述結果是不合理的,對於同一張票進行了多次售出。這就是多線程情況下,出現了數據“髒讀”情況。即多個線程訪問餘票num時,當一個線程獲得餘票的數量,要在此基礎上進行-1的操作之前,其他線程可能已經賣出多張票,導致獲得的num不是最新的,然後-1後更新的數據就會有誤。這就需要線程同步的實現了。

問題的解決:
因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調用,從而保證了該變量的唯一性和準確性。

一共有兩種鎖,來實現線程同步問題,分別是:synchronized和ReentrantLock。下面我們就帶着上述問題,看看這兩種鎖是如何解決的。

二、synchronized關鍵字

1.synchronized簡介

synchronized實現同步的基礎:Java中每個對象都可以作爲鎖。當線程試圖訪問同步代碼時,必須先獲得對象鎖,退出或拋出異常時必須釋放鎖。Synchronzied實現同步的表現形式分爲:代碼塊同步和方法同步。

2.synchronized原理

JVM基於進入和退出Monitor對象來實現代碼塊同步和方法同步,兩者實現細節不同。

代碼塊同步:在編譯後通過將monitorenter指令插入到同步代碼塊的開始處,將monitorexit指令插入到方法結束處和異常處,通過反編譯字節碼可以觀察到。任何一個對象都有一個monitor與之關聯,線程執行monitorenter指令時,會嘗試獲取對象對應的monitor的所有權,即嘗試獲得對象的鎖。

方法同步:synchronized方法在method_info結構有ACC_synchronized標記,線程執行時會識別該標記,獲取對應的鎖,實現方法同步。

兩者雖然實現細節不同,但本質上都是對一個對象的監視器(monitor)的獲取。任意一個對象都擁有自己的監視器,當同步代碼塊或同步方法時,執行方法的線程必須先獲得該對象的監視器才能進入同步塊或同步方法,沒有獲取到監視器的線程將會被阻塞,並進入同步隊列,狀態變爲BLOCKED。當成功獲取監視器的線程釋放了鎖後,會喚醒阻塞在同步隊列的線程,使其重新嘗試對監視器的獲取。

對象、監視器、同步隊列和執行線程間的關係如下圖:


3.synchronized的使用場景

①方法同步

public synchronized void method1

鎖住的是該對象,類的其中一個實例,當該對象(僅僅是這一個對象)在不同線程中執行這個同步方法時,線程之間會形成互斥。達到同步效果,但如果不同線程同時對該類的不同對象執行這個同步方法時,則線程之間不會形成互斥,因爲他們擁有的是不同的鎖。

②代碼塊同步

synchronized(this){ //TODO }

描述同①

③方法同步

public synchronized static void method3

鎖住的是該類,當所有該類的對象(多個對象)在不同線程中調用這個static同步方法時,線程之間會形成互斥,達到同步效果。

④代碼塊同步

synchronized(Test.class){ //TODO}

同③

⑤代碼塊同步

synchronized(o) {}

這裏面的o可以是一個任何Object對象或數組,並不一定是它本身對象或者類,誰擁有o這個鎖,誰就能夠操作該塊程序代碼。

4.解決線程同步的實例

針對上述方法,具體的解決方式如下:

public class Ticket implements Runnable {
    // 當前擁有的票數
    private int num = 100;

    public void run() {
        while (true) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                }
                synchronized (this) {
                    // 輸出賣票信息
                    if(num>0){
                        System.out.println(Thread.currentThread().getName() + ".....sale...." + num--);
                    }

                }
        }
    }
}

輸出部分結果:

Thread-2.....sale....10
Thread-1.....sale....9
Thread-3.....sale....8
Thread-0.....sale....7
Thread-2.....sale....6
Thread-1.....sale....5
Thread-2.....sale....4
Thread-1.....sale....3
Thread-3.....sale....2
Thread-0.....sale....1

可以看出實現了線程同步。同時改了一下邏輯,在進入到同步代碼塊時,先判斷現在是否有沒有票,然後再買票,防止出現沒票還要售出的情況。通過同步代碼塊實現了線程同步,其他方法也一樣可以實現該效果。

三、ReentrantLock鎖

1.Lock接口

Lock,鎖對象。在Java中鎖是用來控制多個線程訪問共享資源的方式,一般來說,一個鎖能夠防止多個線程同時訪問共享資源(但有的鎖可以允許多個線程併發訪問共享資源,比如讀寫鎖,後面我們會分析)。在Lock接口出現之前,Java程序是靠synchronized關鍵字(後面分析)實現鎖功能的,而JAVA SE5.0之後併發包中新增了Lock接口用來實現鎖的功能,它提供了與synchronized關鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖,缺點就是缺少像synchronized那樣隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性,可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性。

Lock接口的主要方法(還有兩個方法比較複雜,暫不介紹):

void lock(): 執行此方法時, 如果鎖處於空閒狀態, 當前線程將獲取到鎖. 相反, 如果鎖已經被其他線程持有, 將禁用當前線程, 直到當前線程獲取到鎖.
boolean tryLock():如果鎖可用, 則獲取鎖, 並立即返回true, 否則返回false. 該方法和lock()的區別在於, tryLock()只是"試圖"獲取鎖, 如果鎖不可用, 不會導致當前線程被禁用, 當前線程仍然繼續往下執行代碼. 而lock()方法則是一定要獲取到鎖, 如果鎖不可用, 就一直等待, 在未獲得鎖之前,當前線程並不繼續向下執行. 通常採用如下的代碼形式調用tryLock()方法:
void unlock():執行此方法時, 當前線程將釋放持有的鎖. 鎖只能由持有者釋放, 如果線程並不持有鎖, 卻執行該方法, 可能導致異常的發生.
Condition newCondition():條件對象,獲取等待通知組件。該組件和當前的鎖綁定,當前線程只有獲取了鎖,才能調用該組件的await()方法,而調用後,當前線程將縮放鎖。

ReentrantLock,一個可重入的互斥鎖,它具有與使用synchronized方法和語句所訪問的隱式監視器鎖相同的一些基本行爲和語義,但功能更強大。(重入鎖後面介紹)

2.ReentrantLock的使用

關於ReentrantLock的使用很簡單,只需要顯示調用,獲得同步鎖,釋放同步鎖即可。

ReentrantLock lock = new ReentrantLock(); //參數默認false,不公平鎖    
 .....................    
lock.lock(); //如果被其它資源鎖定,會在此等待鎖釋放,達到暫停的效果    
try {    
    //操作    
} finally {    
    lock.unlock();  //釋放鎖  
}
3.解決線程同步的實例

針對上述方法,具體的解決方式如下:

public class Ticket implements Runnable {
    // 當前擁有的票數
    private int num = 100;
    ReentrantLock lock = new ReentrantLock();

    public void run() {
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
            }

            lock.lock();
            // 輸出賣票信息
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + ".....sale...." + num--);
            }
            lock.unlock();

        }
    }
}

四、重入鎖

當一個線程得到一個對象後,再次請求該對象鎖時是可以再次得到該對象的鎖的。
具體概念就是:自己可以再次獲取自己的內部鎖。
Java裏面內置鎖(synchronized)和Lock(ReentrantLock)都是可重入的。

public class SynchronizedTest {
    public void method1() {
        synchronized (SynchronizedTest.class) {
            System.out.println("方法1獲得ReentrantTest的鎖運行了");
            method2();
        }
    }
    public void method2() {
        synchronized (SynchronizedTest.class) {
            System.out.println("方法1裏面調用的方法2重入鎖,也正常運行了");
        }
    }
    public static void main(String[] args) {
        new SynchronizedTest().method1();
    }
}

上面便是synchronized的重入鎖特性,即調用method1()方法時,已經獲得了鎖,此時內部調用method2()方法時,由於本身已經具有該鎖,所以可以再次獲取。

public class ReentrantLockTest {
    private Lock lock = new ReentrantLock();
    public void method1() {
        lock.lock();
        try {
            System.out.println("方法1獲得ReentrantLock鎖運行了");
            method2();
        } finally {
            lock.unlock();
        }
    }
    public void method2() {
        lock.lock();
        try {
            System.out.println("方法1裏面調用的方法2重入ReentrantLock鎖,也正常運行了");
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        new ReentrantLockTest().method1();
    }
}

上面便是ReentrantLock的重入鎖特性,即調用method1()方法時,已經獲得了鎖,此時內部調用method2()方法時,由於本身已經具有該鎖,所以可以再次獲取。

五、公平鎖

CPU在調度線程的時候是在等待隊列裏隨機挑選一個線程,由於這種隨機性所以是無法保證線程先到先得的(synchronized控制的鎖就是這種非公平鎖)。但這樣就會產生飢餓現象,即有些線程(優先級較低的線程)可能永遠也無法獲取CPU的執行權,優先級高的線程會不斷的強制它的資源。那麼如何解決飢餓問題呢,這就需要公平鎖了。公平鎖可以保證線程按照時間的先後順序執行,避免飢餓現象的產生。但公平鎖的效率比較低,因爲要實現順序執行,需要維護一個有序隊列。

ReentrantLock便是一種公平鎖,通過在構造方法中傳入true就是公平鎖,傳入false,就是非公平鎖。

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

以下是使用公平鎖實現的效果:

public class LockFairTest implements Runnable{
    //創建公平鎖
    private static ReentrantLock lock=new ReentrantLock(true);
    public void run() {
        while(true){
            lock.lock();
            try{
                System.out.println(Thread.currentThread().getName()+"獲得鎖");
            }finally{
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        LockFairTest lft=new LockFairTest();
        Thread th1=new Thread(lft);
        Thread th2=new Thread(lft);
        th1.start();
        th2.start();
    }
}

輸出結果:

Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖

這是截取的部分執行結果,分析結果可看出兩個線程是交替執行的,幾乎不會出現同一個線程連續執行多次。

六、synchronized和ReentrantLock的比較

1.區別:

1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;

2)synchronized在發生異常時,會自動釋放線程佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;

3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;

4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。

5)Lock可以提高多個線程進行讀操作的效率。

總結:ReentrantLock相比synchronized,增加了一些高級的功能。但也有一定缺陷。
在ReentrantLock類中定義了很多方法,比如:

isFair()        //判斷鎖是否是公平鎖

isLocked()    //判斷鎖是否被任何線程獲取了

isHeldByCurrentThread()   //判斷鎖是否被當前線程獲取了

hasQueuedThreads()   //判斷是否有線程在等待該鎖
2.兩者在鎖的相關概念上區別:

1)可中斷鎖
顧名思義,就是可以相應中斷的鎖。

在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。如果某一線程A正在執行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由於等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。

lockInterruptibly()的用法體現了Lock的可中斷性。

2)公平鎖

公平鎖即儘量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該鎖(並不是絕對的,大體上是這種順序),這種就是公平鎖。

非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些線程永遠獲取不到鎖。

在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。ReentrantLock可以設置成公平鎖。

3)讀寫鎖

讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。

正因爲有了讀寫鎖,才使得多個線程之間的讀操作可以併發進行,不需要同步,而寫操作需要同步進行,提高了效率。

ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。

可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。

4)綁定多個條件

一個ReentrantLock對象可以同時綁定多個Condition對象,而在synchronized中,鎖對象的wait()和notify()或notifyAll()方法可以實現一個隱含的條件,如果要和多餘一個條件關聯的時候,就不得不額外地添加一個鎖,而ReentrantLock則無須這麼做,只需要多次調用new Condition()方法即可。

3.性能比較

在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時ReentrantLock的性能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。

在JDK1.5中,synchronized是性能低效的。因爲這是一個重量級操作,它對性能最大的影響是阻塞的是實現,掛起線程和恢復線程的操作都需要轉入內核態中完成,這些操作給系統的併發性帶來了很大的壓力。相比之下使用Java提供的ReentrankLock對象,性能更高一些。到了JDK1.6,發生了變化,對synchronize加入了很多優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在JDK1.6上synchronize的性能並不比Lock差。官方也表示,他們也更支持synchronize,在未來的版本中還有優化餘地,所以還是提倡在synchronized能實現需求的情況下,優先考慮使用synchronized來進行同步。



作者:Ruheng
鏈接:http://www.jianshu.com/p/96c89e6e7e90
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章