5. 共享模型之内存
5.1 Java 内存模型
java 内存模型,简称 JMM,包括主存(线程共享)和工作内存(线程私有)
5.2 可见性
package com.rui.jmm;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test1")
public class Test1 {
static Boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (run) {
}
}, "t").start();
Thread.sleep(1000);
run = false;
log.debug("run = {}", run);
}
}
// 某次运行结果
16:12:03 [main] c.Test1 - run = false
为何在主线程中将 run 的值修改为 false 后,t 线程并未运行结束?
1. t 线程读取主存中 run 的值到其工作内存中;
2. 因 t 线程频繁向主存中读取 run 的值,故即时编译器(JIT 编译器)会将主存中 run 的值缓存在其工作内存的高速缓存中;
3. t 线程不在向主存中读取 run 的值,转为向即时编译器工作内存的高速缓存中读取 run 的值;
4. 1 秒后,主线程将其工作线程中 run 的值改为 false 并同步到主存中,但因【3】,故 t 线程未运行结束。
那么该如何解决上述问题呢?
这里提供两种解决方法。
volatile
package com.rui.jmm;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test1")
public class Test1 {
static volatile Boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (run) {
}
}, "t").start();
Thread.sleep(1000);
run = false;
log.debug("run = {}", run);
}
}
// 运行结果
16:43:19 [main] c.Test1 - run = false
进程已结束,退出代码 0
使用 volatile 关键字修饰成员变量和类变量(静态变量)可强制线程仅能从主存中读取变量的值
volatile 关键字可以用来修饰局部变量吗?
因局部变量存储在工作内存中,故而无需从主存中读取,也就无需被 volatile 关键字修饰了。
局部变量存储在 JVM - 运行时数据区 - Java 虚拟机栈 - 栈帧 - 局部变量表中,属线程私有。
synchronized
package com.rui.jmm;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test2")
public class Test2 {
static Boolean run = true;
static Object o = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (o) {
while (true) {
if (!run) {
break;
}
}
}
}, "t").start();
synchronized (o) {
Thread.sleep(1000);
run = false;
log.debug("run = {}", run);
}
}
}
// 运行结果
16:54:24 [main] c.Test2 - run = false
进程已结束,退出代码 0
volatile 和 synchronized 两种解决方法的区别
volatile | synchronized | |
可见性 | √ | √ |
原子性 | × | √ |
较轻量级 | 重量级 |
5.3 两阶段终止模式
package com.rui.jmm;
import lombok.extern.slf4j.Slf4j;
public class Test4 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(5000);
tpt.end();
}
}
@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
private Thread t1;
private volatile Boolean stop = false;
public void start() {
t1 = new Thread("t1") {
@Override
public void run() {
while (true) {
if (stop) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("无异常,执行监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
}
public void end() {
stop = true;
t1.interrupt();
}
}
// 某次运行结果
18:35:20 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:35:21 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:35:22 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:35:24 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.rui.jmm.TwoPhaseTermination$1.run(Test4.java:29)
18:35:24 [t1] c.TwoPhaseTermination - 料理后事
进程已结束,退出代码 0
5.4 犹豫模式(Balking)
package com.rui.jmm;
import lombok.extern.slf4j.Slf4j;
public class Test5 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination5 tpt = new TwoPhaseTermination5();
tpt.start();
tpt.start();
tpt.start();
Thread.sleep(5000);
tpt.end();
}
}
@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination5 {
private Thread t1;
private volatile Boolean stop = false;
public void start() {
t1 = new Thread("t1") {
@Override
public void run() {
while (true) {
if (stop) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("无异常,执行监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
}
public void end() {
stop = true;
t1.interrupt();
}
}
// 某次运行结果
18:52:51 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:51 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:51 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:52 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:52 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:52 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:53 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:53 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:53 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:54 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:54 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:54 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.rui.jmm.TwoPhaseTermination5$1.run(Test5.java:31)
18:52:55 [t1] c.TwoPhaseTermination - 料理后事
18:52:55 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:55 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
18:52:55 [t1] c.TwoPhaseTermination - 料理后事
18:52:55 [t1] c.TwoPhaseTermination - 料理后事
如何避免同一时间同一线程多次启动呢?
package com.rui.jmm;
import lombok.extern.slf4j.Slf4j;
public class Test5 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination5 tpt = new TwoPhaseTermination5();
tpt.start();
tpt.start();
tpt.start();
Thread.sleep(5000);
tpt.end();
}
}
@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination5 {
private Thread t1;
private volatile Boolean stop = false;
private Boolean starting = false;
public void start() {
t1 = new Thread("t1") {
@Override
public void run() {
synchronized (this) {
if (starting) {
return;
}
starting = true;
}
while (true) {
if (stop) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("无异常,执行监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
}
public void end() {
stop = true;
t1.interrupt();
}
}
// 某次运行结果
19:04:21 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
19:04:22 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
19:04:23 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
19:04:24 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
19:04:25 [t1] c.TwoPhaseTermination - 无异常,执行监控记录
19:04:25 [t1] c.TwoPhaseTermination - 料理后事
进程已结束,退出代码 0
5.5 有序性
指令重排:JVM 会在不影响(单线程)正确性的前提下调整语句的执行顺序
指令重排可能会影响多线程正确性
// 符合指令重排条件
int a = 1;
int b = 2;
// 不符合指令重排条件
int c = 3;
int d = c + 1;
5.6 内存屏障
- 可见性
在读屏障之后对共享变量的读取均来自主存
在写屏障之前对共享变量的修改同步到主存
- 有序性
语句的执行顺序:
读/写屏障之前的语句 -> 读/写屏障之后的语句
5.7 volatile 原理
volatile 的底层原理是 内存屏障
使用 volatile 修饰共享变量后,读取该变量的语句后会设置读屏障,修改该变量的语句前会设置写屏障。
// 禁止指令重排
int a = 1;
volatile int b = 2;
为何仅 b 变量被 volatile 关键字修饰即可禁止指令重排
// 禁止指令重排
int a = 1;
// --- 写屏障 ---
volatile int b = 2;
double - checked locking(双检索)
以单例模式为例
package com.rui.jmm;
public final class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
假设多个线程同时执行 getInstance 方法,那么这些线程皆需获取锁,该如何优化呢?
package com.rui.jmm;
public final class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
可能先执行 return INSTANCE; 语句(指令重排),如何解决这个问题呢?
package com.rui.jmm;
public final class Singleton {
private Singleton() {
}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
//写屏障
INSTANCE = new Singleton();
}
//读屏障
}
}
// 读屏障
return INSTANCE;
}
}
5.8 happens - before
synchronized:线程拥有对象锁时对共享变量的修改 对 该线程释放该对象锁后,其他线程拥有对象锁时对共享变量的读取 可见
package com.rui.jmm;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test6")
public class Test6 {
static int count = 1;
static Object o = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (o) {
count = 10;
}
}, "t1").start();
new Thread(() -> {
synchronized (o) {
System.out.println(count);
}
}, "t2").start();
}
}
// 运行结果
10
进程已结束,退出代码 0
volatile:使用 volatile 关键字修饰共享变量后,线程对共享变量的修改 对 之后其他线程对共享变量的读取 可见
package com.rui.jmm;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test7")
public class Test7 {
static volatile int count = 1;
public static void main(String[] args) {
new Thread(() -> {
count = 10;
}, "t1").start();
new Thread(() -> {
System.out.println(count);
}, "t2").start();
}
}
// 运行结果
10
进程已结束,退出代码 0
如果对共享变量的修改发生在线程的初始状态,那么该修改 对 就绪状态及其以后的该线程 可见
package com.rui.jmm;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test8")
public class Test8 {
static int count = 1;
public static void main(String[] args) {
count = 5;
new Thread(() -> {
System.out.println(count);
}, "t").start();
}
}
// 运行结果
5
进程已结束,退出代码 0
终止状态前的线程对共享变量的修改 对 该线程运行结束后,其他线程对共享变量的读取 可见
package com.rui.jmm;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test9")
public class Test9 {
static int count = 1;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
count = 10;
}, "t");
t.start();
t.join();
System.out.println(count);
}
}
// 运行结果
10
进程已结束,退出代码 0
t1 线程打断 t2 线程前对共享变量的修改 对 该修改后,其他线程对共享变量的读取 可见
package com.rui.jmm;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test10")
public class Test10 {
static int count = 1;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
count = 5;
t1.interrupt();
}, "t2");
t1.start();
t2.start();
while (true) {
if (t1.isInterrupted()) {
System.out.println(count);
break;
}
}
}
}
// 运行结果
5
进程已结束,退出代码 0
对默认初始化的共享变量的修改 对 修改后,线程对共享变量的读取 可见
内存屏障
package com.rui.jmm;
public class Test12 {
static volatile int x = 1;
static int y = 5;
public static void main(String[] args) {
new Thread(() -> {
y = 1;
x = 5;
}).start();
new Thread(() -> {
System.out.println("x = " + x);
System.out.println("y = " + y);
}).start();
}
}
// 运行结果
x = 5
y = 1
进程已结束,退出代码 0
说些废话
本篇文章为博主日常学习记录,故而会小概率存在各种错误,若您在浏览过程中发现一些,请在评论区指正,望我们共同进步,谢谢!