1. 程式人生 > >【轉】JAVA 並發性和多線程 -- 讀感 (二 線程間通訊,共享內存的機制)

【轉】JAVA 並發性和多線程 -- 讀感 (二 線程間通訊,共享內存的機制)

instance bar log 通信 java t 是的 復制代碼 ott bus

原文地址:https://www.cnblogs.com/edenpans/p/6020113.html

參考文章:http://ifeve.com/java-concurrency-thread-directory/

其中的競態,線程安全,內存模型,線程間的通信,java ThreadLocal類小節部分內容。

  • 1.目錄略覽
線程的基本概念:介紹線程的優點,代價,並發編程的模型。如何創建運行java 線程。 線程間通訊,共享內存的機制:競態條件與臨界區,線程安全和共享資源與不可變性。java內存模型,線程間的通信,java ThreadLocal類,線程信號 死鎖相關,資源競爭相關:死鎖,如何避免死鎖,饑餓和公平,嵌套管程鎖死,Slipped conditions(從一個線程檢查某一特定條件到該線程操作此條件期間,這個條件已經被其它線程改變,導致第一個線程在該條件上執行了錯誤的操作),鎖,讀鎖和寫鎖,重入寫死,信號量,阻塞隊列,線程池,CAS(compare and swap 理論),同步器,無阻塞算法,阿姆達爾定律(計算處理器平行運算之後效率提升的能力)。
  • 2.競態條件與臨界區
當多個線程訪問了相同的資源,並且對這些資源做了寫操作的時候,是不安全的。資源可以代表:同一內存區(變量,數組或者對象),系統(數據庫,web services)或文件。 對於一個簡單的加法操作 this.count = this.count + value,JVM執行指令的順序應該是: 從內存獲取 this.count 值放到寄存器 將寄存器的值添加value 將寄存器的值寫會內存 如果兩個線程 交叉執行,一個線程加2 一個線程加3,可能最後的結果不是5,而是2 或者3. 競態條件:兩個線程競爭同一個資源時,如果對資源訪問順序敏感,就存在競態條件。 臨界區:導致競態條件發生的代碼區成為臨界區。 在臨界區適當的同步可以避免競態條件。
  • 3.線程安全與共享資源
允許被多個線程同時執行的代碼稱為線程安全的代碼。線程安全的代碼不包含競態條件。 1.局部變量 1.1局部基本類型變量 是存儲在線程自己的棧中的,所以基礎類型的局部變量是線程安全的。 1.2.局部對象引用 引用所指向的對象沒有存儲到線程的棧內。所有的對象都在共享堆中。 兩段代碼,不管是基礎類型還是引用對象,它們都是局部變量,由於都沒有被其他線程獲取,是線程安全的。
public void someMethod(){
    long threadSafeInt = 0;
    threadSafeInt++;
}
技術分享圖片
public void someMethod(){
    LocalObject localObject = new LocalObject();
    localObject.callMethod();
    method2(localObject);
}

public void method2(LocalObject localObject){
    localObject.setValue("value");
}
技術分享圖片 2.對象成員 對象成員是存儲在堆上。如果兩個線程同時更新同一個對象的同一成員,這個代碼就是線程不安全的。
public class NotThreadSage{
    StringBuilder builder = New StringBuilder();
    public add(String text) {
        this.builder.append(text);
    }
}

線程控制逃逸判斷

一個資源的創建,使用銷毀都在同一個線程內完成,且永遠不會脫離該線程的控制。

即使對象本身線程安全,但是該對象中包含的其他的資源,也許整體的應用不是線程安全的。 3.線程安全及不可變性 immutable 和 read only 的差別:當一個變量是只讀的時候,變量的值不可改變,但是可以在其他變量發生改變的時候發生改變。而不變 是不會改變的。
  • 4.java 內存模型
java內存模型規範了java虛擬機與計算機內存如何協同工作的。 技術分享圖片

每個java虛擬機的線程都擁有自己的線程棧,包括了這個線程調用的方法當前執行點的相關信息。一個線程只能訪問自己的線程棧。本地變量只對當前線程可見。 技術分享圖片

對象是放在堆上。 每個線程都有自己的線程棧,如果是基本類型的變量,直接存放在線程棧中,如果是對象的引用,那麽引用地址會放在線程棧中,而對象會在堆中,這樣有可能存在兩個線程同時引用相同的對象。 技術分享圖片
public class MyRunnable implements Runnable() {

    public void run() {
        methodOne();
    }

    public void methodOne() {
        int localVariable1 = 45;

        MySharedObject localVariable2 =
            MySharedObject.sharedInstance;

        //... do more with local variables.

        methodTwo();
    }

    public void methodTwo() {
        Integer localVariable1 = new Integer(99);

        //... do more with local variable.
    }
}


public class MySharedObject {

    //static variable pointing to instance of MySharedObject

    public static final MySharedObject sharedInstance =
        new MySharedObject();


    //member variables pointing to two objects on the heap

    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);

    public long member1 = 12345;
    public long member1 = 67890;
}
技術分享圖片

技術分享圖片 兩個線程啟動後,Object3就是 MySharedObject,而Object2,Object4 是 MySharedObject中的 object2 和 Object4. 現代硬件內存架構 技術分享圖片

Java內存模型和硬件內存架構之間的橋接 技術分享圖片

硬件內存架構中沒有區分線程棧和堆。對於硬件所有線程棧和堆都是分布在主存中。部分線程棧和堆可能出現在CPU緩存和CPU內部的寄存器中。 當對象和變量被存放在計算機不同的內存區域中時,會有一些問題: 1.線程對共享變量修改的可見性。— 兩個線程分布運行在不同的CPU上時,線程的部分變量沒有刷新回主存,此時可能會導致不同步。可以使用 volatile 來避免。 2.當讀,寫和檢查共享變量時出現race conditions。多個線程同時修改共享內存的值,如下圖: 技術分享圖片

可以使用java同步塊,這樣同一時刻只能有一個線程可以進入代碼的臨界區。同步塊還可以保證代碼塊中所有被訪問的變量從主存中讀入,當線程退出同步塊時,所有被更新的變量也會被刷新回主存中,無論該變量是否被聲明為volatile. 5.java 同步塊 java同步塊 (synchronized block) 用來標記方法或者代碼塊是同步的。用來避免競爭。 java同步關鍵字:synchronized 所有其他等待進入該同步塊的線程將被阻塞,直到執行該同步塊的線程退出。 四種不同的同步塊: 實例方法;靜態方法;實例方法中的同步塊;靜態方法中的同步塊。——都是方法上的同步塊。 實例方法同步:
  public synchronized void add(int value){
      this.count += value;
  }
  每個實例其方法同步都是同步在不同的對象上。這樣每個實例方法同步都同步在不同的對象上,即該方法所屬的實例,只有一個線程可以在實例方法同步塊中運行。一個實例一個線程。 靜態方法同步:
public static synchronized void add(int value){
    count += value;
}
靜態方法同步是指同步在該方法上所在的類對象上的。java虛擬機中一個類只能對應一個類對象,所以同時只允許一個線程執行同一個類中的靜態同步方法。不管類中的哪個靜態同步方法被調用,一個類只能由一個線程同時執行。 實例方法中同步塊:
  public void add(int value){
    synchronized(this){
       this.count += value;   
    }
  }

示例中使用的this 是代表的調用add方法的實例本身。在同步構造器中用括號括起來的對象叫做監視器對象。

靜態方法中同步塊: 技術分享圖片
public class MyClass {

    public static synchronized void log1(String msg1, String msg2){
       log.writeln(msg1);
       log.writeln(msg2);
    }

 
    public static void log2(String msg1, String msg2){
       synchronized(MyClass.class){
          log.writeln(msg1);
          log.writeln(msg2); 
       }
    }
  }
技術分享圖片 兩個方法不允許同時被線程訪問。 如果第二個同步塊不是同步在MyClass.class這個同步器上,這兩個方法可以同時被線程訪問。 java同步示例: 技術分享圖片
 public class Counter{
     
     long count = 0;
   
     public synchronized void add(long value){
       this.count += value;
     }
  }
 
 
  public class CounterThread extends Thread{

     protected Counter counter = null;

     public CounterThread(Counter counter){
        this.counter = counter;
     }

     public void run() {
        for(int i=0; i<10; i++){
           counter.add(i);
        }
     }
  }
  public class Example {

    public static void main(String[] args){
      Counter counter = new Counter();
      Thread  threadA = new CounterThread(counter);
      Thread  threadB = new CounterThread(counter);

      threadA.start();
      threadB.start(); 
    }
  }
技術分享圖片

由於兩個線程都是共用一個counter實例,所以add()被調用的時候是同步的,只有一個線程可以調用,另外一個需要等待。 技術分享圖片
  public class Example {

    public static void main(String[] args){
      Counter counterA = new Counter();
      Counter counterB = new Counter();
      Thread  threadA = new CounterThread(counterA);
      Thread  threadB = new CounterThread(counterB);

      threadA.start();
      threadB.start(); 
    }
  }
技術分享圖片    這個時候兩個線程就可以同時調用add()方法,因為它們分別在不同的實例中。
  • 6.線程通信
線程通信的目的是使線程間可以互相發送信號。 方式: 1.通過共享對象通信 技術分享圖片
public class MySignal{

  protected boolean hasDataToProcess = false;

  public synchronized boolean hasDataToProcess(){
    return this.hasDataToProcess;
  }

  public synchronized void setHasDataToProcess(boolean hasData){
    this.hasDataToProcess = hasData; 
  }

}
技術分享圖片   兩個線程獲得指向一個MySingal共享實例的引用,以便通信。同時獲取變量的方法設置為同步方法,防止線程不一致。   2.忙等待(Busy Wait) 技術分享圖片
protected MySignal sharedSignal = ...

...

while(!sharedSignal.hasDataToProcess()){
  //do nothing... busy waiting
}
技術分享圖片   線程B一直在等待數據。但是感覺這裏和前面獲取共享變量是一個原理。   3.wait(),notify()和 notifyAll()   wait()調用後就處於非運行狀態,直到另外一個線程調用了同一個對象的notify()方法。同時線程必須獲取這個對象的鎖才能調用。 技術分享圖片
public class MonitorObject{
}

public class MyWaitNotify{

  MonitorObject myMonitorObject = new MonitorObject();

  public void doWait(){
    synchronized(myMonitorObject){
      try{
        myMonitorObject.wait();
      } catch(InterruptedException e){...}
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      myMonitorObject.notify();
    }
  }
}
技術分享圖片 調用這個對象的notify() 的時候,有一個wait的線程會被隨機喚醒,同時也有一個notifyAll()方法來喚醒所有線程。 一旦線程調用了wait()方法,就釋放了所持有的監視器對象上的鎖,就允許了其他線程也可以調用wait()或者notify().同時一個線程被喚醒不是立刻就退出wait()的方法,直到調用notify()的線程退出了自己的同步塊。 4.丟失信號 由於notify()和notifyAll()不會保存調用它們的方法,他們發送的信號如果在wait()之前就有可能丟失,這個時候必須把他們保存在信號類裏。    技術分享圖片
public class MyWaitNotify2{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      if(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}
技術分享圖片 應該就是借助一個變量來記錄是否調用過Notify()。   5.假喚醒 有時由於莫名其妙的原因,線程可能在沒有掉用過notify()和 notifyAll()的情況下醒來。防止假喚醒,保存信號的成員變量會檢查是否是自己的信號,如果不是的話,就繼續wait()。 技術分享圖片
public class MyWaitNotify3{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      while(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}
技術分享圖片   6.多個線程等待相同信號 while 循環也可以解決當多線程在等待時,只需要喚醒一個線程,並且是使用nitifyAll()來喚醒的情況。   7.不要在字符串常量或全局對象中調用wait() 就是導致假喚醒的原因之一,並且可能會導致信號沒有接收到。 管程 (Monitor)是對多個工作線程實現互斥訪問共享資源的對象和模塊。管程實現了在一個時間點,最多只有一個線程在執行他的某個子程序。
  • 6 Java ThreadLocal
java中的ThreadLocal 可以讓變量只被同一個線程進行讀和寫操作。 創建: private ThreadLocal myThreadLocal = new ThreadLocal() 訪問: myThreadLocal.set(“local value”); String threadLocalValue = (String) myThreadLocal.get(); 如果不想用強制類型轉換,可以創建一個泛型化的ThreadLocal對象。 private ThreadLocal myThreadLocal1 = new ThreadLocal<String>();

【轉】JAVA 並發性和多線程 -- 讀感 (二 線程間通訊,共享內存的機制)