溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java多線程與并發(fā)筆記

發(fā)布時(shí)間:2020-07-21 08:03:09 來源:網(wǎng)絡(luò) 閱讀:1056 作者:ZeroOne01 欄目:編程語言

synchronized

synchronized主要是用于解決線程安全問題的,而線程安全問題的主要誘因有如下兩點(diǎn):

  • 存在共享數(shù)據(jù)(也稱臨界資源)
  • 存在多條線程共同操作這些共享數(shù)據(jù)

解決線程安全問題的根本方法:

  • 同一時(shí)刻有且只有一個(gè)線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再對(duì)共享數(shù)據(jù)進(jìn)行操作

所以互斥鎖是解決問題的辦法之一,互斥鎖的特性如下:

互斥性:即在同一時(shí)間只允許一個(gè)線程持有某個(gè)對(duì)象鎖,通過這種特性來實(shí)現(xiàn)多線程的協(xié)調(diào)機(jī)制,這樣在同一時(shí)間只有一個(gè)線程對(duì)需要同步的代碼塊(復(fù)合操作)進(jìn)行訪問?;コ庑砸卜Q為操作的原子性
可見性:必須確保在鎖被釋放之前,對(duì)共享變量所做的修改,對(duì)于隨后獲得該鎖的另一個(gè)線程是可見的(即在獲得鎖時(shí)應(yīng)獲得最新共享變量的值),否則另一個(gè)線程可能是在本地緩存的某個(gè)副本上繼續(xù)操作,從而引起數(shù)據(jù)不一致問題

而synchronized就可以實(shí)現(xiàn)互斥鎖的特性,不過需要注意的是synchronized鎖的不是代碼,而是對(duì)象。

根據(jù)獲取的鎖可以分為兩類:

  • 對(duì)象鎖:獲取對(duì)象鎖有兩種用法
    1. 同步代碼塊(synchronized(this),synchronized(類實(shí)例對(duì)象)),鎖是小括號(hào)()中的實(shí)例對(duì)象
    2. 同步非靜態(tài)方法(synchronized method),鎖是當(dāng)前的實(shí)例對(duì)象,即this
  • 類鎖:獲取類鎖也有兩種用法
    1. 同步代碼塊(synchronized(類.class)),鎖是小括號(hào)()中的類對(duì)象(Class對(duì)象)
    2. 同步靜態(tài)方法(synchronized static method),鎖是當(dāng)前對(duì)象的類對(duì)象(Class對(duì)象)

對(duì)象鎖和類鎖的總結(jié):

  1. 有線程訪問對(duì)象的同步塊代碼時(shí),另外的線程可以訪問該對(duì)象的非同步代碼塊
  2. 若鎖住的是同一個(gè)對(duì)象,一個(gè)線程在訪問對(duì)象的同步代碼塊時(shí),另一個(gè)訪問對(duì)象的同步代碼塊的線程會(huì)被阻塞
  3. 若鎖住的是同一個(gè)對(duì)象,一個(gè)線程在訪問對(duì)象的同步方法時(shí),另一個(gè)訪問對(duì)象同步方法的線程會(huì)被阻塞
  4. 若鎖住的是同一個(gè)對(duì)象,一個(gè)線程在訪問對(duì)象的同步塊時(shí),另一個(gè)訪問對(duì)象同步方法的線程會(huì)被阻塞,反之亦然
  5. 同一個(gè)類的不同對(duì)象的對(duì)象鎖互不干擾
  6. 類鎖由于也是一把特殊的對(duì)象鎖,因此表現(xiàn)與上述1,2,3,4一致,而由于一個(gè)類只有一把對(duì)象鎖,所以同一個(gè)類的不同對(duì)象使用類鎖將會(huì)是同步的
  7. 類鎖和對(duì)象鎖互補(bǔ)干擾,因?yàn)轭悓?duì)象和實(shí)例對(duì)象不是同一個(gè)對(duì)象

synchronized底層實(shí)現(xiàn)原理

實(shí)現(xiàn)synchronized需要依賴兩個(gè)基礎(chǔ)概念:

  • Java對(duì)象頭
  • Monitor

Java對(duì)象在內(nèi)存中的布局主要分為三塊區(qū)域:

  • 對(duì)象頭
  • 實(shí)例數(shù)據(jù)
  • 對(duì)齊填充

synchronized使用的鎖對(duì)象是存儲(chǔ)在Java對(duì)象頭里的,對(duì)象頭結(jié)構(gòu)如下:
Java多線程與并發(fā)筆記

由于對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)沒有關(guān)系的額外存儲(chǔ)成本,考慮到JVM的空間效率,Mark Word被設(shè)計(jì)為非固定的數(shù)據(jù)結(jié)構(gòu)以便存儲(chǔ)更多有效的數(shù)據(jù),它會(huì)根據(jù)對(duì)象自身的狀態(tài)賦予自己的存儲(chǔ)空間:
Java多線程與并發(fā)筆記

簡(jiǎn)單介紹了對(duì)象頭,接著我們來了解一下Monitor,每個(gè)Java對(duì)象天生自帶了一把看不見的鎖,它叫做內(nèi)部鎖或Monitor鎖。Monitor的主要實(shí)現(xiàn)代碼在ObjectMonitor.hpp中:
Java多線程與并發(fā)筆記

Monitor鎖的競(jìng)爭(zhēng)、獲取與釋放:
Java多線程與并發(fā)筆記

然后我們從字節(jié)碼層面上看一下synchronized,將如下代碼通過javac編譯成class文件:

package com.example.demo.thread;

/**
 * @author 01
 * @date 2019-07-20
 **/
public class SyncBlockAndMethod {

    public void syncsTask() {
        synchronized (this) {
            System.out.println("Hello syncsTask");
        }
    }

    public synchronized void syncTask() {
        System.out.println("Hello syncTask");
    }
}

然后通過 javap -verbose 將class文件反編譯成可閱讀的字節(jié)碼內(nèi)容,如下:

Classfile /E:/Java_IDEA/demo/src/main/java/com/example/demo/thread/SyncBlockAndMethod.class
  Last modified 2019年7月20日; size 637 bytes
  MD5 checksum 7600723349daa088a5353acd84c80fa5
  Compiled from "SyncBlockAndMethod.java"
public class com.example.demo.thread.SyncBlockAndMethod
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #6                          // com/example/demo/thread/SyncBlockAndMethod
  super_class: #7                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #7.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #19.#20        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #21            // Hello syncsTask
   #4 = Methodref          #22.#23        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #24            // Hello syncTask
   #6 = Class              #25            // com/example/demo/thread/SyncBlockAndMethod
   #7 = Class              #26            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               syncsTask
  #13 = Utf8               StackMapTable
  #14 = Class              #27            // java/lang/Throwable
  #15 = Utf8               syncTask
  #16 = Utf8               SourceFile
  #17 = Utf8               SyncBlockAndMethod.java
  #18 = NameAndType        #8:#9          // "<init>":()V
  #19 = Class              #28            // java/lang/System
  #20 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #21 = Utf8               Hello syncsTask
  #22 = Class              #31            // java/io/PrintStream
  #23 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #24 = Utf8               Hello syncTask
  #25 = Utf8               com/example/demo/thread/SyncBlockAndMethod
  #26 = Utf8               java/lang/Object
  #27 = Utf8               java/lang/Throwable
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public com.example.demo.thread.SyncBlockAndMethod();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0

  public void syncsTask();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter                      // 指向同步代碼塊的開始位置
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String Hello syncsTask
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit                       // 指向同步代碼塊的結(jié)束位置,monitorenter和monitorexit之間就是同步代碼塊
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit                       // 若代碼發(fā)生異常時(shí)就會(huì)執(zhí)行這句指令釋放鎖
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 10: 0
        line 11: 4
        line 12: 12
        line 13: 22
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class com/example/demo/thread/SyncBlockAndMethod, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void syncTask();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED  // 用于標(biāo)識(shí)是一個(gè)同步方法,不需要像同步塊那樣需要通過顯式的字節(jié)碼指令去標(biāo)識(shí)哪里需要獲取鎖,哪里需要釋放鎖。同步方法無論是正常執(zhí)行還是發(fā)生異常都會(huì)釋放鎖
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String Hello syncTask
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 16: 0
        line 17: 8
}
SourceFile: "SyncBlockAndMethod.java"

什么是重入:

從互斥鎖的設(shè)計(jì)上來說,當(dāng)一個(gè)線程試圖操作一個(gè)由其他線程持有的對(duì)象鎖的臨界資源時(shí),將會(huì)處于阻塞狀態(tài),但當(dāng)一個(gè)線程再次請(qǐng)求自己持有對(duì)象鎖的臨界資源時(shí),這種情況屬于重入

為什么會(huì)對(duì)synchronized嗤之以鼻:

  • 在早期版本中,synchronized屬于重量級(jí)鎖效率低下,因?yàn)橐蕾囉贛utex Lock實(shí)現(xiàn),因?yàn)榫€程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),開銷較大。不過在Java6以后引入了許多鎖優(yōu)化機(jī)制,synchronized性能已經(jīng)得到了很大的提升

鎖優(yōu)化之自旋鎖:

許多情況下,共享數(shù)據(jù)的鎖定狀態(tài)持續(xù)時(shí)間較短,切換線程不值得。于是自旋鎖應(yīng)運(yùn)而生,所謂自旋就是通過讓線程執(zhí)行忙循環(huán)等待鎖的釋放,從而不讓出CPU時(shí)間片,例如while某個(gè)標(biāo)識(shí)變量

缺點(diǎn):若鎖被其他線程長(zhǎng)時(shí)間占用,將會(huì)帶來許多性能上的開銷,所以一般超過指定的自旋次數(shù)就會(huì)將線程掛起處于阻塞狀態(tài)

鎖優(yōu)化之自適應(yīng)自旋鎖:

自適應(yīng)自旋鎖與普通自旋鎖不同的就是可以自適應(yīng)自旋次數(shù),即自旋次數(shù)不再固定。而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來決定

鎖優(yōu)化之鎖消除,鎖消除是JVM另一種鎖優(yōu)化,這種優(yōu)化更徹底:

在JIT編譯時(shí),對(duì)運(yùn)行上下文進(jìn)行掃描,去除不可能存在資源競(jìng)爭(zhēng)的鎖。這種方式可以消除不必要的鎖,可以減少毫無意義的請(qǐng)求鎖時(shí)間

關(guān)于鎖消除,我們可以看一個(gè)例子,代碼如下:

public class StringBufferWithoutSync {

    public void add(String str1, String str2) {
        //StringBuffer是線程安全,由于sb只會(huì)在append方法中使用,不可能被其他線程引用
        //因此sb屬于不可能共享的資源,JVM會(huì)自動(dòng)消除內(nèi)部的鎖
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
        for (int i = 0; i < 1000; i++) {
            withoutSync.add("aaa", "bbb");
        }
    }
}

鎖優(yōu)化之鎖粗化,我們?cè)賮砹私怄i粗化的概念,有些情況下可能會(huì)需要頻繁且重復(fù)進(jìn)行加鎖和解鎖操作,例如同步代碼寫在循環(huán)語句里,此時(shí)JVM會(huì)有鎖粗化的機(jī)制,即通過擴(kuò)大加鎖的范圍,以避免反復(fù)加鎖和解鎖操作。代碼示例:

public class CoarseSync {

    public static String copyString100Times(String target){
        int i = 0;
        // JVM會(huì)將鎖粗化到外部,使得重復(fù)的加解鎖操作只需要進(jìn)行一次
        StringBuffer sb = new StringBuffer();
        while (i < 100){
            sb.append(target);
        }

        return sb.toString();
    }
}

synchronized鎖存在四種狀態(tài):

  • 無鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖
  • 鎖膨脹的方向:無鎖 -> 偏向鎖 -> 輕量級(jí)鎖 -> 重量級(jí)鎖
  • 鎖膨脹存在跨級(jí)現(xiàn)象,例如直接從無鎖膨脹到重量級(jí)鎖

偏向鎖:

大多數(shù)情況下,鎖不存在多線程競(jìng)爭(zhēng),總是由同一線程多次獲得,為了減少同一線程獲取鎖的代價(jià),就會(huì)使用偏向鎖

核心思想:
如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式,此時(shí)Mark Word的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)該線程再次請(qǐng)求鎖時(shí),無需再做任何同步操作,即獲取鎖的過程只需要檢查Mark Word的鎖標(biāo)記位為偏向鎖以及當(dāng)前線程id等于Mark Word的ThreadID即可,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作,那么這個(gè)鎖也就偏向于該線程了

偏向鎖不適用于鎖競(jìng)爭(zhēng)比較激烈的多線程場(chǎng)合

輕量級(jí)鎖:

輕量級(jí)鎖是由偏向鎖升級(jí)而來,偏向鎖運(yùn)行在一個(gè)線程進(jìn)入同步塊的情況下,當(dāng)有第二個(gè)線程加入鎖競(jìng)爭(zhēng)時(shí),偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖

適用場(chǎng)景:線程交替執(zhí)行同步塊

若存在線程同一時(shí)間訪問同一鎖的情況,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖

輕量級(jí)鎖的加鎖過程:

  1. 在代碼進(jìn)入同步塊的時(shí)候,如果同步對(duì)象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài)),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(LockRecord)的空間,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝,官方稱之為Displaced Mark Word。這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如下圖所示:
    Java多線程與并發(fā)筆記

  2. 拷貝對(duì)象頭中的Mark Word復(fù)制到鎖記錄中
  3. 拷貝成功后,虛擬機(jī)將使用CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指針,并將Lock record里的owner指針指向object mark word。如果更新成功,則執(zhí)行步驟4,否則執(zhí)行步驟5
  4. 如果這個(gè)更新動(dòng)作成功了,那么這個(gè)線程就擁有了該對(duì)象的鎖,并且對(duì)象Mark Word的鎖標(biāo)志位設(shè)置為“00",即表示此對(duì)象處于輕量級(jí)鎖定狀態(tài),這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如下圖所示:
    Java多線程與并發(fā)筆記

  5. 如果這個(gè)更新操作失敗了,虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說明多個(gè)線程競(jìng)爭(zhēng)鎖,輕量級(jí)鎖就要膨脹為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0",Mark Word中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。而當(dāng)前線程便嘗試使用自旋來獲取鎖,自旋咱們前面講過,就是為了不讓線程阻塞,而采用循環(huán)去獲取鎖的過程

輕量級(jí)鎖的解鎖過程:

  1. 通過CAS操作嘗試把線程中復(fù)制的Displaced Mark Word對(duì)象替換當(dāng)前的Mark Word
  2. 如果替換成功,整個(gè)同步過程就完成了
  3. 如果替換失敗,說明有其他線程嘗試過獲取該鎖(此時(shí)鎖己膨脹),那就要在釋放鎖的同時(shí),喚醒被掛起的線程

鎖的內(nèi)存語義:

當(dāng)線程釋放鎖時(shí),Java內(nèi)存模型會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中;而當(dāng)線程獲取鎖時(shí),Java內(nèi)存模型會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效,從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量
Java多線程與并發(fā)筆記

偏向鎖、輕量級(jí)鎖、重量級(jí)鎖的匯總:
Java多線程與并發(fā)筆記


synchronized和ReentrantLock的區(qū)別

在JDK1.5之前,synchronized是Java唯一的同步手段,而在1.5之后則有了ReentrantLock類(重入鎖):

  • 位于java.util.concurrent.locks包
  • 和CountDownLatch、FuturaTask、Semaphore一樣基于AQS框架實(shí)現(xiàn)
  • 能夠?qū)崿F(xiàn)比synchronized更細(xì)粒度的控制,如控制fairness
  • 調(diào)用lock之后,必須調(diào)用unlock釋放鎖
  • 在JDK6之后性能未必比synchronized高,并且也是可重入的

ReentrantLock公平性的設(shè)置:

  • ReentrantLock fairLock = new ReentrantLock(true);
  • 參數(shù)為true時(shí),傾向于將鎖賦予等待時(shí)間最久的線程,即設(shè)置為所謂的公平鎖,公平性是減少線程饑餓的一個(gè)辦法
  • 公平鎖:獲取鎖的順序按先后調(diào)用lock方法的順序,公平鎖需慎用,因?yàn)闀?huì)影響性能
  • 非公平鎖:線程搶占鎖的順序不一定,與調(diào)用順序無關(guān),看運(yùn)氣
  • synchronized是非公平鎖

ReentrantLock的好處在于將鎖對(duì)象化了,因此可以實(shí)現(xiàn)synchronized難以實(shí)現(xiàn)的邏輯,例如:

  • 判斷是否有線程,或者某個(gè)特定線程,在排隊(duì)等待獲取鎖
  • 帶超時(shí)的獲取鎖的嘗試
  • 感知有沒有成功獲取鎖

如果說ReentrantLock將synchronized轉(zhuǎn)變?yōu)榱丝煽氐膶?duì)象,那么是否能將wait、notify及notifyall等方法對(duì)象化,答案是有的,即Condition:

  • 位于java.util.concurrent.locks包
  • 可以通過ReentrantLock的newCondition方法獲取該Condition對(duì)象實(shí)例

synchronized和ReentrantLock的區(qū)別:

  • synchronized是關(guān)鍵字,ReentrantLock是類
  • ReentrantLock可以對(duì)獲取鎖的等待時(shí)間進(jìn)行設(shè)置,避免死鎖
  • ReentrantLock可以獲取各種鎖的信息
  • ReentrantLock可以靈活地實(shí)現(xiàn)多路通知
  • 內(nèi)部機(jī)制:synchronized操作的是Mark Word,而ReentrantLock底層是調(diào)用Unsafe類的park方法來加鎖

jmm的內(nèi)存可見性

Java內(nèi)存模型(JMM):

Java內(nèi)存模型(Java Memory Model,簡(jiǎn)稱JMM)本身是一種抽象的概念,并不真實(shí)存在,它描述的是一組規(guī)則或規(guī)范,通過這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問方式

Java多線程與并發(fā)筆記

JMM中的主內(nèi)存(即堆空間):

  • 存儲(chǔ)Java實(shí)例對(duì)象
  • 包括成員變量、類信息、常量、靜態(tài)變量等
  • 屬于數(shù)據(jù)共享的區(qū)域,多線程并發(fā)操作時(shí)會(huì)引發(fā)線程安全問題

JMM中的工作內(nèi)存(即本地內(nèi)存,或線程棧):

  • 存儲(chǔ)當(dāng)前方法的所有本地變量信息,本地變量對(duì)其他線程不可見
  • 字節(jié)碼行號(hào)指示器、Native方法信息
  • 屬于線程私有數(shù)據(jù)區(qū)域,不存在線程安全問題

JMM與Java內(nèi)存區(qū)域劃分(即Java內(nèi)存結(jié)構(gòu))是不同的概念層次:

  • JMM描述的是一組規(guī)則,通過這組控制程序中各個(gè)變量在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域的訪問方式,JMM是圍繞原子性、有序性及可見性展開的
  • 兩者相似點(diǎn):存在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域

主內(nèi)存與工作內(nèi)存的數(shù)據(jù)存儲(chǔ)類型以及操作方式歸納:

  • 方法里的基本數(shù)據(jù)類型本地變量將直接存儲(chǔ)在工作內(nèi)存的棧幀結(jié)構(gòu)中
  • 引用類型的本地變量,則是其引用存儲(chǔ)在工作內(nèi)存中,而具體的實(shí)例存儲(chǔ)在主內(nèi)存中
  • 對(duì)象的成員變量、static變量、類信息均會(huì)被存儲(chǔ)在主內(nèi)存中
  • 主內(nèi)存共享的方式是線程各拷貝一份數(shù)據(jù)到工作內(nèi)存,操作完成后刷新回主內(nèi)存

JMM如何解決可見性問題:
Java多線程與并發(fā)筆記

指令重排序需要滿足的條件:

  • 在單線程環(huán)境下不能改變程序運(yùn)行的結(jié)果
  • 存在數(shù)據(jù)依賴關(guān)系的不允許重排序
  • 以上兩點(diǎn)可以歸結(jié)為:無法通過happens-before原則推導(dǎo)出來的,才能進(jìn)行指令的重排序

什么是Java內(nèi)存模型中的happens-before:

  • 如果兩個(gè)操作不滿足下述任意一個(gè)happens-before原則,那么這兩個(gè)操作就沒有順序的保障,JVM可以對(duì)這兩個(gè)操作進(jìn)行重排序
  • 如果操作A happens-before 操作B,那么操作A在內(nèi)存上所做的操作對(duì)操作B都是可見的
  • 若A操作的結(jié)果需要對(duì)B操作可見,則A與B存在happens-before關(guān)系

happens-before的八大原則:

  1. 程序次序規(guī)則:一個(gè)線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作
  2. 鎖定規(guī)則:一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作
  3. volatile變量規(guī)則:對(duì)一個(gè)變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作(保證了可見性)
  4. 傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C
  5. 線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作
  6. 線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生
  7. 線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測(cè),我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行
  8. 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開始

volatile:

  • 是JVM提供的輕量級(jí)同步機(jī)制
  • JVM保證被volatile修飾的共享變量對(duì)所有線程總是可見的
  • 禁止指令的重排序優(yōu)化
  • 使用volatile不能保證線程安全,需要變量的操作滿足原子性

volatile變量為何立即可見?簡(jiǎn)單來說:

  • 當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存中的共享變量值刷新到主內(nèi)存中
  • 當(dāng)讀取一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存置為無效,那么就需要從主內(nèi)存中重新讀取該變量

volatile變量如何禁止重排序優(yōu)化:

  • 對(duì)此我們需要先了解內(nèi)存屏障(Memory Barrier),其作用有二:
    1. 保證特定操作的執(zhí)行順序
    2. 保證某些變量的內(nèi)存可見性
  • 通過插入內(nèi)存屏障指令來禁止對(duì)內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化
  • 強(qiáng)制刷出各種CPU的緩存數(shù)據(jù),因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本

volatile和synchronized的區(qū)別:

  1. volatile本質(zhì)是在告訴JVM當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀?。籹ynchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住直到該線程完成變量操作為止
  2. volatile僅能使用在變量級(jí)別;synchronized則可以使用在變量、方法和類級(jí)別
  3. volatile僅能實(shí)現(xiàn)變量的修改可見性,不能保證原子性;而synchronized則可以保證變量修改的可見性和原子性
  4. volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞
  5. volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化

CAS

CAS(Compare and Swap)是一種線程安全性的方法:

  • 支持原子更新操作,適用于計(jì)數(shù)器,序列發(fā)生器等場(chǎng)景
  • 屬于樂觀鎖機(jī)制,號(hào)稱lock-free
  • CAS操作失敗時(shí)由開發(fā)者決定是繼續(xù)嘗試,還是執(zhí)行別的操作

CAS思想:

  • 包含三個(gè)操作數(shù):內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)

CAS多數(shù)情況下對(duì)開發(fā)者來說是透明的:

  • J.U.C的atomic包提供了常用的原子性數(shù)據(jù)類型以及引用、數(shù)組等相關(guān)原子類型和更新操作工作,是很多線程安全程序的首選
  • Unsafe類雖然提供CAS服務(wù),但因能夠操縱任意內(nèi)存地址讀寫而有隱患
  • Java9以后,可以使用Variable Handle API來代替Unsafe

缺點(diǎn):

  • 若循環(huán)時(shí)間長(zhǎng),則開銷很大
  • 只能保證一個(gè)共享變量的原子操作
  • 存在ABA問題,可以通過使用AtomicStampedReference來解決,但由于是通過版本標(biāo)記來解決所以存在一定程度的性能損耗

Java線程池

利用Executors創(chuàng)建不同的線程池滿足不同場(chǎng)景的需求:

  1. newFixedThreadPool(int nThreads):指定工作線程數(shù)量的線程池
  2. newCachedThreadPool():處理大量短時(shí)間工作任務(wù)的線程池,特點(diǎn):
  3. 試圖緩存線程并重用,當(dāng)無緩存線程可用時(shí),就會(huì)創(chuàng)建新的工作線程
  4. 如果線程閑置的時(shí)間超過閾值,則會(huì)被終止并移出緩存
  5. 系統(tǒng)長(zhǎng)時(shí)間閑置的時(shí)候,不會(huì)消耗什么資源
  6. newSingleThreadExecutor():創(chuàng)建唯一的工作者線程來執(zhí)行任務(wù),如果線程異常結(jié)束,會(huì)有另一個(gè)線程取代它
  7. newSingleThreadScheduledExecutor()與newScheduledThreadPool(int corePoolSize):定時(shí)或者周期性的工作調(diào)度,兩者的區(qū)別在于單一工作線程還是多個(gè)線程
  8. JDK8新增的newWorkStealingPool():內(nèi)部會(huì)構(gòu)建ForkJoinPool ,利用working-stealing算法,并行地處理任務(wù),不保證處理順序
    • working-stealing算法:某個(gè)線程從其他線程的任務(wù)隊(duì)列里竊取任務(wù)來執(zhí)行

Fork/Join框架(JDK7提供):

  • 是一個(gè)可以把大任務(wù)分割成若干個(gè)小任務(wù)并行執(zhí)行,最終匯總每個(gè)小任務(wù)結(jié)果后得到大任務(wù)結(jié)果的框架

Java多線程與并發(fā)筆記

為什么要使用線程池:

  1. 減低資源消耗,避免頻繁地創(chuàng)建和銷毀線程
  2. 提高線程的可管理性,例如可控的線程數(shù)量,線程狀態(tài)的監(jiān)控和統(tǒng)一創(chuàng)建/銷毀線程

Executor的框架:
Java多線程與并發(fā)筆記

J.U.C的三個(gè)Executor接口:

  • Executor:運(yùn)行新任務(wù)的簡(jiǎn)單接口,將任務(wù)提交和任務(wù)執(zhí)行細(xì)節(jié)解耦
  • ExecutorService:具備管理執(zhí)行器和任務(wù)生命周期的方法,提交任務(wù)機(jī)制更完善
  • ScheduleExecutorService:支持Future和定期執(zhí)行任務(wù)

線程池執(zhí)行任務(wù)流程圖:
Java多線程與并發(fā)筆記

ThreadPoolExecutor的七個(gè)構(gòu)造器參數(shù):

  • int corePoolSize:核心線程數(shù)
  • int maximumPoolSize:最大線程數(shù)
  • long keepAliveTime:線程空閑存活時(shí)間
  • TimeUnit unit:存活時(shí)間的單位
  • BlockingQueue&lt;Runnable&gt; workQueue:任務(wù)等待隊(duì)列
  • ThreadFactory threadFactory:線程創(chuàng)建工廠,用于創(chuàng)建新線程
  • RejectedExecutionHandler handler:任務(wù)拒絕策略
    • AbortPolicy:直接拋出異常,這是默認(rèn)策略
    • CallerRunsPolicy:使用調(diào)用者所在的線程來執(zhí)行任務(wù)
    • DiscardOldestPolicy:丟棄隊(duì)列中最靠前的任務(wù),并執(zhí)行當(dāng)前任務(wù)
    • DiscardPolicy:直接丟棄提交的任務(wù)
    • 另外可以實(shí)現(xiàn)RejectedExecutionHandler接口來自定義handler

新任務(wù)提交execute執(zhí)行后的判斷:

  • 如果運(yùn)行的線程少于corePoolSize ,則創(chuàng)建新線程來處理任務(wù),即使線程池中的其他線程是空閑的;
  • 如果線程池中的線程數(shù)量大于等于corePoolSize且小于maximumPoolSize,則只有當(dāng)workQueue滿時(shí)才創(chuàng)建新的線程去處理任務(wù);
  • 如果設(shè)置的corePoolSize和maximumPoolSize相同,則創(chuàng)建的線程池的大小是固定的,這時(shí)如果有新任務(wù)提交,若workQueue未滿,則將請(qǐng)求放入workQueue中,等待有空閑的線程去從workQueue中取任務(wù)并處理;
  • 如果運(yùn)行的線程數(shù)量大于等于maximumPoolSize,這時(shí)如果workQueue已經(jīng)滿了,則通過handler所指定的策略來處理任務(wù);

execute執(zhí)行流程圖:
Java多線程與并發(fā)筆記

線程池的狀態(tài):

  • RUNNING:能接受新提交的任務(wù),并且也能處理阻塞隊(duì)列中的任務(wù)
  • SHUTDOWN:不再接受新提交的任務(wù),但可以處理存量任務(wù)(調(diào)用shutdown方法)
  • STOP:不再接受新提交的任務(wù),也不處理存量任務(wù)(調(diào)用shutdownNow方法)
  • TIDYING:所有的任務(wù)都已終止
  • TERMINATED:terminated() 方法執(zhí)行完后進(jìn)入該狀態(tài)

線程池狀態(tài)轉(zhuǎn)換圖:
Java多線程與并發(fā)筆記

線程池中工作線程的生命周期:
Java多線程與并發(fā)筆記

關(guān)于線程池大小如何選定參考:

  • CPU密集型任務(wù):線程數(shù) = 按照CPU核心數(shù)或者CPU核心數(shù) + 1設(shè)定
  • I/O密集型任務(wù):線程數(shù) = CPU核心數(shù) * (1 + 平均等待時(shí)間 / 平均工作時(shí)間)
向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI