1. 程式人生 > >鎖 -synchronized、Lock解析

鎖 -synchronized、Lock解析

鎖主要解決執行緒安全問題。而執行緒安全問題,即多個執行緒同時訪問一個資源時,會導致程式執行結果並不是想看到的結果。

synchronized

先來了解一下互斥鎖,顧名思義:能到達到互斥訪問目的的鎖。

如果對臨界資源加上互斥鎖,當一個執行緒在訪問該臨界資源時,其他執行緒便只能等待。

  在Java中,每一個物件都擁有一個鎖標記(monitor),也稱為監視器,多執行緒同時訪問某個物件時,執行緒只有獲取了該物件的鎖才能訪問。

  在Java中,可以使用synchronized關鍵字來標記一個方法或者程式碼塊,當某個執行緒呼叫該物件的synchronized方法或者訪問synchronized程式碼塊時,這個執行緒便獲得了該物件的鎖,其他執行緒暫時無法訪問這個方法,只有等待這個方法執行完畢或者程式碼塊執行完畢,這個執行緒才會釋放該物件的鎖,其他執行緒才能執行這個方法或者程式碼塊。

Synchronized使用

synchronized使用方式:

  • 同步方法;
  • 同步塊。

同步方法我們以StringBuffer原始碼為例(StringBuffer是現成安全的):

    public synchronized StringBuffer append(String str) {
        super.append(str);
        return this;
    }
  • 1
  • 2
  • 3
  • 4

同步塊

synchronized(synObject) {
       // 允許訪問控制的程式碼   
    }
  • 1
  • 2
  • 3

synchronized 塊是這樣一個程式碼塊,其中的程式碼必須獲得物件 syncObject 。由於可以針對任意程式碼塊,且可任意指定上鎖的物件,故靈活性較高。

synchronized (this) {
            for(int i=0;i<100;i++){
                System.out.println(thread.getName()+"在插入資料"+i);
                arrayList.add(i);
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

對synchronized (this)的一些理解

  1. 當兩個併發執行緒訪問同一個物件object中的這個synchronized(this)同步程式碼塊時,一個時間內只能有一個執行緒得到執行。另一個執行緒必須等待當前執行緒執行完這個程式碼塊以後才能執行該程式碼塊。
  2. 當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,其他執行緒對object中所有其它synchronized(this)同步程式碼塊的訪問將被阻塞。
  3. 當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,另一個執行緒仍然可以訪問該object中的除synchronized(this)同步程式碼塊以外的部分。
  4. 當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,它就獲得了這個object的物件鎖。結果,其它執行緒對該object物件所有同步程式碼部分的訪問都被暫時阻塞。 
    我理解的是當一個執行緒獲取了object鎖,其他執行緒就不能過去object鎖,如果想獲取只能阻塞等待,但可以訪問此object中其他沒有被鎖定的方法。而如果是同步方法的話,一個執行緒執行一個物件的非static synchronized方法,另外一個執行緒需要執行這個物件所屬類的static synchronized方法,此時不會發生互斥現象,因為訪問static synchronized方法佔用的是類鎖,而訪問非static synchronized方法佔用的是物件鎖,所以不存在互斥現象。 看個例子就明白了:

同步方法例項程式碼

public class Test {

    public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
        new Thread(){
            @Override
            public void run() {
                insertData.insert();
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                insertData.insert1();
            }
        }.start();
    }
}

class InsertData {
    public synchronized void insert(){
        System.out.println("執行insert");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行insert完畢");
    }

    public synchronized static void insert1() {
        System.out.println("執行insert1");
        System.out.println("執行insert1完畢");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

結果是 
執行insert 
執行insert1 
執行insert1完畢 
執行insert完畢 
兩個方法互不影響。但要注意如果是在public synchronized void insert()新增一個static結果就不一樣了,及程式碼

public class Test {

    public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
        new Thread(){
            @Override
            public void run() {
                insertData.insert();
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                insertData.insert1();
            }
        }.start();
    }
}

class InsertData {
// ------------ 這裡添加了static修復 ------------
    public synchronized static void insert(){
        System.out.println("執行insert");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行insert完畢");
    }

    public synchronized static void insert1() {
        System.out.println("執行insert1");
        System.out.println("執行insert1完畢");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

記過就是: 
執行insert 
執行insert完畢 
執行insert1 
執行insert1完畢 
也就是說:

  • 某個物件例項內,synchronized aMethod(){} 可以防止多個執行緒同時 訪問這個物件的synchronized方法,這時,不同的物件例項的 synchronized方法是不相干擾的。也就是說,其它執行緒照樣可以同時訪問相同類的另一個物件例項中的synchronized方法;
  • 是某個類的範圍,synchronized static aStaticMethod{} 防止多個執行緒同時訪問這個類中的synchronized static 方法 。它可以對類的所有物件例項起作用。

來看看程式碼塊

public class Test1 {

    public static void main(String[] args)  {
        final InsertData1 insertData = new InsertData1();
        new Thread(){
            @Override
            public void run() {
                insertData.insert();
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                insertData.insert1();
            }
        }.start();
    }
}

class InsertData1 {
    public void insert(){
        synchronized(this){
            System.out.println("執行insert");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("執行insert完畢");
        }
    }

    public void insert1() {
        synchronized(this){
            System.out.println("執行insert1");
            System.out.println("執行insert1完畢");
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

執行結果: 
執行insert 
執行insert完畢 
執行insert1 
執行insert1完畢 
這就證實了上面總結的。

synchronized底層

可以大致瞭解一下synchronized的底層,通過反編譯看一下。

public class InsertData {
    private Object object = new Object();

    public void insert(Thread thread){
        synchronized (object) {

        }
    }

    public synchronized void insert1(Thread thread){

    }

    public void insert2(Thread thread){

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

這裡寫圖片描述

從反編譯獲得的位元組碼可以看出,synchronized程式碼塊實際上多了monitorenter和monitorexit兩條指令。monitorenter指令執行時會讓物件的鎖計數加1,而monitorexit指令執行時會讓物件的鎖計數減1,其實這個與作業系統裡面的PV操作很像,作業系統裡面的PV操作就是用來控制多個執行緒對臨界資源的訪問。對於synchronized方法,執行中的執行緒識別該方法的 method_info 結構是否有 ACC_SYNCHRONIZED 標記設定,然後它自動獲取物件的鎖,呼叫方法,最後釋放鎖。如果有異常發生,執行緒自動釋放鎖。

還有兩點說明:

  1. 對於synchronized方法或者synchronized程式碼塊,當出現異常時,JVM會自動釋放當前執行緒佔用的鎖,因此不會由於異常導致出現死鎖現象;
  2. synchronized關鍵字是不能繼承的,也就是說,基類的方法synchronized f(){} 在繼承類中並不自動是synchronized f(){},而是變成了f(){}。繼承類需要你顯式的指定它的某個方法為synchronized方法。

Lock

先來說說synchronized缺陷和lock與它的不同 
缺陷

  1. 獲取鎖的執行緒執行完了該程式碼塊,然後執行緒釋放對鎖的佔有;
  2. 執行緒執行發生異常,此時JVM會讓執行緒自動釋放鎖。

那麼如果這個獲取鎖的執行緒由於要等待IO或者其他原因(比如呼叫sleep方法)被阻塞了,但是又沒有釋放鎖,其他執行緒便只能乾巴巴地等待,試想一下,這多麼影響程式執行效率。

不同

  1. Lock不是Java語言內建的,synchronized是Java語言的關鍵字,因此是內建特性。Lock是一個類,通過這個類可以實現同步訪問;
  2. Lock和synchronized有一點非常大的不同,採用synchronized不需要使用者去手動釋放鎖,當synchronized方法或者synchronized程式碼塊執行完之後,系統會自動讓執行緒釋放對鎖的佔用;而Lock則必須要使用者去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象;
  3. 除此之外還多了一下如下知識點: 
    • 等待可中斷,在持有鎖的執行緒長時間不釋放鎖的時候,等待的執行緒可以選擇放棄等待. tryLock(long timeout, TimeUnit unit);
    • 公平鎖,按照申請鎖的順序來一次獲得鎖稱為公平鎖.synchronized的是非公平鎖,ReentrantLock可以通過建構函式實現公平鎖. new RenentrantLock(boolean fair);
    • 繫結多個Condition(這個在堵塞佇列中用到),通過多次newCondition可以獲得多個Condition物件,可以簡單的實現比較複雜的執行緒同步的功能.通過await(),signal()。

ReentrantLock

Lock是一個介面,這裡重點介紹ReentrantLock。ReentrantLock,意思是“可重入鎖”,ReentrantLock是唯一實現了Lock介面的類,並且ReentrantLock提供了更多的方法。

可重入鎖

在學習ReentrantLock之前我們先來了解一下可重入鎖。 
在JAVA環境下 ReentrantLock 和synchronized 都是 可重入鎖

可重入鎖,也叫做遞迴鎖,指的是同一執行緒 外層函式獲得鎖之後 ,內層遞迴函式仍然有獲取該鎖的程式碼,但不受影響。簡單的說:當一個執行緒請求得到一個物件鎖後再次請求此物件鎖,可以再次得到該物件鎖,就是說在一個synchronized方法/塊或ReentrantLock的內部呼叫本類的其他synchronized方法/塊或ReentrantLock時,是永遠可以拿到鎖。

看程式碼:

public class ReentrantLockTest implements Runnable {
    ReentrantLock lock = new ReentrantLock();

    public void get() {
        lock.lock();
        System.out.println(Thread.currentThread().getId());
        set();
        lock.unlock();
    }

    public void set() {
        lock.lock();
        System.out.println(Thread.currentThread().getId());
        lock.unlock();
    }

    @Override
    public void run() {
        get();
    }

    public static void main(String[] args) {
        ReentrantLockTest ss = new ReentrantLockTest();
        new Thread(ss).start();
        new Thread(ss).start();
        new Thread(ss).start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

結果是: 
11 
11 
12 
12 
13 
13 
(可以每次執行的結構不太一樣)但可以看到同一個執行緒id被連續輸出兩次。

ReentrantLock的正確使用

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    public static void main(String[] args)  {
        final Test test = new Test();

        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();

        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  

    public void insert(Thread thread) {
        Lock lock = new ReentrantLock();    //注意這個地方
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了鎖");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

結果: 
Thread-0得到了鎖 
Thread-1得到了鎖 
Thread-0釋放了鎖 
Thread-1釋放了鎖 
為什麼會是這樣呢?原因在於,在insert方法中的lock變數是區域性變數,每個執行緒執行該方法時都會儲存一個副本,那麼理所當然每個執行緒執行到lock.lock()處獲取的是不同的鎖,所以就不會發生衝突。

正確是使用方式是,只需要將lock宣告為類的屬性即可:

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意這個地方
    public static void main(String[] args)  {
        final Test test = new Test();

        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();

        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  

    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了鎖");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

ReentrantLock可重入鎖的使用場景

場景1:如果發現該操作已經在執行中則不再執行(有狀態執行)

private ReentrantLock lock = new ReentrantLock();
    if (lock.tryLock()) {  //如果已經被lock,則立即返回false不會等待,達到忽略操作的效果 
         try {

            //操作

         } finally {
             lock.unlock();
         }

     }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

場景2:如果發現該操作已經在執行,等待一個一個執行(同步執行,類似synchronized)

private ReentrantLock lock = new ReentrantLock(); //引數預設false,不公平鎖
// private ReentrantLock lock = new ReentrantLock(true); //公平鎖
    try {
        lock.lock(); //如果被其它資源鎖定,會在此等待鎖釋放,達到暫停的效果

       //操作

    } finally {
        lock.unlock();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

不公平鎖與公平鎖的區別

公平情況下,操作會排一個隊按順序執行,來保證執行順序。(會消耗更多的時間來排隊) 
不公平情況下,是無序狀態允許插隊,jvm會自動計算如何處理更快速來排程插隊。(如果不關心順序,這個速度會更快)

場景3:如果發現該操作已經在執行,則嘗試等待一段時間,等待超時則不執行(嘗試等待執行)

try {
     if (lock.tryLock(5, TimeUnit.SECONDS)) {  //如果已經被lock,嘗試等待5s,看是否可以獲得鎖,如果5s後仍然無法獲得鎖則返回false繼續執行
        try {
            //操作
        } finally {
            lock.unlock();
        }
      }
  } catch (InterruptedException e) {
      e.printStackTrace(); //當前執行緒被中斷時(interrupt),會拋InterruptedException                 
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

場景4:如果發現該操作已經在執行,等待執行。這時可中斷正在進行的操作立刻釋放鎖繼續下一操作。 
這種情況主要用於取消某些操作對資源的佔用。如:(取消正在同步執行的操作,來防止不正常操作長時間佔用造成的阻塞),該操作的開銷也很大,一般不建議用。

try {
    lock.lockInterruptibly();
    //操作

} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    lock.unlock();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

下期會介紹volatile 和 ThreadPool