Java併發學習之synchronized使用小結

synchronized工做原理及使用小結

爲確保共享變量不會出現併發問題,一般會對修改共享變量的代碼塊用synchronized加鎖,確保同一時刻只有一個線程在修改共享變量,從而避免併發問題java

本篇將集中在synchronized關鍵字的工組原理以及使用方式上安全

I. 工做原理

以一個case進行分析,源碼以下多線程

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

反編譯

在加鎖的代碼塊, 多了一個 monitorenter , monitorexit併發

每一個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的全部權,過程以下:性能

  1. 若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者。
  2. 若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1.
  3. 若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權

執行monitorexit的線程必須是objectref所對應的monitor的全部者。測試

  1. 指令執行時,monitor的進入數減1
  2. 若是減1後進入數爲0,那線程退出monitor,再也不是這個monitor的全部者
  3. 其餘被這個monitor阻塞的線程能夠嘗試去獲取這個 monitor 的全部權

this

談到 synchronized 就不可避免的要說到鎖這個東西,基本上在網上能夠搜索到一大批的關於偏向鎖,輕量鎖,重量鎖的講解文檔,對這個東西基本上我也不太理解,多看幾篇博文以後,簡單的記錄一下操作系統

先拋一個結論: 輕量級鎖是爲了在線程交替執行同步塊時提升性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提升性能線程

1. 偏向鎖

獲取過程code

  • 判斷是否爲可偏向狀態
  • 是,則判斷線程ID是否指向當前線程
    • 是,即表示這個偏向鎖就是這個線程持有, 直接執行代碼塊
    • 否,經過CAS操做競爭鎖
      • 競爭成功, 則設置線程ID爲當前線程, 並執行代碼塊;
      • 競爭失敗,說明多線程競爭啦,問題嚴重了,當偏向鎖到達安全點時,將偏向鎖升級爲輕量鎖

釋放過程

  • 當偏向鎖遇到其餘線程嘗試競爭時,持有偏向鎖的線程會釋放,並升級爲輕量鎖
  • 到達安全點, 暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖的狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲「01」)或輕量級鎖(標誌位爲「00」)的狀態。

2. 輕量鎖

「輕量級」是相對於使用操做系統互斥量來實現的傳統鎖而言的。

可是,首先須要強調一點的是,輕量級鎖並非用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用產生的性能消耗。

在解釋輕量級鎖的執行過程以前,先明白一點,輕量級鎖所適應的場景是線程交替執行同步塊的狀況,若是存在同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖

輕量鎖

3. 轉換

簡單來說,單線程時,使用偏向鎖,若是這個時候,又來了一個線程訪問這個代碼塊,那麼就要升級爲輕量鎖,若是這個線程在訪問代碼塊同時,又來了一個線程來訪問這個代碼塊,那麼就要升級爲重量鎖了。下面更多的顯示了這些變更時,標記位的隨之改變

轉換


II. 三中使用姿式

1. 三種方法說明

  1. 修飾實例方法

    多個線程訪問同一個實例的加鎖方法時,會出現鎖的競爭

  2. 修飾靜態方法

    多個線程訪問類的加鎖方法時,會出現鎖的競爭

  3. 修飾代碼塊

    多線程訪問到同一個代碼塊時,會出現競爭的問題

2. 幾個疑問

一個case: TestDemo方法定義以下

public class TestDemo {
  public synchronized void a() { 
    // ...
  }
  public synchronized void b() { 
    // ...
  }
  public static synchronized void c() { 
    // ...
  }
  public static synchronized void d() { 
    // ...
  }
  public void e() {
  // ...
  }
  
  public void f() {
    synchronized(this) {
    // ....
    }
  }
  
  public void g() {
    synchronized(this) {
    // ....
    }
  }
}
  1. 線程1訪問a方法時,線程2訪問a方法會被阻塞;若此時線程2訪問b方法會被阻塞麼?訪問c,d, e方法呢?
  2. 線程1訪問c方法時,線程2訪問c方法會被阻塞,若此時線程2訪問d方法會被阻塞麼,訪問a,b,e方法呢?
  3. 線程1進入f方法內部的同步代碼塊,此時線程2若訪問f會被阻塞;那麼線程2訪問g方法會如何?訪問a,b,c,d,e方法又是怎樣?

對上面的問題,核心的一點就是synchronized是否只做用於修飾的代碼塊or方法上

3. 測試驗證

TestDemo的具體實現以下

public class TestDemo {

    public synchronized void a(String msg) {
        System.out.println(Thread.currentThread().getName() + ":a() before");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":a() after: " + msg);
    }


    public synchronized void b(String msg) {
        System.out.println(Thread.currentThread().getName() + ":b() before");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":b() after: " + msg);
    }

    public static synchronized void c(String msg) {
        System.out.println(Thread.currentThread().getName() + ":c() before");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":c() after: " + msg);
    }


    public static synchronized void d(String msg) {
        System.out.println(Thread.currentThread().getName() + ":d() before");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":d() after: " + msg);
    }


    public void e(String msg) {
        System.out.println(Thread.currentThread().getName() + ":e() before");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":e() after: " + msg);
    }


    public void f(String msg) {
        System.out.println(Thread.currentThread().getName() + ":f() before");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":f() after: " + msg);
    }


    public void g(String msg) {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + ":a() before");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":a() after: " + msg);
        }
    }


    public void h(String msg) {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + ":h() before");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":h() after: " + msg);
        }
    }
}

測試一 實例加鎖方法的訪問測試

/**
 * 非靜態同步方法測試
 */
private void nonStaticSynFun() throws InterruptedException {

    TestDemo testDemo = new TestDemo();

    Thread thread1 = new Thread(()->testDemo.a("訪問同一加鎖方法"), "thread1");
    Thread thread2 = new Thread(()->testDemo.a("訪問同一加鎖方法"), "thread2");

    System.out.println("---兩個線程,訪問同一個加鎖方法開始---");
    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();
    System.out.println("---兩個線程,訪問同一個加鎖方法結束---\n");

  
    // 

    TestDemo testDemo2 = new TestDemo();
    thread1 = new Thread(()->testDemo.a("訪問第一個實例同一加鎖方法"), "thread1");
    thread2 = new Thread(()->testDemo2.a("訪問第二個實例同一加鎖方法"), "thread2");

    System.out.println("---兩個線程,訪問兩個實例同一個加鎖方法開始---");
    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();
    System.out.println("---兩個線程,訪問兩個同一個加鎖方法結束---\n");

    //

    thread1 = new Thread(()->testDemo.a("訪問兩個加鎖方法"), "thread1");
    thread2 = new Thread(()->testDemo.b("訪問兩個加鎖方法"), "thread2");
    System.out.println("---兩個線程,訪問兩個加鎖方法開始---");
    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();
    System.out.println("---兩個線程,訪問兩個加鎖方法結束---\n");


    //
    thread1 = new Thread(()->testDemo.a("訪問加鎖實例方法"), "thread1");
    thread2 = new Thread(()->TestDemo.c("訪問加鎖靜態方法"), "thread2");
    System.out.println("---兩個線程,訪問實例和靜態加鎖方法開始---");
    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();
    System.out.println("---兩個線程,訪問實例和靜態加鎖方法結束---\n");

}

@Test
public void testNoStaticSynFun() throws InterruptedException {
    for(int i = 0; i < 2000; i++) {
        nonStaticSynFun();
    }
}

上面的測試case主要覆蓋:

  1. 兩個線程訪問一個實例的同一個加鎖方法(指望阻塞,順序執行)
  2. 線程1訪問實例1的加鎖方法,線程2訪問實例2的加鎖方法(無阻塞,併發執行)
  3. 兩個線程訪問一個實例的兩個加鎖方法
  4. 兩個線程,一個訪問實例的加鎖方法,一個訪問靜態加鎖方法

輸出結果以下

---兩個線程,訪問同一個加鎖方法開始---
thread1:a() before
thread1:a() after: 訪問同一加鎖方法
thread2:a() before
thread2:a() after: 訪問同一加鎖方法
---兩個線程,訪問同一個加鎖方法結束---

---兩個線程,訪問兩個實例同一個加鎖方法開始---
thread1:a() before
thread2:a() before
thread2:a() after: 訪問第二個實例同一加鎖方法
thread1:a() after: 訪問第一個實例同一加鎖方法
---兩個線程,訪問兩個同一個加鎖方法結束---

---兩個線程,訪問兩個加鎖方法開始---
thread1:a() before
thread1:a() after: 訪問兩個加鎖方法
thread2:b() before
thread2:b() after: 訪問兩個加鎖方法
---兩個線程,訪問兩個加鎖方法結束---

---兩個線程,訪問實例和靜態加鎖方法開始---
thread1:a() before
thread2:c() before
thread2:c() after: 訪問加鎖靜態方法
thread1:a() after: 訪問加鎖實例方法
---兩個線程,訪問實例和靜態加鎖方法結束---

驗證結果:

  1. 同一個實例中加鎖的方法,只要有一個線程已經獲取到了鎖,其餘線程再去訪問時,都會被阻塞(即此時的鎖,是一個實例共享同一把鎖;不一樣的實例,鎖不一樣)
  2. 一個線程獲取到一個實例中的加鎖方法的鎖時,另外一個線程依然能夠訪問靜態加鎖方法(即實例的鎖與靜態方法的鎖是不一樣的,二者不影響)

測試case二: 靜態加鎖方法測試

private void staticSynFun() throws InterruptedException {
    Thread thread1 = new Thread(() -> TestDemo.c("訪問加鎖靜態方法"), "thread1");
    Thread thread2 = new Thread(() -> TestDemo.c("訪問加鎖靜態方法"), "thread2");

    System.out.println("---兩個線程,訪問靜態加鎖方法開始---");
    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();
    System.out.println("---兩個線程,訪問靜態加鎖方法結束---\n");


    //

    TestDemo testDemo1 = new TestDemo(), testDemo2 = new TestDemo();
    thread1 = new Thread(() -> testDemo1.c("訪問加鎖靜態方法"), "thread1");
    thread2 = new Thread(() -> testDemo2.d("訪問加鎖靜態方法"), "thread2");
    Thread thread3 = new Thread(() -> testDemo1.a("訪問加鎖實例方法"), "thread3");

    System.out.println("---兩個線程,訪問不一樣實例的靜態加鎖方法開始---");
    thread1.start();
    thread2.start();
    thread3.start();

    thread1.join();
    thread2.join();
    thread3.join();
    System.out.println("---兩個線程,訪問不一樣實例的靜態加鎖方法結束---\n");
}

@Test
public void testStaticSynFunc() throws InterruptedException {
    for (int i = 0; i < 2000; i++) {
        staticSynFun();
    }
}

上面的測試主要覆蓋

  • 兩個線程訪問相同的靜態加鎖方法(期待阻塞)
  • 三個線程,兩個訪問不一樣實例的靜態加鎖方法,一個訪問實例加鎖方法

輸出結果以下

---兩個線程,訪問靜態加鎖方法開始---
thread1:c() before
thread1:c() after: 訪問加鎖靜態方法
thread2:c() before
thread2:c() after: 訪問加鎖靜態方法
---兩個線程,訪問靜態加鎖方法結束---

---兩個線程,訪問不一樣實例的靜態加鎖方法開始---
thread1:c() before
thread3:a() before
thread1:c() after: 訪問加鎖靜態方法
thread2:d() before
thread3:a() after: 訪問加鎖實例方法
thread2:d() after: 訪問加鎖靜態方法
---兩個線程,訪問不一樣實例的靜態加鎖方法結束---

驗證結果:

  • 不一樣的線程訪問靜態同步方法時,會阻塞(即靜態同步方法,共享一把鎖;全部的實例訪問靜態同步方法依然是共享這把鎖)
  • 靜態同步方法的鎖和實例同步方法的鎖不一樣,二者沒有關係,不會相互影響

測試case三: 同步代碼塊

基本上和上面的相同,同步代碼塊分爲靜態同步代碼塊(共享類鎖);非靜態同步代碼塊(共享實例鎖)

III. 小結

  1. synchronized 三中使用姿式,修飾靜態方法,實例方法,(靜態/非靜態)代碼塊
  2. 靜態同步方法,靜態同步代碼塊共享同一把鎖(簡易稱爲類鎖),全部這些同步代碼的訪問,都會去競爭類鎖,從而出現阻塞
  3. 一個實例中的同步方法,非靜態同步代碼塊共享一把鎖(簡易成爲實例鎖),全部訪問同一個實例中的這些同步代碼時,都會競爭實例鎖,從而出現阻塞
  4. 不一樣的實例擁有不一樣的實例鎖,彼此相互沒有影響
  5. 實例鎖和類鎖沒有影響,不會形成彼此阻塞
  6. synchronized底層主要是經過偏向鎖,輕量級鎖和重量級鎖組合來實現線程同步的功能
  7. 幾個鎖的簡要說明爲:單線程時,使用偏向鎖,若是這個時候,又來了一個線程訪問同步代碼塊,那麼就要升級爲輕量鎖,若是這個線程在訪問代碼塊同時,又來了一個線程來訪問這個代碼塊,那麼就要升級爲重量鎖了

掃描關注,java分享

QrCodeGG

相關文章
相關標籤/搜索