Java併發編程:synchronized、Lock、ReentrantLock以及ReadWriteLock的那些事兒

前言

多線程開發中,同步控制是必不可少的手段。而同步的實現須要用到鎖,Java中提供了兩種基本的鎖,分別是synchronized 和 Lock。兩種鎖都很是經常使用,但也各有利弊,下面開始學習。程序員

synchronized用法

synchronized 是Java的關鍵字,是應用最爲普遍的同步工具之一。當它用來修飾一個方法或者一個代碼塊的時候,可以保證在同一時刻最多隻有一個線程執行該段代碼,同時,值得說明的是,它是在軟件層面依賴JVM實現同步的。安全

synchronized 的用法很簡單,直接用其修飾代碼塊便可,通常可將其用於修飾方法和代碼塊,根據修飾地方的不一樣還有不一樣的做用域,下面一一介紹。bash

修飾方法

synchronized 修飾方法分爲兩種狀況:多線程

  • 修飾實例方法,做用於當前實例加鎖,進入同步代碼前要得到當前實例的鎖。
  • 修飾靜態方法,做用於當前類對象加鎖,進入同步代碼前要得到當前類對象的鎖。

修飾實例方法

顧名思義就是修飾類中的實例方法,而且默認是當前對象做爲鎖的對象,而一個對象只有一把鎖,因此同一時刻只能有一個線程執行被同步的方法,等到線程執行完方法後,其餘線程才能繼續執行被同步的方法。實例代碼以下:併發

public class SyncTest implements Runnable{

    //靜態變量
    public static int TEST_INT = 0;

    //被同步的實例方法
    public synchronized void increase(){
        TEST_INT++;
    }

    @Override
    public void run() {
        for(int i=1;i<=100000;i++){
            increase();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //實例化對象
        SyncTest instance = new SyncTest();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(TEST_INT);
    }
}
複製代碼

運行上方的程序,結果會是200000,由於main函數中只實例化一個SyncTest對象,因此,兩個線程運行的時候只能有一個線程獲取到對象的鎖,當一個線程獲取了該對象的鎖以後,其餘線程沒法獲取該對象的鎖,因此沒法訪問該對象的其餘synchronized實例方法,固然其餘線程仍是能夠訪問該對象的非synchronized方法的。dom

不過,上面的狀況只是針對一個對象實例進行操做,若是有多個對象實例的話,修飾實例方法是沒法保證線程安全的,咱們能夠把main函數的程序修改下:ide

public static void main(String[] args) throws InterruptedException {
    //每一個線程實例化一個SyncTest對象
    Thread t1=new Thread(new SyncTest());
    Thread t2=new Thread(new SyncTest());
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(TEST_INT);
}
複製代碼

運行程序後,會發現結果永遠小於200000,說明synchronized沒有起到同步的做用了,說明修飾實例方法只能做用實例對象,不能做用到類對象。函數

修飾靜態方法

要想synchronized同步到類對象自己,能夠用它修飾類中的靜態方法。修改下上述代碼中的increase方法爲靜態方法,並在main函數中新建兩條線程:工具

//被同步的靜態方法
public synchronized static void increase(){
    TEST_INT++;
}
public static void main(String[] args) throws InterruptedException {
    //每一個線程實例化一個SyncTest對象
    Thread t1=new Thread(new SyncTest());
    Thread t2=new Thread(new SyncTest());
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(TEST_INT);
}
複製代碼

運行程序,結果是200000,說明synchronized是做用到類對象自己的,其鎖對象是當前類的class對象,因此,無論實例化多個對象實例時,被同步的方法同一時刻只能被一個線程執行。性能

同步代碼塊

除了同步實例方法和靜態方法外,還可使用synchronized 同步代碼塊,某些狀況下,咱們可能只須要同步一小塊代碼,假設代碼所在的方法體量太大的話,直接同步整個方法會影響程序的運行效率,這種狀況下同步代碼塊就很是的合適,實例代碼以下:

public class SyncTest implements Runnable{
	
    public static SyncTest instance = new SyncTest();

    //靜態變量
    public static int TEST_INT = 0;
    
	@Override
    public void run() {
        synchronized (instance) {
            for (int i = 1; i <= 100000; i++) {
                TEST_INT++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //每一個線程實例化一個SyncTest對象
        Thread t1=new Thread(new SyncTest());
        Thread t2=new Thread(new SyncTest());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(TEST_INT);
    }
}
複製代碼

上面的代碼中,在run()方法中對實例對象instance作了同步處理,運行程序後輸出的結果爲200000。之因此能達到同步的效果,是由於每次當線程進入synchronized包裹的代碼塊時就會要求當前線程持有instance這個實例對象鎖,其餘的線程就必須等待,這樣也就保證了每次只有一個線程執行被同步的代碼塊。

引出Lock

synchronized的用法仍是比較簡單的,同步的效果也比較明顯,儘管如此,synchronized自己仍是存在着很多缺陷,好比對鎖的釋放。

當線程執行到synchronized同步的程序後會獲取對應的鎖,其餘的線程要一直等待,等到該線程釋放對應的鎖,而該線程釋放鎖的狀況無非是這兩種:

  • 線程執行完了該代碼塊,而後釋放對鎖的佔有;
  • 線程執行過程發生異常,此時JVM會讓線程自動釋放鎖。

由於synchronized是由JDK實現的,不須要程序員編寫代碼去控制加鎖和釋放。這種釋放機制有很大的弊端,舉個例子,若是獲取到該鎖的線程有很是耗時的程序,例如等待IO或者被阻塞了,而後沒有及時釋放鎖,那麼其餘的線程就必須一直等待,白白浪費了很多時間,這樣的結果顯然不是咱們想看到的,那麼有什麼辦法能解決呢?

針對這樣的狀況,Lock就派上用場了。Lock是Java併發工具包下提供的一個接口,一樣能夠實現同步訪問。

與synchronized不一樣的是,Lock要求程序員手動控制加鎖和釋放,它不會自動釋放鎖,若是沒有手動釋放鎖,線程會一直佔用鎖,可能形成死鎖現象。

Lock用法

Lock是一個接口,點開源碼,能夠發現其代碼中定義這幾個方法:

public interface Lock {
    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;

    void unlock();

    Condition newCondition();
}
複製代碼

其中,lock()、lockInterruptibly()、tryLock()、unlock()都是對鎖的獲取操做,unLock()是釋放鎖的方法,newCondition()是返回一個Condition接口,Condition接口能夠代替Object監視器方法的使用,至關於充當了Object.wait() 和Object.notify() 的做用,起到線程等待和通知的做用。

前面說到了Lock必須手動釋放鎖的操做,因此,當調用Lock的獲取鎖方法後,在執行完程序時還須要調用釋放鎖的方法,用法大體以下:

Lock lock = new ReentrantLock();
lock.lock();
try {
    //.............執行程序..........
} finally {

    lock.unlock();
}
複製代碼

經過捕獲異常的方式來調用Lock釋放鎖的方法,這樣就能保證即便程序發生異常也能成功釋放鎖。

值得說明的是,Lock只是一個接口,在做爲同步工具使用時,必須先實例化它的子類,而代碼中的ReentrantLock就是Lock的子類。

子類:ReentrantLock

ReentrantLock是Lock一個很是強大的子類,意思是 「可重入鎖」,那麼可重入鎖是什麼意思呢?後面會細說,先展現ReentrantLock的具體用法。

public class LockTest implements Runnable {

    public Lock lock = new ReentrantLock();
    public static int i = 0;

    @Override
    public void run() {
        for (int j = 0;j<100000;j++){
            lock.lock();
            try {
                i++;
            } finally {

                lock.unlock();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        LockTest lt = new LockTest();
        Thread t1 = new Thread(lt);
        Thread t2 = new Thread(lt);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
複製代碼

咱們在 LockTest 的 run() 里加了ReentrantLock保護臨界區資源 i,確保多線程對臨界區資源操做的安全性,執行main方法,能夠看到結果成功輸出 200000。說明ReentrantLock 確實起到了同步 的做用。

接着說回可重入鎖的話題,之因此這麼叫,是由於這種鎖是能夠重複進入的,例如,改造一下run()方法中的代碼:

@Override
public void run() {
    for (int j = 0;j<100000;j++){
        lock.lock();
        lock.lock();
        try {
            i++;
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }
}
複製代碼

運行main方法,代碼正常輸出200000。說明鎖能夠被連續使用,由於若是不能被連續使用的話,那麼當第二次獲取鎖時,將會由於第一個鎖沒釋放而一直在等待,同時第二個鎖的釋放又必須等第二個鎖獲取並執行 i++ 的程序後才能實現,這樣就至關於線程與本身產生了死鎖。固然,還須要注意一點,那就是線程獲取鎖的次數和釋放次數必須是相同的,不然就會拋出異常。

讀寫分離鎖:ReadWriteLock

除了Lock接口外,Java的API還提供了另外一種讀寫分離鎖,那就是ReadWriteLock。ReadWriteLock是JDK1.5後才引入的,做爲讀寫分離鎖,能夠有效的幫助減小鎖的競爭,提高系統性能。

用鎖分離的機制來提高性能比較好理解。舉個例子,有三個線程A一、A二、A3進行寫操做,三個線程B一、B二、B3進行讀的操做。若是使用重入鎖或者synchronized(內部鎖),理論上全部的讀之間、讀與寫之間、寫與寫之間都是串行操做。當B1進行讀取時,B二、B3則必須進行等待。因爲讀操做並不對數據的完整性進行破壞,因此這種等待是不合理的。所以,讀寫分離鎖就派上了用場,它能支持多個讀的操做並行執行。

須要注意的是,讀寫分離鎖只是針對讀讀之間可以並行,在讀寫和寫寫之間依然會互斥,總結起來就是這三種狀況:

  • 讀-讀不互斥:讀讀之間不阻塞;
  • 讀-寫互斥:讀阻塞寫,寫也會阻塞讀;
  • 寫-寫互斥:寫寫阻塞;

概念上就大概是這樣了,下面就是如何使用了。先看一下ReadWriteLock的源碼:

public interface ReadWriteLock {
    Lock readLock();

    Lock writeLock();
}
複製代碼

能夠看出,ReadWriteLock是一個接口,而且只提供了兩個方法,從字面上很容易就能夠理解,分別是寫入鎖的方法 readLock 和 讀取鎖的方法 writeLock ,返回的都是Lock接口。

值得說明的是,ReadWriteLock是一個接口,其使用的方式和Lock相似,都是須要先實例化接口的實現類,而其子類只有一個,那就是 ReentrantReadWriteLock,下面用一段代碼來測驗一下讀寫鎖的性能:

public class ReadWriteLockDemo {

    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock writeLock = readWriteLock.readLock();
    private static Lock readLock = readWriteLock.readLock();
    private int i;

    //讀的方法
    public int ReadValue(Lock lock) throws Exception {
        try {
            lock.lock();
            Thread.sleep(1000);
            return i;
        } finally {
            lock.unlock();
        }
    }

    //寫的方法
    public void setValue(Lock lock, int value) throws Exception {
        try {
            lock.lock();
            Thread.sleep(1000);
            i = value;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final ReadWriteLockDemo demo = new ReadWriteLockDemo();
        Runnable readRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    demo.ReadValue(readLock);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };

        Runnable writeRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    demo.setValue(writeLock, new Random().nextInt());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };

        for (int i = 0; i < 20; i++) {
            new Thread(readRunnable).start();
        }
        for (int j = 0; j < 2; j++) {
            new Thread(writeRunnable).start();
        }
    }
}
複製代碼

先說明一下這段代碼,在ReadWriteLockDemo類中定義了一個ReentrantReadWriteLock實例,並建立它的讀寫對象,分別是 writeLockreadLock,同時,在類中還定義了一個讀的方法和寫的方法,用Thread.sleep模擬了耗時操做,分別對應讀耗時和寫耗時。main函數裏定義讀的線程和寫的線程,同時用for循環開啓了20個讀線程和2個寫的線程。

以上的代碼採用的就是簡單的讀寫分離操做,正常運行後,程序兩秒多鍾就結束了 ,這說明,讀的線程之間是並行的,而寫的線程之間會相互阻塞,這也印證了以前的結論。

讀寫分離鎖就講到這吧,關於ReentrantReadWriteLock自己還有不少妙用,這裏就不展開了。

Lock和synchronized比較

最後,說一下老生常談的話題吧,就是對Lock和synchronized作個對比總結。

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

二、synchronized由程序自動釋放鎖,而Lock須要程序員手動釋放,避免死鎖;

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

四、Lock能夠知道是否成功得到鎖,但synchronized不行;

五、Lock支持可重入鎖,但synchronized不行;

六、synchronized鎖的範圍是整個方法或代碼塊;而Lock是方法調用的方式,靈活性更大;

七、ReadWriteLock能夠提高多個線程進行讀操做的效率,而synchronized作不到;

再說明一點,從JDK1.6開始,synchronized的性能已經作到了很大的優化,若是是競爭資源不激烈也就是線程很少的狀況下,synchronized和Lock的性能是差很少的,而若是資源競爭比較激烈,使用Lock的性能要遠遠優於synchronized的。

因此,仍是那句話,根據不一樣的場景選擇適合的技術纔是最好的。

相關文章
相關標籤/搜索