目录
1、Java内存模型
JMM即Java Memory Model,它定义了主存、工作内存抽象概念、底层对应着cpu寄存器、缓存、硬件内存、cpu指令优化等。
JMM体现在以下几个方面
- 原子性:保证指令不会受到线程上下文切换的影响
- 可见性:保证指令不会受cpu缓存的影响
- 有序性:保证指令不会受到cpu指令并行优化的影响
2、可见性
当我们运行以下代码时,按理说1秒后线程会停下来。但是实际却没有停下来,为什么?
public class VisibilityTest {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t =new Thread(() -> {
while (run) {
System.out.println("run...");
}
});
t.start();
Thread.sleep(1000);
System.out.println("停止");
run = false;
}
}
1.初始状态,t线程刚开始从主内存读取了run的值到工作内存。
2.因为t线程要频繁的从主内存中读取run值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率
3.1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。
解决方案: 加上volatile
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存
通过加synchronized也可以解决
class VisibilityTest2 {
volatile static boolean run = true;
final static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t =new Thread(() -> {
while (true) {
synchronized (lock) {
if(!run) {
break;
}
}
}
});
t.start();
Thread.sleep(1000);
System.out.println("停止");
synchronized (lock) {
run = false;
}
}
}
3、可见性 VS 原子性
volatile:保证的是在多个线程之间,一个volatile变量的修改对另一个线程可变,不能保证原子性,仅用在一个写线程,多个读线程的情况。
synchronized:既可以保证代码的原子性,也同时保证代码块内变量的可见性。但是缺点是synchronized是属于重量级操作,性能相对更低
4、终止模式之两线程终止模式
在一个线程T1中如何优雅的终止线程T2?这里的【优雅】指的是给T2一个料理后事的机会
4.1 错误思路
-
使用线程对象的stop方法停止线程
stop方法会真正杀死线程,如果这是线程锁住了共享资源,那么当它被杀死后,就再也没有机会释放锁,其他线程将永远无法获取锁
-
使用System.exit(int) 方法停止线程
目的仅是停止一个线程,但这种做法会让整个程序都停止
4.2 正确思路
package com.sharing_model.visibility;
/**
* 两阶段终止模式 (volatile实现)
*/
public class TwoPhaseTermination {
private Thread monitor;
private volatile boolean stop = false;
//启动监控线程
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (stop) {
System.out.println("料理后事");
break;
}
try {
Thread.sleep(1000);
System.out.println("执行监控");
} catch (InterruptedException e) {
}
}
});
monitor.start();
}
public void stop() {
stop = true;
}
}
class Test {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination t = new TwoPhaseTermination();
t.start();
Thread.sleep(3500);
t.stop();
}
}
5、设计模式——犹豫模式
Balking(犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回。
例如:
package com.sharing_model.visibility;
import java.util.concurrent.TimeUnit;
/**
* 设计模式: 犹豫模式
* 当发现想要做的事被做了就不做了
*/
public class HesitateMode {
public static void main(String[] args) {
TwoPhaseTermination1 tp1 = new TwoPhaseTermination1();
tp1.start();
tp1.start();
}
}
class TwoPhaseTermination1 {
//监控线程
private Thread monitorThread;
//停止标记
private boolean stop = false;
//判断是否执行过start方法
private boolean staring = false;
//自动监控线程
public void start() {
synchronized (this) {
if (staring) {
return;
}
}
staring = true;
monitorThread = new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行监控");
}
},"monitor");
monitorThread.start();
}
}
6、有序性
JVM在不影响正确性的前提下,可以调整语句的执行顺序,思考下面的一段代码:
static int i;
static int j;
//在某个线程内执行如下的赋值操作
i = ...;
j = ...;
可以看到,至于先执行i还是先执行j,对最终的结果不会产生什么影响,所以上面代码最终执行的时候,即可以是
i = ...;
j = ...;
也可以是
j = ...;
i = ...;
这种特性称为指令重排,多线程下的【指令重排】会影响正确性。为什么要有指令重排这项优化?下面从CPU执行指令的原理来理解一下。
在不改变程序的运行结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令集并行,这一技术在80中叶到90中叶占据了计算架构的重要地位。
分阶段,分工是提升效率的关键!
7、volatile原理
1、如何保证可见性
- 写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存中
- 读屏障保证在该屏障之后,对共享变量的读取,加载是主存中最新的数据
2、如何保证有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
8、DCL 单例模式
/**
* 双重校验锁
*/
public class DoubleCheckedLocking {
private DoubleCheckedLocking() {}
private static volatile DoubleCheckedLocking INSTANCE = null;
public static DoubleCheckedLocking getInstance() {
//加if的母的是,每次获取实例的时候都要加速,效率太低了,加if后只有第一次会加锁,这样大大的提高了运行效率
if (INSTANCE == null) {
//加synchronized的目的时为了防止多个线程在操作的时候,虽然第一个线程判断实例为空,但是在前一个线程还没有执行实例的创建的时候,第二个线程也认为实例为空
synchronized (DoubleCheckedLocking.class) {
if (INSTANCE == null) {
INSTANCE = new DoubleCheckedLocking();
}
}
}
return INSTANCE;
}
}
9、happens-before
happens-before规定了对共享变量写操作对其它线程的读操作可见,可见可见性和有序性的一套规则总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
-
线程解锁m之前对变量的写,对于接下来对m加锁的其它线程对该变量的读可见
static int x; static Object m = new Object(); new Thread(() -> { synchronized(m) { x = 10; } },"t1").start(); new Thread(() -> { synchronized(m) { System.out.println(x); } },"t2").start();
-
线程对volatile变量的写,对接下里其它线程对该变量的读可见
volatile static int x; new Thread(() -> { x = 10; },"t1").start(); new Thread(() -> { Sysytem.out.println(x); },"t2").start();
-
线程start前对变量的写,对该线程开始后对该变量的读可见
static int x; x = 10; new Thread(() -> { System.out.println(x); },"t2").start();
-
线程结束前对变量的写,对其它线程得知它结束后的读可见
static int x; Thread t1 = new Thread(() -> { x = 10; }, "t1"); t1.start(); t1.join(); System.out.println(x);
- 线程t1打断它t2前对变量的写,对于其它线程得知t2被打断后对变量的读可见
-
-
-
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(() -> {
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(() -> {
sleep(1);
x=10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
- 对变量默认值(0, false, null) 的写,对其它线程对改变量的读可见
- 具有可见性,如果 x hb -> y 并且 y hb -> z 那么有x hb ->z ,配合volatile的防止指令重排
volatile static int x;
static int y;
new Thread(() -> {
y = 10;
x = 20;
},"t1").start();
new Thread(() -> {
//x = 20 对 t2 可见, 同时 y= 10也对 t2 可见
System.out.println(x);
},"t2").start();