一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|JAVA教程|ASP教程|

服務器之家 - 編程語言 - JAVA教程 - 詳解Java中synchronized關鍵字的死鎖和內存占用問題

詳解Java中synchronized關鍵字的死鎖和內存占用問題

2020-05-17 12:30可文分身 JAVA教程

Java的synchronized關鍵字用來進行線程同步操作,然而這在使用中經常會遇到一些問題,這里我們就來詳解Java中synchronized關鍵字的死鎖和內存占用問題:

先看一段synchronized 的詳解:
synchronized 是 java語言的關鍵字,當它用來修飾一個方法或者一個代碼塊的時候,能夠保證在同一時刻最多只有一個線程執行該段代碼。

一、當兩個并發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以后才能執行該代碼塊。

二、然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。

三、尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。

四、第三個例子同樣適用其它同步代碼塊。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。

五、以上規則對其它對象鎖同樣適用.
簡單來說, synchronized就是為當前的線程聲明一個鎖, 擁有這個鎖的線程可以執行區塊里面的指令, 其他的線程只能等待獲取鎖, 然后才能相同的操作.
這個很好用, 但是筆者遇到另一種比較奇葩的情況.
1. 在同一類中, 有兩個方法是用了synchronized關鍵字聲明
2. 在執行完其中一個方法的時候, 需要等待另一個方法(異步線程回調)也執行完, 所以用了一個countDownLatch來做等待
3. 代碼解構如下:

?
1
2
3
4
5
6
7
8
9
synchronized void a(){
 countDownLatch = new CountDownLatch(1);
 // do someing
 countDownLatch.await();
}
 
synchronized void b(){
   countDownLatch.countDown();
}

其中
a方法由主線程執行, b方法由異步線程執行后回調
執行結果是:
主線程執行 a方法后開始卡住, 不再往下做, 任你等多久都沒用.
這是一個很經典的死鎖問題
a等待b執行, 其實不要看b是回調的, b也在等待a執行. 為什么呢? synchronized 起了作用.
一般來說, 我們要synchronized一段代碼塊的時候, 我們需要使用一個共享變量來鎖住, 比如:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
byte[] mutex = new byte[0];
 
void a1(){
   synchronized(mutex){
     //dosomething
   }
}
 
void b1(){
 
   synchronized(mutex){
     // dosomething
   }
 
}

如果把a方法和b方法的內容分別遷移到 a1和b1 方法的synchronized塊里面, 就很好理解了.
a1執行完后會間接等待(countDownLatch)b1方法執行.
然而由于 a1 中的mutex并沒有釋放, 就開始等待b1了, 這時候, 即使是異步的回調b1方法, 由于需要等待mutex釋放鎖, 所以b方法并不會執行.
于是就引起了死鎖!
而這里的synchronized關鍵字放在方法前面, 起的作用就是一樣的. 只是java語言幫你隱去了mutex的聲明和使用而已. 同一個對象中的synchronized 方法用到的mutex是相同的, 所以即使是異步回調, 也會引起死鎖, 所以要注意這個問題. 這種級別的錯誤是屬于synchronized關鍵字使用不當. 不要亂用, 而且要用對.
那么這樣的 隱形的mutex 對象究竟是 什么呢?
很容易想到的就是 實例本身. 因為這樣就不用去定義新的對象了做鎖了. 為了證明這個設想, 可以寫一段程序來證明.
思路很簡單, 定義一個類, 有兩個方法, 一個方法聲明為 synchronized, 一個在 方法體里面使用synchronized(this), 然后啟動兩個線程, 來分別調用這兩個方法, 如果兩個方法之間發生鎖競爭(等待)的話, 就可以說明 方法聲明的 synchronized 中的隱形的mutex其實就是 實例本身了.

?
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
40
41
42
43
44
45
46
47
public class MultiThreadSync {
 
  public synchronized void m1() throws InterruptedException{
     System. out.println("m1 call" );
     Thread. sleep(2000);
     System. out.println("m1 call done" );
  }
 
  public void m2() throws InterruptedException{
     synchronized (this ) {
       System. out.println("m2 call" );
       Thread. sleep(2000);
       System. out.println("m2 call done" );
     }
  }
 
  public static void main(String[] args) {
     final MultiThreadSync thisObj = new MultiThreadSync();
 
     Thread t1 = new Thread(){
       @Override
       public void run() {
         try {
           thisObj.m1();
         } catch (InterruptedException e) {
           e.printStackTrace();
         }
       }
     };
 
     Thread t2 = new Thread(){
       @Override
       public void run() {
         try {
           thisObj.m2();
         } catch (InterruptedException e) {
           e.printStackTrace();
         }
       }
     };
 
     t1.start();
     t2.start();
 
  }
 
}

結果輸出是:

?
1
2
3
4
m1 call
m1 call done
m2 call
m2 call done

說明方法m2的sync塊等待了m1的執行. 這樣就可以證實 上面的設想了.
另外需要說明的是, 當sync加在 static的方法上的時候, 由于是類級別的方法, 所以鎖住的對象是當前類的class實例. 同樣也可以寫程序進行證明.這里略.
所以方法的synchronized 關鍵字, 在閱讀的時候可以自動替換為synchronized(this){}就很好理解了.

?
1
2
3
4
5
                    void method(){
void synchronized method(){         synchronized(this){
   // biz code                // biz code
}               ------>>>   }
                    }

由Synchronized的內存可見性說開去
在Java中,我們都知道關鍵字synchronized可以用于實現線程間的互斥,但我們卻常常忘記了它還有另外一個作用,那就是確保變量在內存的可見性 - 即當讀寫兩個線程同時訪問同一個變量時,synchronized用于確保寫線程更新變量后,讀線程再訪問該 變量時可以讀取到該變量最新的值。

比如說下面的例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class NoVisibility {
  private static boolean ready = false;
  private static int number = 0;
 
  private static class ReaderThread extends Thread {
    @Override
    public void run() {
      while (!ready) {
        Thread.yield(); //交出CPU讓其它線程工作
      }
      System.out.println(number);
    }
  }
 
  public static void main(String[] args) {
    new ReaderThread().start();
    number = 42;
    ready = true;
  }
}

你認為讀線程會輸出什么? 42? 在正常情況下是會輸出42. 但是由于重排序問題,讀線程還有可能會輸出0 或者什么都不輸出。

我們知道,編譯器在將Java代碼編譯成字節碼的時候可能會對代碼進行重排序,而CPU在執行機器指令的時候也可能會對其指令進行重排序,只要重排序不會破壞程序的語義 -

在單一線程中,只要重排序不會影響到程序的執行結果,那么就不能保證其中的操作一定按照程序寫定的順序執行,即使重排序可能會對其它線程產生明顯的影響。
這也就是說,語句"ready=true"的執行有可能要優先于語句"number=42"的執行,這種情況下,讀線程就有可能會輸出number的默認值0.

而在Java內存模型下,重排序問題是會導致這樣的內存的可見性問題的。在Java內存模型下,每個線程都有它自己的工作內存(主要是CPU的cache或寄存器),它對變量的操作都在自己的工作內存中進行,而線程之間的通信則是通過主存和線程的工作內存之間的同步來實現的。

比如說,對于上面的例子而言,寫線程已經成功的將number更新為42,ready更新為true了,但是很有可能寫線程只同步了number到主存中(可能是由于CPU的寫緩沖導致),導致后續的讀線程讀取的ready值一直為false,那么上面的代碼就不會輸出任何數值。

詳解Java中synchronized關鍵字的死鎖和內存占用問題

而如果我們使用了synchronized關鍵字來進行同步,則不會存在這樣的問題,

?
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
public class NoVisibility {
  private static boolean ready = false;
  private static int number = 0;
  private static Object lock = new Object();
 
  private static class ReaderThread extends Thread {
    @Override
    public void run() {
      synchronized (lock) {
        while (!ready) {
          Thread.yield();
        }
        System.out.println(number);
      }
    }
  }
 
  public static void main(String[] args) {
    synchronized (lock) {
      new ReaderThread().start();
      number = 42;
      ready = true;
    }
  }
}

這個是因為Java內存模型對synchronized語義做了以下的保證,

詳解Java中synchronized關鍵字的死鎖和內存占用問題

即當ThreadA釋放鎖M時,它所寫過的變量(比如,x和y,存在它工作內存中的)都會同步到主存中,而當ThreadB在申請同一個鎖M時,ThreadB的工作內存會被設置為無效,然后ThreadB會重新從主存中加載它要訪問的變量到它的工作內存中(這時x=1,y=1,是ThreadA中修改過的最新的值)。通過這樣的方式來實現ThreadA到ThreadB的線程間的通信。

這實際上是JSR133定義的其中一條happen-before規則。JSR133給Java內存模型定義以下一組happen-before規則,

  • 單線程規則:同一個線程中的每個操作都happens-before于出現在其后的任何一個操作。
  • 對一個監視器的解鎖操作happens-before于每一個后續對同一個監視器的加鎖操作。
  • 對volatile字段的寫入操作happens-before于每一個后續的對同一個volatile字段的讀操作。
  • Thread.start()的調用操作會happens-before于啟動線程里面的操作。
  • 一個線程中的所有操作都happens-before于其他線程成功返回在該線程上的join()調用后的所有操作。
  • 一個對象構造函數的結束操作happens-before與該對象的finalizer的開始操作。
  • 傳遞性規則:如果A操作happens-before于B操作,而B操作happens-before與C操作,那么A動作happens-before于C操作。

實際上這組happens-before規則定義了操作之間的內存可見性,如果A操作happens-before B操作,那么A操作的執行結果(比如對變量的寫入)必定在執行B操作時可見。

為了更加深入的了解這些happens-before規則,我們來看一個例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//線程A,B共同訪問的代碼
Object lock = new Object();
int a=0;
int b=0;
int c=0;
 
//線程A,調用如下代碼
synchronized(lock){
  a=1; //1
  b=2; //2
} //3
c=3; //4
 
 
//線程B,調用如下代碼
synchronized(lock){ //5
  System.out.println(a); //6
  System.out.println(b); //7
  System.out.println(c); //8
}

我們假設線程A先運行,分別給a,b,c三個變量進行賦值(注:變量a,b的賦值是在同步語句塊中進行的),然后線程B再運行,分別讀取出這三個變量的值并打印出來。那么線程B打印出來的變量a,b,c的值分別是多少?

根據單線程規則,在A線程的執行中,我們可以得出1操作happens before于2操作,2操作happens before于3操作,3操作happens before于4操作。同理,在B線程的執行中,5操作happens before于6操作,6操作happens before于7操作,7操作happens before于8操作。而根據監視器的解鎖和加鎖原則,3操作(解鎖操作)是happens before 5操作的(加鎖操作),再根據傳遞性 規則我們可以得出,操作1,2是happens before 操作6,7,8的。

則根據happens-before的內存語義,操作1,2的執行結果對于操作6,7,8是可見的,那么線程B里,打印的a,b肯定是1和2. 而對于變量c的操作4,和操作8. 我們并不能根據現有的happens before規則推出操作4 happens before于操作8. 所以在線程B中,訪問的到c變量有可能還是0,而不是3.

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 乳女教师欲乱动漫无修版动画3d | 久久福利影院 | 国产一区日韩二区欧美三区 | 美女班主任让我爽了一夜视频 | 国产大秀视频 | 国产一区二区三区福利 | 国产成人啪精品午夜在线观看 | 波多野结衣亚洲一区 | 国产高清自拍 | 蜜桃影像传媒推广 | 精品一区二区三区中文 | 国产精亚洲视频 | 456成人免费高清视频 | 日韩精品亚洲专区在线影视 | 日本免费一区二区三区四区五六区 | 天美蜜桃精东乌鸦传媒 | 玩高中女同桌肉色短丝袜脚文 | 精品久久香蕉国产线看观看亚洲 | 男人边吃奶边做好爽视频免费 | 91短视频社区在线观看 | 国产一及毛片 | 欧美一区二区三区成人看不卡 | 欧美成人一区二区三区 | 成人免费淫片95视频观看网站 | 娇妻在床上迎合男人 | 视频在线观看高清免费 | 亚洲成在人线久久综合 | 9久热这里只有精品视频在线观看 | 四虎精品成人免费观看 | 男人天堂亚洲 | 热穴高校| 午夜宅男在线观看 | 免费看黄色片网站 | 日本在线观看免费高清 | 美女张开腿让我了一夜 | 青草视频在线观看免费网站 | 青草视频在线观看免费网站 | 免费二区 | 久久99精品涩AV毛片观看 | 性夜a爽黄爽 | 99热99在线 |