詳解synchronized與Lock的區別與使用

引言:

昨天在學習別人分享的面試經驗時,看到Lock的使用。想起自己在上次面試也遇到了synchronized與Lock的區別與使用。於是,我整理了兩者的區別和使用情況,同時,對synchronized的使用過程一些常見問題的總結,最後是參照源碼和說明文檔,對Lock的使用寫了幾個簡單的Demo。請大家批評指正。

技術點:

1、線程與進程:

在開始之前先把進程與線程進行區分一下,一個程序最少需要一個進程,而一個進程最少需要一個線程。關係是線程–>進程–>程序的大致組成結構。所以線程是程序執行流的最小單位,而進程是系統進行資源分配和調度的一個獨立單位。以下我們所有討論的都是建立在線程基礎之上。

2、Thread的幾個重要方法:

我們先了解一下Thread的幾個重要方法。a、start()方法,調用該方法開始執行該線程;b、stop()方法,調用該方法強制結束該線程執行;c、join方法,調用該方法等待該線程結束。d、sleep()方法,調用該方法該線程進入等待。e、run()方法,調用該方法直接執行線程的run()方法,但是線程調用start()方法時也會運行run()方法,區別就是一個是由線程調度運行run()方法,一個是直接調用了線程中的run()方法!!

看到這裏,可能有些人就會問啦,那wait()和notify()呢?要注意,其實wait()與notify()方法是Object的方法,不是Thread的方法!!同時,wait()與notify()會配合使用,分別表示線程掛起和線程恢復。

這裏還有一個很常見的問題,順帶提一下:wait()與sleep()的區別,簡單來說wait()會釋放對象鎖而sleep()不會釋放對象鎖。這些問題有很多的資料,不再贅述。

3、線程狀態:

這裏寫圖片描述

線程總共有5大狀態,通過上面第二個知識點的介紹,理解起來就簡單了。

  • 新建狀態:新建線程對象,並沒有調用start()方法之前

  • 就緒狀態:調用start()方法之後線程就進入就緒狀態,但是並不是說只要調用start()方法線程就馬上變爲當前線程,在變爲當前線程之前都是爲就緒狀態。值得一提的是,線程在睡眠和掛起中恢復的時候也會進入就緒狀態哦。

  • 運行狀態:線程被設置爲當前線程,開始執行run()方法。就是線程進入運行狀態

  • 阻塞狀態:線程被暫停,比如說調用sleep()方法後線程就進入阻塞狀態

  • 死亡狀態:線程執行結束

4、鎖類型

  • 可重入鎖:在執行對象中所有同步方法不用再次獲得鎖

  • 可中斷鎖:在等待獲取鎖過程中可中斷

  • 公平鎖: 按等待獲取鎖的線程的等待時間進行獲取,等待時間長的具有優先獲取鎖權利

  • 讀寫鎖:對資源讀取和寫入的時候拆分爲2部分處理,讀的時候可以多線程一起讀,寫的時候必須同步地寫


synchronized與Lock的區別

1、我把兩者的區別分類到了一個表中,方便大家對比:

類別synchronizedLock
存在層次Java的關鍵字,在jvm層面上是一個類
鎖的釋放1、以獲取鎖的線程執行完同步代碼,釋放鎖  2、線程執行發生異常,jvm會讓線程釋放鎖在finally中必須釋放鎖,不然容易造成線程死鎖
鎖的獲取假設A線程獲得鎖,B線程等待。如果A線程阻塞,B線程會一直等待分情況而定,Lock有多個鎖獲取的方式,具體下面會說道,大致就是可以嘗試獲得鎖,線程可以不用一直等待
鎖狀態無法判斷可以判斷
鎖類型可重入 不可中斷  非公平可重入 可判斷 可公平(兩者皆可)
性能少量同步大量同步

或許,看到這裏還對LOCK所知甚少,那麼接下來,我們進入LOCK的深入學習。

Lock詳細介紹與Demo

以下是Lock接口的源碼,筆者修剪之後的結果:

public interface Lock {

    /**
     * Acquires the lock.
     */
    void lock();    /**
     * Acquires the lock unless the current thread is
     * {@linkplain Thread#interrupt interrupted}.
     */
    void lockInterruptibly() throws InterruptedException;    /**
     * Acquires the lock only if it is free at the time of invocation.
     */
    boolean tryLock();    /**
     * Acquires the lock if it is free within the given waiting time and the
     * current thread has not been {@linkplain Thread#interrupt interrupted}.
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;    /**
     * Releases the lock.
     */
    void unlock();

}123456789101112131415161718192021222324252627282930
從Lock接口中我們可以看到主要有個方法,這些方法的功能從註釋中可以看出:12
  • lock():獲取鎖,如果鎖被暫用則一直等待

  • unlock():釋放鎖

  • tryLock(): 注意返回類型是boolean,如果獲取鎖的時候鎖被佔用就返回false,否則返回true

  • tryLock(long time, TimeUnit unit):比起tryLock()就是給了一個時間期限,保證等待參數時間

  • lockInterruptibly():用該鎖的獲得方式,如果線程在獲取鎖的階段進入了等待,那麼可以中斷此線程,先去做別的事

通過 以上的解釋,大致可以解釋在上個部分中“鎖類型(lockInterruptibly())”,“鎖狀態(tryLock())”等問題,還有就是前面子所獲取的過程我所寫的“大致就是可以嘗試獲得鎖,線程可以不會一直等待”用了“可以”的原因。

下面是Lock一般使用的例子,注意ReentrantLock是Lock接口的實現。12

lock():

package com.brickworkers;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class LockTest {
    private Lock lock = new ReentrantLock();    //需要參與同步的方法
    private void method(Thread thread){
        lock.lock();        try {
            System.out.println("線程名"+thread.getName() + "獲得了鎖");
        }catch(Exception e){
            e.printStackTrace();
        } finally {
            System.out.println("線程名"+thread.getName() + "釋放了鎖");
            lock.unlock();
        }
    }    public static void main(String[] args) {
        LockTest lockTest = new LockTest();        //線程1
        Thread t1 = new Thread(new Runnable() {            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}//執行情況:線程名t1獲得了鎖//         線程名t1釋放了鎖//         線程名t2獲得了鎖//         線程名t2釋放了鎖1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950

tryLock():

package com.brickworkers;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class LockTest {
    private Lock lock = new ReentrantLock();    //需要參與同步的方法
    private void method(Thread thread){/*      lock.lock();
        try {
            System.out.println("線程名"+thread.getName() + "獲得了鎖");
        }catch(Exception e){
            e.printStackTrace();
        } finally {
            System.out.println("線程名"+thread.getName() + "釋放了鎖");
            lock.unlock();
        }*/


        if(lock.tryLock()){            try {
                System.out.println("線程名"+thread.getName() + "獲得了鎖");
            }catch(Exception e){
                e.printStackTrace();
            } finally {
                System.out.println("線程名"+thread.getName() + "釋放了鎖");
                lock.unlock();
            }
        }else{
            System.out.println("我是"+Thread.currentThread().getName()+"有人佔着鎖,我就不要啦");
        }
    }    public static void main(String[] args) {
        LockTest lockTest = new LockTest();        //線程1
        Thread t1 = new Thread(new Runnable() {            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}//執行結果: 線程名t2獲得了鎖//         我是t1有人佔着鎖,我就不要啦//         線程名t2釋放了鎖12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364

看到這裏相信大家也都會使用如何使用Lock了吧,關於tryLock(long time, TimeUnit unit)和lockInterruptibly()不再贅述。前者主要存在一個等待時間,在測試代碼中寫入一個等待時間,後者主要是等待中斷,會拋出一箇中斷異常,常用度不高,喜歡探究可以自己深入研究。

前面比較重提到“公平鎖”,在這裏可以提一下ReentrantLock對於平衡鎖的定義,在源碼中有這麼兩段:12
 /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {        private static final long serialVersionUID = 7316153563782823691L;        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());            else
                acquire(1);
        }        protected final boolean tryAcquire(int acquires) {            return nonfairTryAcquire(acquires);
        }
    }    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {        private static final long serialVersionUID = -3000897897090466540L;        final void lock() {
            acquire(1);
        }        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {            final Thread current = Thread.currentThread();            int c = getState();            if (c == 0) {                if (!hasQueuedPredecessors() &&
                    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);                return true;
            }            return false;
        }
    }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657

從以上源碼可以看出在Lock中可以自己控制鎖是否公平,而且,默認的是非公平鎖,以下是ReentrantLock的構造函數:

   public ReentrantLock() {
        sync = new NonfairSync();//默認非公平鎖
    }123

尾記錄:

筆者水平一般,不過此博客在引言中的目的已全部達到。這只是筆者在學習過程中的總結與概括,如存在不正確的,歡迎大家批評指出。12

延伸學習:對於LOCK底層的實現,大家可以參考:  
點擊Lock底層介紹博客

兩種同步方式性能測試,大家可以參考:
點擊查看兩種同步方式性能測試博客

博主18年3月新增:

回來看自己博客。發現東西闡述的不夠完整。這裏在做補充,因爲這篇博客訪問較大,所以爲了不誤導大家,儘量介紹給大家正確的表述:
1、兩種鎖的底層實現方式:
   synchronized:我們知道java是用字節碼指令來控制程序(這裏不包括熱點代碼編譯成機器碼)。在字節指令中,存在有synchronized所包含的代碼塊,那麼會形成2段流程的執行。
   這裏寫圖片描述

我們點擊查看SyncDemo.java的源碼SyncDemo.class,可以看到如下:
這裏寫圖片描述

如上就是這段代碼段字節碼指令,沒你想的那麼難吧。言歸正傳,我們可以清晰段看到,其實synchronized映射成字節碼指令就是增加來兩個指令:monitorenter和monitorexit。當一條線程進行執行的遇到monitorenter指令的時候,它會去嘗試獲得鎖,如果獲得鎖那麼鎖計數+1(爲什麼會加一呢,因爲它是一個可重入鎖,所以需要用這個鎖計數判斷鎖的情況),如果沒有獲得鎖,那麼阻塞。當它遇到monitorexit的時候,鎖計數器-1,當計數器爲0,那麼就釋放鎖。

那麼有的朋友看到這裏就疑惑了,那圖上有2個monitorexit呀?馬上回答這個問題:上面我以前寫的文章也有表述過,synchronized鎖釋放有兩種機制,一種就是執行完釋放;另外一種就是發送異常,虛擬機釋放。圖中第二個monitorexit就是發生異常時執行的流程,這就是我開頭說的“會有2個流程存在“。而且,從圖中我們也可以看到在第13行,有一個goto指令,也就是說如果正常運行結束會跳轉到19行執行。

這下,你對synchronized是不是瞭解的很清晰了呢。接下來我們再聊一聊Lock。

Lock:Lock實現和synchronized不一樣,後者是一種悲觀鎖,它膽子很小,它很怕有人和它搶吃的,所以它每次吃東西前都把自己關起來。而Lock呢底層其實是CAS樂觀鎖的體現,它無所謂,別人搶了它吃的,它重新去拿吃的就好啦,所以它很樂觀。具體底層怎麼實現,博主不在細述,有機會的話,我會對concurrent包下面的機制好好和大家說說,如果面試問起,你就說底層主要靠volatile和CAS操作實現的。

現在,纔是我真正想在這篇博文後面加的,我要說的是:儘可能去使用synchronized而不要去使用LOCK

什麼概念呢?我和大家打個比方:你叫jdk,你生了一個孩子叫synchronized,後來呢,你領養了一個孩子叫LOCK。起初,LOCK剛來到新家的時候,它很乖,很懂事,各個方面都表現的比synchronized好。你很開心,但是你內心深處又有一點淡淡的憂傷,你不希望你自己親生的孩子竟然還不如一個領養的孩子乖巧。這個時候,你對親生的孩子教育更加深刻了,你想證明,你的親生孩子synchronized並不會比領養的孩子LOCK差。(博主只是打個比方)

那如何教育呢?
在jdk1.6~jdk1.7的時候,也就是synchronized16、7歲的時候,你作爲爸爸,你給他優化了,具體優化在哪裏呢:

1、線程自旋和適應性自旋
我們知道,java’線程其實是映射在內核之上的,線程的掛起和恢復會極大的影響開銷。並且jdk官方人員發現,很多線程在等待鎖的時候,在很短的一段時間就獲得了鎖,所以它們在線程等待的時候,並不需要把線程掛起,而是讓他無目的的循環,一般設置10次。這樣就避免了線程切換的開銷,極大的提升了性能。
而適應性自旋,是賦予了自旋一種學習能力,它並不固定自旋10次一下。他可以根據它前面線程的自旋情況,從而調整它的自旋,甚至是不經過自旋而直接掛起。

2、鎖消除
什麼叫鎖消除呢?就是把不必要的同步在編譯階段進行移除。
那麼有的小夥伴又迷糊了,我自己寫的代碼我會不知道這裏要不要加鎖?我加了鎖就是表示這邊會有同步呀?
並不是這樣,這裏所說的鎖消除並不一定指代是你寫的代碼的鎖消除,我打一個比方:
在jdk1.5以前,我們的String字符串拼接操作其實底層是StringBuffer來實現的(這個大家可以用我前面介紹的方法,寫一個簡單的demo,然後查看class文件中的字節碼指令就清楚了),而在jdk1.5之後,那麼是用StringBuilder來拼接的。我們考慮前面的情況,比如如下代碼:

String str1="qwe";
String str2="asd";
String str3=str1+str2;123

底層實現會變成這樣:

StringBuffer sb = new StringBuffer();
sb.append("qwe");
sb.append("asd");123

我們知道,StringBuffer是一個線程安全的類,也就是說兩個append方法都會同步,通過指針逃逸分析(就是變量不會外泄),我們發現在這段代碼並不存在線程安全問題,這個時候就會把這個同步鎖消除。

3、鎖粗化
在用synchronized的時候,我們都講究爲了避免大開銷,儘量同步代碼塊要小。那麼爲什麼還要加粗呢?
我們繼續以上面的字符串拼接爲例,我們知道在這一段代碼中,每一個append都需要同步一次,那麼我可以把鎖粗化到第一個append和最後一個append(這裏不要去糾結前面的鎖消除,我只是打個比方)

4、輕量級鎖

5、偏向鎖

關於最後這兩種,我希望留個有緣的讀者自己去查找,我不希望我把一件事情描述的那麼詳細,自己動手得到纔是你自己的,博主可以告訴你的是,最後兩種並不難。。加油吧,各位。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章