詳解synchronized與Lock的區別與使用

知識點

一、線程與進程

  在開始以前先把進程與線程進行區分一下,一個程序最少須要一個進程,而一個進程最少須要一個線程。關係是線程–>進程–>程序的大體組成結構。因此線程是程序執行流的最小單位,而進程是系統進行資源分配和調度的一個獨立單位。如下咱們全部討論的都是創建在線程基礎之上。java

二、Thread的幾個重要方法

   咱們先了解一下Thread的幾個重要方法。面試

    a、start()方法,調用該方法開始執行該線程;安全

    b、stop()方法,調用該方法強制結束該線程執行;多線程

    c、join方法,調用該方法等待該線程結束。app

    d、sleep()方法,調用該方法該線程進入等待。less

    e、run()方法,調用該方法直接執行線程的run()方法,可是線程調用start()方法時也會運行run()方法,區別就是一個是由線程調度運行run()方法,一個是直接調用了線程中的run()方法!!ide

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

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

三、線程狀態

 

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

  新建狀態:新建線程對象,並無調用start()方法以前
  就緒狀態:調用start()方法以後線程就進入就緒狀態,可是並非說只要調用start()方法線程就立刻變爲當前線程,在變爲當前線程以前都是爲就緒狀態。值得一提的是,線程在睡眠和掛起中恢復的時候也會進入就緒狀態哦。
  運行狀態:線程被設置爲當前線程,開始執行run()方法。就是線程進入運行狀態
  阻塞狀態:線程被暫停,好比說調用sleep()方法後線程就進入阻塞狀態
  死亡狀態:線程執行結束

四、鎖類型

  可重入鎖:在執行對象中全部同步方法不用再次得到鎖
  可中斷鎖:在等待獲取鎖過程當中可中斷
  公平鎖: 按等待獲取鎖的線程的等待時間進行獲取,等待時間長的具備優先獲取鎖權利
  讀寫鎖:對資源讀取和寫入的時候拆分爲2部分處理,讀的時候能夠多線程一塊兒讀,寫的時候必須同步地寫

synchronized與Lock的區別

詳情對比見下表:

Lock詳解

以下爲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();
 
}

  從Lock接口中咱們能夠看到主要有5個方法,這些方法的功能從註釋中能夠看出:

    lock():獲取鎖,若是鎖被暫用則一直等待
    unlock():釋放鎖
    tryLock(): 注意返回類型是boolean,若是獲取鎖的時候鎖被佔用就返回false,不然返回true
    tryLock(long time, TimeUnit unit):比起tryLock()就是給了一個時間期限,保證等待參數時間
    lockInterruptibly():用該鎖的得到方式,若是線程在獲取鎖的階段進入了等待,那麼能夠中斷此線程,先去作別的事
  經過 以上的解釋,大體能夠解釋在上個部分中「鎖類型(lockInterruptibly())」,「鎖狀態(tryLock())」等問題,還有就是前面子所獲取的過程我所寫的「大體就是能夠嘗試得到鎖,線程能夠不會一直等待」用了「能夠」的緣由。

下面是Lock通常使用的例子,注意ReentrantLock是Lock接口的實現。

lock():

public class LockTest {
    private Lock lock = new ReentrantLock();

    private void method(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName() + " has gotten the lock!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(thread.getName() + " has unlocked the lock!");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final LockTest test = new LockTest();

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

}

運行結果:

t1 has gotten the lock!
t1 has unlocked the lock!
t2 has gotten the lock!
t2 has unlocked the lock!

tryLock():

public class LockTest {
    private Lock lock = new ReentrantLock();

    private void method(Thread thread) {
        /*lock.lock();
        try {
            System.out.println(thread.getName() + " has gotten the lock!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(thread.getName() + " has unlocked the lock!");
            lock.unlock();
        }*/
        if (lock.tryLock()) {
            lock.lock();
            try {
                System.out.println(thread.getName() + " has gotten the lock!");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println(thread.getName() + " has unlocked the lock!");
                lock.unlock();
            }
        } else {
            System.out.println("I'm "+thread.getName()+". Someone has gotten the lock!");
        }
    }

    public static void main(String[] args) {
        LockTest test = new LockTest();

        Thread t1 = new Thread(() -> test.method(Thread.currentThread()), "t1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                test.method(Thread.currentThread());
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

運行結果:

t1 has gotten the lock!
t1 has unlocked the lock!
I'm t2. Someone has gotten the lock!

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

  前面比較重提到「公平鎖」,在這裏能夠提一下ReentrantLock對於平衡鎖的定義,在源碼中有這麼兩段:

/**
    * 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;
       }
   }

  從以上源碼能夠看出在Lock中能夠本身控制鎖是否公平,並且,默認的是非公平鎖,如下是ReentrantLock的構造函數:

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

補充

一、兩種鎖的底層實現方式

  synchronized:咱們知道java是用字節碼指令來控制程序(這裏不包括熱點代碼編譯成機器碼)。在字節指令中,存在有synchronized所包含的代碼塊,那麼會造成2段流程的執行。

public class SyncDemo {
    public void sync(){
        synchronized (SyncDemo.class){
            System.out.println(" ");
        }
    }
}

咱們點擊查看SyncDemo.java的源碼SyncDemo.class,能夠看到以下:

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

  那麼有的朋友看到這裏就疑惑了,那圖上有2個monitorexit呀?由於synchronized鎖釋放有兩種機制,一種就是執行完釋放另一種就是發送異常,虛擬機釋放。圖中第二個monitorexit就是發生異常時執行的流程,這就是我開頭說的「會有2個流程存在「。並且,從圖中咱們也能夠看到在第13行,有一個goto指令,也就是說若是正常運行結束會跳轉到19行執行。

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

Lock

  Lock實現和synchronized不同,後者是一種悲觀鎖,它膽子很小,它很怕有人和它搶吃的,因此它每次吃東西前都把本身關起來。而Lock呢底層實際上是CAS樂觀鎖的體現,它無所謂,別人搶了它吃的,它從新去拿吃的就好啦,因此它很樂觀。具體底層怎麼實現,這裏不在細述。若是面試問起,就說底層主要靠volatile和CAS操做實現的。

  如今,我要說的是:儘量去使用synchronized而不要去使用LOCK

  什麼概念呢?我和你們打個比方:你叫jdk,你生了一個孩子叫synchronized,後來呢,你領養了一個孩子叫LOCK。起初,LOCK剛來到新家的時候,它很乖,很懂事,各個方面都表現的比synchronized好。你很開心,可是你心裏深處又有一點淡淡的憂傷,你不但願你本身親生的孩子居然還不如一個領養的孩子乖巧。這個時候,你對親生的孩子教育更加深入了,你想證實,你的親生孩子synchronized並不會比領養的孩子LOCK差。(只是打個比方)那如何教育呢?在jdk1.6~jdk1.7的時候,也就是synchronized1六、7歲的時候,你做爲爸爸,你給他優化了,具體優化在哪裏呢:

一、線程自旋和適應性自旋

  咱們知道,java線程實際上是映射在內核之上的,線程的掛起和恢復會極大的影響開銷。而且jdk官方人員發現,不少線程在等待鎖的時候,在很短的一段時間就得到了鎖,因此它們在線程等待的時候,並不須要把線程掛起,而是讓他無目的的循環,通常設置10次。這樣就避免了線程切換的開銷,極大的提高了性能。

  而適應性自旋,是賦予了自旋一種學習能力,它並不固定自旋10次一下。他能夠根據它前面線程的自旋狀況,從而調整它的自旋,甚至是不通過自旋而直接掛起。

二、鎖消除

  什麼叫鎖消除呢?就是把沒必要要的同步在編譯階段進行移除。那麼有的小夥伴又迷糊了,我本身寫的代碼我會不知道這裏要不要加鎖?我加了鎖就是表示這邊會有同步呀?並非這樣,這裏所說的鎖消除並不必定指代是你寫的代碼的鎖消除,我打一個比方:

  在jdk1.5之前,咱們的String字符串拼接操做其實底層是StringBuffer來實現的(這個你們能夠用我前面介紹的方法,寫一個簡單的demo,而後查看class文件中的字節碼指令就清楚了),而在jdk1.5以後,那麼是用StringBuilder來拼接的。咱們考慮前面的狀況,好比以下代碼:

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

  底層實現會變成這樣:

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

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

三、鎖粗化

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

四、輕量級鎖

五、偏向鎖

 

 

 

 原文參考【Java知音網站

相關文章
相關標籤/搜索