并發(fā)編程-JMM

并發(fā)編程-JMM

Q&A

什么是多線程并發(fā)編程?

多線程編程中,線程個數(shù)往往多于CPU核數(shù)

為什么要進(jìn)行多線程并發(fā)編程?

多核CPU時代,隨著對應(yīng)用性能和吞吐量要求提高,出現(xiàn)海量數(shù)據(jù)和請求的要求,高性能應(yīng)用程序中需要并發(fā)編程提升硬件利用率來提高整體處理性能

多線程基本概念

進(jìn)程與線程

進(jìn)程是代碼在數(shù)據(jù)集合上的一次運(yùn)行活動,是系統(tǒng)運(yùn)行程序的基本單位,是系統(tǒng)資源分配和調(diào)度的基本單位。線程是進(jìn)程中的一個實(shí)體(執(zhí)行單元),一個進(jìn)程至少有一個線程。

進(jìn)程擁有獨(dú)立的內(nèi)存空間,進(jìn)程間是獨(dú)立的,線程共享進(jìn)程的內(nèi)存空間

CPU資源是分配到線程的,所以線程是CPU調(diào)度的基本單位

線程間堆和方法區(qū)是共享的,線程棧和程序計數(shù)器是獨(dú)立的

并發(fā)與并行

并發(fā)是單位時間內(nèi),多個線程任務(wù)根據(jù)CPU時間片分配依次執(zhí)行,并不一定是同時執(zhí)行

并行是單位時間內(nèi),多個線程任務(wù)同時執(zhí)行,并行上限取決于CPU核數(shù)

線程上下文切換

并發(fā)編程中線程數(shù)一般大于CPU核數(shù),所以每個CPU同一時刻只能被一個線程使用,為了讓用戶感受在同時執(zhí)行,采用搶占式時間片輪轉(zhuǎn)策略,而線程CPU時間片用完、主動讓出或者被中斷,其他線程使用CPU,期間存在線程執(zhí)行現(xiàn)場的存儲和恢復(fù)操作(程序計數(shù)器和CPU寄存器),即上下文切換

線程的生命周期

線程生命周期.png

wait與sleep的區(qū)別

wait方法釋放鎖,sleep方法不釋放鎖

wait方法屬于Object類,sleep方法屬于Thread類

wait方法可指定時間也可不指定時間,調(diào)用notify、notifyAll方法喚醒

sleep方法必須指定時間,自動蘇醒,蘇醒后處于Ready狀態(tài)等待CPU時間片執(zhí)行

線程安全問題

多個線程同時讀寫一個共享資源并且在沒有任何同步措施下,導(dǎo)致出現(xiàn)臟數(shù)據(jù)和其他不可預(yù)見結(jié)果的問題

賣票案例

public class TicketDemo {

    public static void main(String[] args) throws InterruptedException {
        // 三個售票員線程共享100張票,模擬賣票
        SellTicketTask sellTicketTask = new SellTicketTask();
        Thread thread1 = new Thread(sellTicketTask);
        Thread thread2 = new Thread(sellTicketTask);
        Thread thread3 = new Thread(sellTicketTask);

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        thread1.start();
        thread2.start();
        thread3.start();
        thread1.join();
        thread2.join();
        thread3.join();
        stopWatch.stop();
        System.out.println(stopWatch.getTotalTimeMillis());
    }


    static class SellTicketTask implements Runnable {
        private int ticketNum = 50;

        @Override
        public void run() {
            while (ticketNum > 0) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "正在賣:" + ticketNum--);
            }
        }
    }
}
D:\Java\jdk1.8.0_144\bin\java.exe 
// 存在大量重復(fù)賣同一張票的線程,因?yàn)槿肿兞考办o態(tài)變量共享引起
Thread-2正在賣:50
Thread-0正在賣:50
Thread-1正在賣:49
Thread-1正在賣:48
Thread-2正在賣:48
Thread-0正在賣:48
Thread-0正在賣:47
Thread-2正在賣:46
Thread-1正在賣:46
Thread-1正在賣:45
Thread-0正在賣:44
Thread-2正在賣:43
Thread-2正在賣:42
Thread-0正在賣:41
Thread-1正在賣:42
Thread-1正在賣:40
Thread-2正在賣:40
Thread-0正在賣:40
Thread-0正在賣:39
Thread-2正在賣:38
Thread-1正在賣:39
Thread-1正在賣:37
Thread-2正在賣:36
Thread-0正在賣:35
Thread-1正在賣:34
Thread-0正在賣:33
Thread-2正在賣:32
Thread-1正在賣:31
Thread-2正在賣:31
Thread-0正在賣:30
Thread-0正在賣:29
Thread-1正在賣:27
Thread-2正在賣:28
Thread-1正在賣:26
Thread-2正在賣:25
Thread-0正在賣:24
Thread-0正在賣:23
Thread-1正在賣:21
Thread-2正在賣:22
Thread-1正在賣:20
Thread-0正在賣:20
Thread-2正在賣:20
Thread-0正在賣:19
Thread-1正在賣:18
Thread-2正在賣:18
Thread-0正在賣:17
Thread-2正在賣:17
Thread-1正在賣:16
Thread-2正在賣:15
Thread-0正在賣:15
Thread-1正在賣:15
Thread-2正在賣:14
Thread-1正在賣:14
Thread-0正在賣:14
Thread-1正在賣:13
Thread-0正在賣:13
Thread-2正在賣:13
Thread-0正在賣:12
Thread-1正在賣:10
Thread-2正在賣:11
Thread-1正在賣:8
Thread-2正在賣:7
Thread-0正在賣:9
Thread-2正在賣:6
Thread-1正在賣:6
Thread-0正在賣:5
Thread-2正在賣:4
Thread-1正在賣:3
Thread-0正在賣:2
Thread-1正在賣:1
Thread-2正在賣:0
Thread-0正在賣:1
726

Process finished with exit code 0

解決方案

  1. 線程同步synchronized、JUC的鎖

  2. volatile保證變量可見性

  3. JUC的原子類

    static class SellTicketTask implements Runnable {
        private int ticketNum = 50;

        @Override
        public void run() {
            synchronized (this) {
                while (ticketNum > 0) {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣:" + ticketNum--);
                }
            }
        }
    }
D:\Java\jdk1.8.0_144\bin\java.exe 
// 線程安全但耗時增加
Thread-0正在賣:50
Thread-0正在賣:49
Thread-0正在賣:48
Thread-0正在賣:47
Thread-0正在賣:46
Thread-0正在賣:45
Thread-0正在賣:44
Thread-0正在賣:43
Thread-0正在賣:42
Thread-0正在賣:41
Thread-0正在賣:40
Thread-0正在賣:39
Thread-0正在賣:38
Thread-0正在賣:37
Thread-0正在賣:36
Thread-0正在賣:35
Thread-0正在賣:34
Thread-0正在賣:33
Thread-0正在賣:32
Thread-0正在賣:31
Thread-0正在賣:30
Thread-0正在賣:29
Thread-0正在賣:28
Thread-0正在賣:27
Thread-0正在賣:26
Thread-0正在賣:25
Thread-0正在賣:24
Thread-0正在賣:23
Thread-0正在賣:22
Thread-0正在賣:21
Thread-0正在賣:20
Thread-0正在賣:19
Thread-0正在賣:18
Thread-0正在賣:17
Thread-0正在賣:16
Thread-0正在賣:15
Thread-0正在賣:14
Thread-0正在賣:13
Thread-0正在賣:12
Thread-0正在賣:11
Thread-0正在賣:10
Thread-0正在賣:9
Thread-0正在賣:8
Thread-0正在賣:7
Thread-0正在賣:6
Thread-0正在賣:5
Thread-0正在賣:4
Thread-0正在賣:3
Thread-0正在賣:2
Thread-0正在賣:1
1517

Process finished with exit code 0
    static class SellTicketTask implements Runnable {
        private int ticketNum = 50;
        private Lock lock = new ReentrantLock();

        @Override
        public void run() {
            lock.lock();
            try {
                while (ticketNum > 0) {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣:" + ticketNum--);
                }
            } finally {
                lock.unlock();
            }
        }
    }
D:\Java\jdk1.8.0_144\bin\java.exe 
Thread-0正在賣:50
Thread-0正在賣:49
Thread-0正在賣:48
Thread-0正在賣:47
Thread-0正在賣:46
Thread-0正在賣:45
Thread-0正在賣:44
Thread-0正在賣:43
Thread-0正在賣:42
Thread-0正在賣:41
Thread-0正在賣:40
Thread-0正在賣:39
Thread-0正在賣:38
Thread-0正在賣:37
Thread-0正在賣:36
Thread-0正在賣:35
Thread-0正在賣:34
Thread-0正在賣:33
Thread-0正在賣:32
Thread-0正在賣:31
Thread-0正在賣:30
Thread-0正在賣:29
Thread-0正在賣:28
Thread-0正在賣:27
Thread-0正在賣:26
Thread-0正在賣:25
Thread-0正在賣:24
Thread-0正在賣:23
Thread-0正在賣:22
Thread-0正在賣:21
Thread-0正在賣:20
Thread-0正在賣:19
Thread-0正在賣:18
Thread-0正在賣:17
Thread-0正在賣:16
Thread-0正在賣:15
Thread-0正在賣:14
Thread-0正在賣:13
Thread-0正在賣:12
Thread-0正在賣:11
Thread-0正在賣:10
Thread-0正在賣:9
Thread-0正在賣:8
Thread-0正在賣:7
Thread-0正在賣:6
Thread-0正在賣:5
Thread-0正在賣:4
Thread-0正在賣:3
Thread-0正在賣:2
Thread-0正在賣:1
1504

Process finished with exit code 0

同步控制后耗時增加,但從結(jié)果看,ReentrantLock耗時與synchronized不相上下

得益于jvm對synchronized一系列鎖優(yōu)化措施

多線程并發(fā)的特性

原子性:類似于事務(wù)的原子性,要么全部執(zhí)行,要么全部不執(zhí)行

有序性:程序代碼按順序執(zhí)行(存在指令重排)

可見性:任何線程對共享變量的修改其他線程可見(由于Java內(nèi)存模型JMM存在)

有序性

什么是指令重排序?

編譯器和處理器在不影響輸出結(jié)果前提下,為了提升程序運(yùn)行效率進(jìn)行的優(yōu)化,調(diào)整指令運(yùn)行順序

因?yàn)镃PU雖然是多核,但運(yùn)行進(jìn)程和線程是遠(yuǎn)多于核心數(shù)的,所以使用CPU時間片調(diào)度,指令流水線是間隔一個單位時間并行走的,如果前后兩條指令存在關(guān)聯(lián),第二條指令執(zhí)行(EX)需等待第一條指令寫回寄存器之后才可以,導(dǎo)致浪費(fèi)一個單位時間,可以通過先執(zhí)行不相關(guān)的一條指令后再執(zhí)行第二條指令來充分利用資源

int a = 2;//1
int b = 1 +a;//2
int c = 1;//3

由于3與12沒有關(guān)聯(lián),1必須在2之前,所以可能會出現(xiàn)312/132/123多種執(zhí)行順序

指令流水線.png

多線程版本

由于num與ready賦值沒有關(guān)聯(lián),所以可能出現(xiàn)0和4兩種輸出情況

public class ReOrderInstruction {
    private static int num = 0;
    private static boolean ready = false;

    public static void main(String[] args) throws InterruptedException {
        ReadThread readThread = new ReadThread();
        WriteThread writeThread = new WriteThread();

        readThread.start();
        writeThread.start();
        Thread.sleep(5);
        readThread.interrupt();
        // 可能會出現(xiàn)輸出0
        System.out.println("main done");
    }

    static class ReadThread extends Thread{
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                if (ready) {
                    System.out.println("read:" +(num + num));
                }
                System.out.println("read is done");
            }
        }
    }

    static class WriteThread extends Thread{
        @Override
        public void run() {
            num = 2;
            ready = true;
            System.out.println("write is done");
        }
    }
}

可見性

JMM內(nèi)存模型

JMM決定了共享變量何時寫入,何時對其它線程可見

線程之間的共享變量存儲在主內(nèi)存

每個線程有一個私有本地內(nèi)存,本地內(nèi)存存儲共享變量副本

本地內(nèi)存是抽象的

線程操作共享變量必須讀取到本地內(nèi)存中,不能直接操作主內(nèi)存

線程間無法直接訪問其他線程的本地內(nèi)存,需要通過主內(nèi)存進(jìn)行傳遞

JMM.png

volatile保證了修改后的共享變量新值立即同步到主內(nèi)存,對于其他線程可見,使用該共享變量前立即從主內(nèi)存刷新共享變量新值到自己的本地內(nèi)存,保證了多線程操作共享變量可見性

JMM內(nèi)存天然的現(xiàn)行發(fā)生原則(Happens-before)
  • 程序順序原則:一個線程內(nèi),書寫在前的操作先行發(fā)生于書寫在后面的操作

  • 管程鎖定規(guī)則:一個unlock操作先行發(fā)生于后面同一個鎖的lock操作

  • Volatile變量規(guī)則:一個volatile變量的寫操作先行發(fā)生于該變量的讀操作

  • 傳遞性原則:A先行發(fā)生于B,B先行發(fā)生于C,那么A先行發(fā)生于C

  • 線程啟動規(guī)則:線程啟動先于線程所有操作

  • 線程終止規(guī)則:線程所有操作先于線程終止

  • 線程中斷規(guī)則:線程中斷的調(diào)用先于線程代碼中斷檢測

  • 對象終結(jié)規(guī)則:對象初始化先于對象終結(jié)finalize();

synchronized

保證方法和代碼塊在多線程環(huán)境運(yùn)行,同一時刻只有一個線程執(zhí)行代碼

JDK1.6之前,synchronized底層實(shí)現(xiàn)依賴OS級別互斥鎖MuteLock,存在嚴(yán)重性能問題

JDK1.6之后,synchronized實(shí)現(xiàn)改為管程(Monitor),并進(jìn)行一系列優(yōu)化,性能與JUC的lock不相上下,只是API能力無法滿足場景,例如線程間通信lock的condition

synchronized保證了原子性??梢娦?、有序性,保證了線程安全

修飾不同方法和代碼塊鎖定范圍?

  1. 修飾代碼塊:鎖給定對象

  2. 修飾非靜態(tài)方法:鎖當(dāng)前對象

  3. 修飾靜態(tài)方法:鎖當(dāng)前類對象(字節(jié)碼對象/class對象)

如何解決可見性?

  • Happens-before規(guī)則

  • JMM中線程對共享變量操作規(guī)定

如何實(shí)現(xiàn)同步?

通過monitorenter與monitorexit jvm指令實(shí)現(xiàn),即管程(Monitor)

public class SynchronizedDemo {
    public static void main(String[] args) {

    }

    public void sync1(){
        synchronized (this) {
            int a = 1;
        }
    }

    public synchronized void sync2() {
        int a = 1;
    }

    public static synchronized void sync3() {
        int a = 1;
    }
}
D:\Java\jdk1.8.0_144\bin\javap.exe -c com.zhaoccf.study.juc.SynchronizedDemo
Compiled from "SynchronizedDemo.java"
public class com.zhaoccf.study.juc.SynchronizedDemo {
  public com.zhaoccf.study.juc.SynchronizedDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: return

  public void sync1();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter //對應(yīng)Monitor的lock
       4: iconst_1
       5: istore_2
       6: aload_1
       7: monitorexit //對應(yīng)Monitor的unlock
       8: goto          16
      11: astore_3
      12: aload_1
      13: monitorexit //編譯器會為同步塊添加一個隱式的try-finally,在finally中會調(diào)用monitorexit命令釋放鎖
      14: aload_3
      15: athrow
      16: return
    Exception table:
       from    to  target type
           4     8    11   any
          11    14    11   any

  public synchronized void sync2();
    Code:
       0: iconst_1
       1: istore_1
       2: return

  public static synchronized void sync3();
    Code:
       0: iconst_1
       1: istore_0
       2: return
}

Process finished with exit code 0

何為管程(Monitor)?

管理共享變量即線程對共享變量操作的過程

Java所有對象都可以作為鎖傳入,因每個對象都有一管程與之關(guān)聯(lián)

使用synchronized,JVM會自動加入兩個指令monitorenter和monitorexit,對應(yīng)Monitor的就是lock和unlock操作

Monitor.png

JDK1.6對synchronized的鎖優(yōu)化

同步鎖狀態(tài):無鎖、偏向鎖、輕量級鎖、重量級鎖

鎖優(yōu)化技術(shù):適應(yīng)性自旋、鎖消除、鎖膨脹、輕量級鎖、偏向鎖

鎖信息存儲在對象頭的標(biāo)記字段(MarkWord)

JVM會分析逐步升級鎖,加鎖后獲取偏向鎖(消除同一線程的后續(xù)的同步措施),失敗后獲取輕量級鎖,失敗后循環(huán)自旋加鎖,失敗后膨脹為重量級鎖,失敗后循環(huán)自旋加鎖,失敗后OS層面掛起

!
JVM對象頭MarkWord.png
偏向鎖、輕量級鎖、重量級鎖狀態(tài)轉(zhuǎn)化即對象MarkWord關(guān)系.png

Volatile

Java內(nèi)存語義保證線程可見性,禁止指令重排序

寫入變量時,把寫入本地內(nèi)存的變量同步到內(nèi)存,讀取變量時,清空本地內(nèi)存,從主內(nèi)存刷新新值

無法保證原子性

實(shí)現(xiàn)內(nèi)存可見性原理?

內(nèi)存屏障,Java編譯器會根據(jù)內(nèi)存屏障規(guī)則禁止重排序

Volatile寫變量時:在寫操作之后添加一條store屏障指令,讓本地內(nèi)存變量值刷新到主內(nèi)存

  • 在每個Volatile寫前,插入StoreStore屏障

  • 在每個Volatile寫后,插入StoreLoad屏障

Volatile讀變量時:在讀操作之后添加一條load屏障指令,讀取變量主內(nèi)存的值

  • 在每個Volatile讀前,插入LoadLoad屏障

  • 在每個Volatile讀后,插入LoadStore屏障

可見性驗(yàn)證

public class VolatileDemo {
    public static void main(String[] args) throws InterruptedException {
        Task1 task1 = new Task1();
        new Thread(task1).start();
        Task2 task2 = new Task2();
        new Thread(task2).start();

        Thread.sleep(1000);
        task1.flag = false;
        System.out.println("Task1修改為false");
        task2.flag = false;
        System.out.println("Task2修改為false");
    }

    static class Task1 implements Runnable{
        public boolean flag = true;

        @Override
        public void run() {
            System.out.println("Task1開始");
            while (flag) {
            }
            System.out.println("Task1結(jié)束");
        }
    }

    static class Task2 implements Runnable{
        public volatile boolean flag = true;

        @Override
        public void run() {
            System.out.println("Task2開始");
            while (flag) {
            }
            System.out.println("Task2結(jié)束");
        }
    }
}

此時程序未結(jié)束,修改對Task1線程不可見

D:\Java\jdk1.8.0_144\bin\java.exe 
Task1開始
Task2開始
Task1修改為false
Task2修改為false
Task2結(jié)束

字節(jié)碼指令

其中可以看到Volatile修飾的變量,有關(guān)鍵字ACC_VOLATILE

public volatile int num1;
descriptor: I
flags: ACC_PUBLIC, ACC_VOLATILE

public class Demo {

    public int num;
    public volatile int num1;

    public static void main(String[] args) {

    }
}
D:\Java\jdk1.8.0_144\bin\javap.exe -v com.zhaoccf.study.juc.Demo
Classfile /D:/Program/study/thinking-in-java/target/classes/com/zhaoccf/study/juc/Demo.class
  Last modified 2022-9-18; size 462 bytes
  MD5 checksum 68a69b4141743c22628d8c02296cf702
  Compiled from "Demo.java"
public class com.zhaoccf.study.juc.Demo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#21         // java/lang/Object."<init>":()V
   #2 = Class              #22            // com/zhaoccf/study/juc/Demo
   #3 = Class              #23            // java/lang/Object
   #4 = Utf8               num
   #5 = Utf8               I
   #6 = Utf8               num1
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/zhaoccf/study/juc/Demo;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               MethodParameters
  #19 = Utf8               SourceFile
  #20 = Utf8               Demo.java
  #21 = NameAndType        #7:#8          // "<init>":()V
  #22 = Utf8               com/zhaoccf/study/juc/Demo
  #23 = Utf8               java/lang/Object
{
  public int num;
    descriptor: I
    flags: ACC_PUBLIC

  public volatile int num1;
    descriptor: I
    flags: ACC_PUBLIC, ACC_VOLATILE

  public com.zhaoccf.study.juc.Demo();
    descriptor: ()V
    flags: 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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhaoccf/study/juc/Demo;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "Demo.java"

Process finished with exit code 0

原子性驗(yàn)證

不論加與不加volatile,結(jié)果都不為20000,無法保證原子性

public class VolatileDemo {
    public static void main(String[] args) throws InterruptedException {

        Task3 task3 = new Task3();
        new Thread(task3).start();
        new Thread(task3).start();
        Task4 task4 = new Task4();
        new Thread(task4).start();
        new Thread(task4).start();

        Thread.sleep(1000);
        System.out.println(task3.num);
        System.out.println(task4.num);
    }

    static class Task3 implements Runnable{
        public int num;

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                num++;
            }
        }
    }

    static class Task4 implements Runnable{
        public volatile int num;

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                num++;
            }
        }
    }
}
D:\Java\jdk1.8.0_144\bin\java.exe 
15119
13841

Thread.start()JVM實(shí)現(xiàn)源碼解析

TODO

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容