(一)Java併發學習筆記

weixin_34146805發表於2018-08-02

一、課程導學

11464886-82b9dfcdaecbbb54.jpg
11464886-e465c14d194deceb.jpg

11464886-3e9046f42bd03d97.jpg

二、基本概念

併發:同時擁有兩個或者多個執行緒,如果程式在單核處理器上執行,多個執行緒將交替地換入或者換出記憶體,這些執行緒是同時“存在”的,每個執行緒都處於執行過程中的某個狀態,高速切換感覺同時執行。如果執行多核處理器上,此時,程式中的每個執行緒將分配到一個處理器核上,因此可以真正的同時執行。

高併發:高併發(High Cuncurrency)是網際網路分散式系統架構設計中必須考慮的因素之一,它通常是指,通過設計保證系統能夠 同時併發處理 很多請求。

其實當我們討論併發時主要關注的是以下幾點:

  • 多執行緒操作相同的資源
  • 保證執行緒安全
  • 合理分配和使用資源

而在討論高併發是關注的是以下幾點:

  • 伺服器能同時處理很多個請求
  • 提高程式效能
    比如在12306搶票,淘寶雙11等都需要考慮高併發

三、併發程式設計基礎

11464886-dae63cae1c6e8bff.jpg
11464886-e2f5682600d6b13b.png

11464886-5f2ce1218dfdd0b0.png
11464886-1f38e4b2e64b9303.png

在單核時代處理器做出的亂序優化不會導致執行結果遠離預期目標,但在多核環境下卻並非如此。在多核時代,由多核cpu同時執行指令,同時還引入的l1、l2等快取機制,每個核都有自己的快取,就導致了邏輯順序上後寫入的資料未必真的最後寫入。如果我們不做任何防護措施,就會出現處理器得出的結果和我們邏輯得出的結果大不相同。

比如:我們在一個cpu核心上執行寫入操作,並在最後寫入一個標記來表示該操作已經寫入好了。然後從另外一個核上通過判斷這個標記來確定所需要的資料是否已經就緒,這種做法就存在一定風險:標記位先被寫入但資料操作並未完成。導致另外一個核使用了錯誤資料。

四、Java記憶體模型(Java Memory Model,JMM)

記憶體模型可以理解為在特定的操作協議下,對特定的記憶體或者快取記憶體進行讀寫訪問的過程抽象,不同架構下的物理機擁有不一樣的記憶體模型,Java虛擬機器也有自己的記憶體模型,即Java記憶體模型(Java Memory Model, JMM)。

在C/C++語言中直接使用物理硬體和作業系統記憶體模型,導致不同平臺下併發訪問出錯。而JMM的出現,能夠遮蔽掉各種硬體和作業系統的記憶體訪問差異,實現平臺一致性,是的Java程式能夠“一次編寫,到處執行”。

11464886-9aab2573d225bbd6.png
  • 堆記憶體(Heap): 存放例項域, 靜態域, 陣列元素. 線上程間共享.
  • 棧記憶體(Stack): 存放區域性變數, 方法定義引數和異常處理器引數.


    11464886-22f568ce134e52ca.png

執行緒A和執行緒B要進行通訊,必須先將資料重新整理到主記憶體,執行緒B再從主記憶體讀取執行緒A更新過的變數。

模擬場景:
比如多個執行緒同時修改一個變數:執行緒A 先從主記憶體中獲取共享變數(a=2),然後在自己本地記憶體中計算(a+2),然後寫入到主記憶體。
但此時B也從主記憶體獲取(a=2),在本地記憶體改變(a+2).寫入到主記憶體。
在計算過程中兩個執行緒間的資料是不可見的,此時就會出現結果不正確情況。

Java記憶體模型-同步操作與規則

11464886-d44725e3621788b1.jpg

由上面的互動關係可知,關於主記憶體與工作記憶體之間的具體互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之間的實現細節,Java記憶體模型定義了以下八種操作來完成:

lock(鎖定):作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態。
unlock(解鎖):作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
read(讀取):作用於主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
use(使用):作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。
assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
store(儲存):作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。
write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。

Java記憶體模型還規定了在執行上述八種基本操作時,必須滿足如下規則:

  • 不允許read和load、store和write操作之一單獨出現
  • 不允許一個執行緒丟棄它的最近assign的操作,即變數在工作記憶體中改變了之後必須同步到主記憶體中。
  • 不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體中。
  • 一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數。即就是對一個變數實施use和store操作之前,必須先執行過了assign和load操作。
  • 一個變數在同一時刻只允許一條執行緒對其進行lock操作,lock和unlock必須成對出現
  • 如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行load或assign操作初始化變數的值
  • 如果一個變數事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他執行緒鎖定的變數。
  • 對一個變數執行unlock操作之前,必須先把此變數同步到主記憶體中(執行store和write操作)。
    這8種記憶體訪問操作很繁瑣,後文會使用一個等效判斷原則,即先行發生(happens-before)原則來確定一個記憶體訪問在併發環境下是否安全。

五、併發的優勢和風險

11464886-495080707e079584.jpg

六、執行緒安全性

11464886-bf68e85379fd08e6.png

執行緒安全性主要體現在三個方面:

11464886-cd4820aadb9251ba.png

1. 原子性:
原子是世界上的最小單位,具有不可分割性。比如 a=0;(a非long和double型別)這個操作是不可分割的,那麼我們說這個操作時原子操作。再比如:a++;這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作。非原子操作都會存線上程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那麼我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

Atomicxxx底層工作原理:

藉助於Unsafe.compareAndSwapInt: CAS實現,
每次執行計算之前都會拿當前工作記憶體中的值和主記憶體的值比較,如果不相同就會從新從主記憶體中獲取最新值賦值給當前物件,直到相同執行對應操作。

sun.misc.Unsafe原始碼:
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

AtomicLong和AtomicAddr
AtomicLong的原理是依靠底層的cas來保障原子性的更新資料,在要新增或者減少的時候,會使用死迴圈不斷地cas到特定的值,從而達到更新資料的目的。在競爭不激烈時修改成功的概率很高,否則修改失敗的概率就很高,在大量修改失敗的情況下,這些原子操作就會進行大量的失敗重嘗試,效能就會受到影響。

總結:

  • LongAdder在AtomicLong的基礎上將單點的更新壓力分散到各個節點,在低併發的時候通過對base的直接更新可以很好的保障和AtomicLong的效能基本保持一致,而在高併發的時候通過分散提高了效能。
  • 缺點是LongAdder在統計的時候如果有併發更新,可能導致統計的資料有誤差。
  • 實際使用中在處理高併發計數時,推薦使用LongAddr,但如果遇到類似於序列號生成這種需要全域性唯一的資料情況就需要使用AtomicLong.

AtomicReference

AtomicReference和AtomicInteger非常類似,不同之處就在於AtomicInteger是對整數的封裝,而AtomicReference則對應普通的物件引用。也就是它可以保證你在修改物件引用時的執行緒安全性。在介紹AtomicReference的同時,我希望同時提出一個有關原子操作的邏輯上的不足。

之前我們說過,執行緒判斷被修改物件是否可以正確寫入的條件是物件的當前值和期望是否一致。這個邏輯從一般意義上來說是正確的。但有可能出現一個小小的例外,就是當你獲得物件當前資料後,在準備修改為新值前,物件的值被其他執行緒連續修改了2次,而經過這2次修改後,物件的值又恢復為舊值。這樣,當前執行緒就無法正確判斷這個物件究竟是否被修改過。如圖4.2所示,顯示了這種情況。(ABA問題)

11464886-8b5a28f0149dd64d.png
圖4.2 物件值被反覆修改回原資料

一般來說,發生這種情況的概率很小。而且即使發生了,可能也不是什麼大問題。比如,我們只是簡單得要做一個數值加法,即使在我取得期望值後,這個數字被不斷的修改,只要它最終改回了我的期望值,我的加法計算就不會出錯。也就是說,當你修改的物件沒有過程的狀態資訊,所有的資訊都只儲存於物件的數值本身。

但是,在現實中,還可能存在另外一種場景。就是我們是否能修改物件的值,不僅取決於當前值,還和物件的過程變化有關,這時,AtomicReference就無能為力了。

打一個比方,如果有一家蛋糕店,為了挽留客戶,絕對為貴賓卡里餘額小於20元的客戶一次性贈送20元,刺激消費者充值和消費。但條件是,每一位客戶只能被贈送一次。

現在,我們就來模擬這個場景,為了演示AtomicReference,我在這裡使用AtomicReference實現這個功能。首先,我們模擬使用者賬戶餘額。

static AtomicReference<Integer> money=newAtomicReference<Integer>();
// 設定賬戶初始值小於20,顯然這是一個需要被充值的賬戶
money.set(19);

接著,我們需要若干個後臺執行緒,它們不斷掃描資料,併為滿足條件的客戶充值。

01 //模擬多個執行緒同時更新後臺資料庫,為使用者充值
02 for(int i = 0 ; i < 3 ; i++) {            
03     new Thread(){
04         publicvoid run() {
05            while(true){
06                while(true){
07                    Integer m=money.get();
08                    if(m<20){
09                        if(money.compareAndSet(m, m+20)){
10                  System.out.println("餘額小於20元,充值成功,餘額:"+money.get()+"元");
11                             break;
12                        }
13                    }else{
14                        //System.out.println("餘額大於20元,無需充值");
15                         break ;
16                    }
17                 }
18             }
19         }
20     }.start();
21 }

上述程式碼第8行,判斷使用者餘額並給予贈予金額。如果已經被其他使用者處理,那麼當前執行緒就會失敗。因此,可以確保使用者只會被充值一次。

此時,如果很不幸的,使用者正好正在進行消費,就在贈予金額到賬的同時,他進行了一次消費,使得總金額又小於20元,並且正好累計消費了20元。使得消費、贈予後的金額等於消費前、贈予前的金額。這時,後臺的贈予程式就會誤以為這個賬戶還沒有贈予,所以,存在被多次贈予的可能。下面,模擬了這個消費執行緒:

01 //使用者消費執行緒,模擬消費行為
02 new Thread() {
03     public voidrun() {
04         for(inti=0;i<100;i++){
05            while(true){
06                Integer m=money.get();
07                 if(m>10){
08                    System.out.println("大於10元");
09                    if(money.compareAndSet(m, m-10)){
10                        System.out.println("成功消費10元,餘額:"+money.get());
11                        break;
12                    }
13                }else{
14                    System.out.println("沒有足夠的金額");
15                    break;
16                 }
17             }
18             try{Thread.sleep(100);} catch (InterruptedException e) {}
19         }
20     }
21 }.start();

上述程式碼中,消費者只要貴賓卡里的錢大於10元,就會立即進行一次10元的消費。執行上述程式,得到的輸出如下:

餘額小於20元,充值成功,餘額:39元
大於10元
成功消費10元,餘額:29
大於10元
成功消費10元,餘額:19
餘額小於20元,充值成功,餘額:39元
大於10元
成功消費10元,餘額:29
大於10元
成功消費10元,餘額:39
餘額小於20元,充值成功,餘額:39元
從這一段輸出中,可以看到,這個賬戶被先後反覆多次充值。其原因正是因為賬戶餘額被反覆修改,修改後的值等於原有的數值。使得CAS操作無法正確判斷當前資料狀態。

雖然說這種情況出現的概率不大,但是依然是有可能的出現的。因此,當業務上確實可能出現這種情況時,我們也必須多加防範。體貼的JDK也已經為我們考慮到了這種情況,使用AtomicStampedReference就可以很好的解決這個(ABA)問題。
ABA問題:簡單講就是多執行緒環境,2次讀寫中一個執行緒修改A->B,然後又B->A,另一個執行緒看到的值未改變,又繼續修改成自己的期望值。當然我們如果不關心過程,只關心結果,那麼這個就是無所謂的ABA問題。

  • 為了解決ABA問題,偉大的java為我們提供了AtomicMarkableReference和AtomicStampedReference類,為我們解決了問題
  • AtomicStampedReference是利用版本戳的形式記錄了每次改變以後的版本號,這樣的話就不會存在ABA問題了,在這裡我借鑑一下別人舉得例子

舉個通俗點的例子,你倒了一杯水放桌子上,幹了點別的事,然後同事把你水喝了又給你重新倒了一杯水,你回來看水還在,拿起來就喝,如果你不管水中間被人喝過,只關心水還在,這就是ABA問題。如果你是一個講衛生講文明的小夥子,不但關心水在不在,還要在你離開的時候水被人動過沒有,因為你是程式設計師,所以就想起了放了張紙在旁邊,寫上初始值0,別人喝水前麻煩先做個累加才能喝水。這就是AtomicStampedReference的解決方案。

Synchronized關鍵字

在Java中,synchronized關鍵字是用來控制執行緒同步的,就是在多執行緒的環境下,控制synchronized程式碼段不被多個執行緒同時執行。

11464886-03d183ae46db0c13.png

1. 修飾方法
Synchronized修飾一個方法很簡單,就是在方法的前面加synchronized,synchronized修飾方法和修飾一個程式碼塊類似,只是作用範圍不一樣,修飾程式碼塊是大括號括起來的範圍,而修飾方法範圍是整個函式。

public synchronized void method()
{
   // todo
}

寫法一修飾的是一個方法,鎖定了整個方法時的內容。

synchronized關鍵字不能繼承。
雖然可以使用synchronized來定義方法,但synchronized並不屬於方法定義的一部分,因此,synchronized關鍵字不能被繼承。如果在父類中的某個方法使用了synchronized關鍵字,而在子類中覆蓋了這個方法,在子類中的這個方法預設情況下並不是同步的,而必須顯式地在子類的這個方法中加上synchronized關鍵字才可以。當然,還可以在子類方法中呼叫父類中相應的方法,這樣雖然子類中的方法不是同步的,但子類呼叫了父類的同步方法,因此,子類的方法也就相當於同步了。這兩種方式的例子程式碼如下:

在子類方法中加上synchronized關鍵字

class Parent {
   public synchronized void method() { }
}
class Child extends Parent {
   public synchronized void method() { }
}

在子類方法中呼叫父類的同步方法

class Parent {
   public synchronized void method() {   }
}
class Child extends Parent {
   public void method() { super.method();   }
} 
  • 在定義介面方法時不能使用synchronized關鍵字。
  • 構造方法不能使用synchronized關鍵字,但可以使用synchronized程式碼塊來進行同步。

2. 修飾程式碼塊
1) 一個執行緒訪問一個物件中的synchronized(this)同步程式碼塊時,其他試圖訪問該物件的執行緒將被阻塞

注意下面兩個程式的區別

class SyncThread implements Runnable {
       private static int count;
 
       public SyncThread() {
          count = 0;
       }
 
       public  void run() {
          synchronized(this) {
             for (int i = 0; i < 5; i++) {
                try {
                   System.out.println(Thread.currentThread().getName() + ":" + (count++));
                   Thread.sleep(100);
                } catch (InterruptedException e) {
                   e.printStackTrace();
                }
             }
          }
       }
 
       public int getCount() {
          return count;
       }
}
 
public class Demo00 {
    public static void main(String args[]){
//test01
//      SyncThread s1 = new SyncThread();
//      SyncThread s2 = new SyncThread();
//      Thread t1 = new Thread(s1);
//      Thread t2 = new Thread(s2);
//test02        
        SyncThread s = new SyncThread();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        
        t1.start();
        t2.start();
    }
}

test01的執行結果

11464886-0fb727c1324e279b.jpg

test02的執行結果


11464886-61fc398dcd03e712.jpg

當兩個併發執行緒(thread1和thread2)訪問同一個物件(syncThread)中的synchronized程式碼塊時,在同一時刻只能有一個執行緒得到執行,另一個執行緒受阻塞,必須等待當前執行緒執行完這個程式碼塊以後才能執行該程式碼塊。Thread1和thread2是互斥的,因為在執行synchronized程式碼塊時會鎖定當前的物件,只有執行完該程式碼塊才能釋放該物件鎖,下一個執行緒才能執行並鎖定該物件

為什麼上面的例子中thread1和thread2同時在執行。這是因為synchronized只鎖定物件,每個物件只有一個鎖(lock)與之相關聯。

2) 當一個執行緒訪問物件的一個synchronized(this)同步程式碼塊時,另一個執行緒仍然可以訪問該物件中的非synchronized(this)同步程式碼塊。

例:

class Counter implements Runnable{
   private int count;
 
   public Counter() {
      count = 0;
   }
 
   public void countAdd() {
      synchronized(this) {
         for (int i = 0; i < 5; i ++) {
            try {
               System.out.println(Thread.currentThread().getName() + ":" + (count++));
               Thread.sleep(100);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }
   }
 
   //非synchronized程式碼塊,未對count進行讀寫操作,所以可以不用synchronized
   public void printCount() {
      for (int i = 0; i < 5; i ++) {
         try {
            System.out.println(Thread.currentThread().getName() + " count:" + count);
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
 
   public void run() {
      String threadName = Thread.currentThread().getName();
      if (threadName.equals("A")) {
         countAdd();
      } else if (threadName.equals("B")) {
         printCount();
      }
   }
}
 
public class Demo00{
    public static void main(String args[]){
        Counter counter = new Counter();
        Thread thread1 = new Thread(counter, "A");
        Thread thread2 = new Thread(counter, "B");
        thread1.start();
        thread2.start();
    }
}

執行結果


11464886-f769bfb6c3eeee77.jpg

可以看見B執行緒的呼叫是非synchronized,並不影響A執行緒對synchronized部分的呼叫。從上面的結果中可以看出一個執行緒訪問一個物件的synchronized程式碼塊時,別的執行緒可以訪問該物件的非synchronized程式碼塊而不受阻塞。

3)指定要給某個物件加鎖


/**
 * 銀行賬戶類
 */
class Account {
   String name;
   float amount;
 
   public Account(String name, float amount) {
      this.name = name;
      this.amount = amount;
   }
   //存錢
   public  void deposit(float amt) {
      amount += amt;
      try {
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
   //取錢
   public  void withdraw(float amt) {
      amount -= amt;
      try {
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
 
   public float getBalance() {
      return amount;
   }
}
 
/**
 * 賬戶操作類
 */
class AccountOperator implements Runnable{
   private Account account;
   public AccountOperator(Account account) {
      this.account = account;
   }
 
   public void run() {
      synchronized (account) {
         account.deposit(500);
         account.withdraw(500);
         System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());
      }
   }
}
 
public class Demo00{
    
    //public static final Object signal = new Object(); // 執行緒間通訊變數
    //將account改為Demo00.signal也能實現執行緒同步
    public static void main(String args[]){
        Account account = new Account("zhang san", 10000.0f);
        AccountOperator accountOperator = new AccountOperator(account);
 
        final int THREAD_NUM = 5;
        Thread threads[] = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i ++) {
           threads[i] = new Thread(accountOperator, "Thread" + i);
           threads[i].start();
        }
    }
}

執行結果


11464886-891351daa7398414.jpg

在AccountOperator 類中的run方法裡,我們用synchronized 給account物件加了鎖。這時,當一個執行緒訪問account物件時,其他試圖訪問account物件的執行緒將會阻塞,直到該執行緒訪問account物件結束。也就是說誰拿到那個鎖誰就可以執行它所控制的那段程式碼。
當有一個明確的物件作為鎖時,就可以用類似下面這樣的方式寫程式。

public void method3(SomeObject obj)
{
   //obj 鎖定的物件
   synchronized(obj)
   {
      // todo
   }
}

當沒有明確的物件作為鎖,只是想讓一段程式碼同步時,可以建立一個特殊的物件來充當鎖:

class Test implements Runnable
{
   private byte[] lock = new byte[0];  // 特殊的instance變數
   public void method()
   {
      synchronized(lock) {
         // todo 同步程式碼塊
      }
   }
 
   public void run() {
 
   }
}

3. 修飾一個靜態的方法

Synchronized也可修飾一個靜態方法,用法如下:

public synchronized static void method() {
   // todo
}

靜態方法是屬於類的而不屬於物件的。同樣的,synchronized修飾的靜態方法鎖定的是這個類的所有物件。

/**
 * 同步執行緒
 */
class SyncThread implements Runnable {
   private static int count;
 
   public SyncThread() {
      count = 0;
   }
 
   public synchronized static void method() {
      for (int i = 0; i < 5; i ++) {
         try {
            System.out.println(Thread.currentThread().getName() + ":" + (count++));
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
 
   public synchronized void run() {
      method();
   }
}
 
public class Demo00{
    
    public static void main(String args[]){
        SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "SyncThread1");
        Thread thread2 = new Thread(syncThread2, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

11464886-cc340bbc06986d71.jpg

syncThread1和syncThread2是SyncThread的兩個物件,但在thread1和thread2併發執行時卻保持了執行緒同步。這是因為run中呼叫了靜態方法method,而靜態方法是屬於類的,所以syncThread1和syncThread2相當於用了同一把鎖。

4. 修飾一個類
Synchronized還可作用於一個類,用法如下:

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}

/**
 * 同步執行緒
 */
class SyncThread implements Runnable {
   private static int count;
 
   public SyncThread() {
      count = 0;
   }
 
   public static void method() {
      synchronized(SyncThread.class) {
         for (int i = 0; i < 5; i ++) {
            try {
               System.out.println(Thread.currentThread().getName() + ":" + (count++));
               Thread.sleep(100);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }
   }
 
   public synchronized void run() {
      method();
   }
}

本例的的給class加鎖和上例的給靜態方法加鎖是一樣的,所有物件公用一把鎖

總結
A. 無論synchronized關鍵字加在方法上還是物件上,如果它作用的物件是非靜態的,則它取得的鎖是物件;如果synchronized作用的物件是一個靜態方法或一個類,則它取得的鎖是對類,該類所有的物件同一把鎖。
B. 每個物件只有一個鎖(lock)與之相關聯,誰拿到這個鎖誰就可以執行它所控制的那段程式碼。
C. 實現同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,所以儘量避免無謂的同步控制。

11464886-60bc8726e666da5d.png

2. 執行緒可見性

11464886-e72a785e2c4c1669.png

  • 可見性-synchronized
11464886-94f37508889cfcb0.png
  • 可見性-volatile
11464886-11f4ffcc2237d277.png

記憶體屏障(memory barrier) 是一個CPU指令。基本上,它是這樣一條指令: a) 確保一些特定操作執行的順序; b) 影響一些資料的可見性(可能是某些指令執行後的結果)。編譯器和CPU可以在保證輸出結果一樣的情況下對指令重排序,使效能得到優化。插入一個記憶體屏障, 相當於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。記憶體屏障另一個作用是強制更新一次不同CPU的快取。例如,一個寫屏障會 把這個屏障前寫入的資料重新整理到快取,這樣任何試圖讀取該資料的執行緒將得到最新值,而不用考慮到底是被哪個cpu核心或者哪顆CPU執行的。

volatile使用場景:
volatile很適合用作狀態標示量:

11464886-a63e622a3e4c585d.png
  • 有序性
11464886-a7df2a59c63c2c0f.png

happend-before原則

1.程式次序規則:一個執行緒內,按照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作;
2.鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作;
3.volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作;
4.傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C;
5.執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每個一個動作;
6.執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生;
7.執行緒終結規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行;
8.物件終結規則:一個物件的初始化完成先行發生於他的finalize()方法的開始;

相關文章