并發(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寄存器),即上下文切換
線程的生命周期

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
解決方案
線程同步synchronized、JUC的鎖
volatile保證變量可見性
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í)行順序

多線程版本
由于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)行傳遞

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保證了原子性??梢娦?、有序性,保證了線程安全
修飾不同方法和代碼塊鎖定范圍?
修飾代碼塊:鎖給定對象
修飾非靜態(tài)方法:鎖當(dāng)前對象
修飾靜態(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操作

JDK1.6對synchronized的鎖優(yōu)化
同步鎖狀態(tài):無鎖、偏向鎖、輕量級鎖、重量級鎖
鎖優(yōu)化技術(shù):適應(yīng)性自旋、鎖消除、鎖膨脹、輕量級鎖、偏向鎖
鎖信息存儲在對象頭的標(biāo)記字段(MarkWord)
JVM會分析逐步升級鎖,加鎖后獲取偏向鎖(消除同一線程的后續(xù)的同步措施),失敗后獲取輕量級鎖,失敗后循環(huán)自旋加鎖,失敗后膨脹為重量級鎖,失敗后循環(huán)自旋加鎖,失敗后OS層面掛起
!

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