线程的三大特性主要划分为原子性,可见性,有序性。在原先的synchronized学习中,涉及到的大多是原子性,现在记录下可见性。
初步了解
Java内存模型
JMM,即Java Memory Model,java内存模型,定义了主存,工作内存等一系列抽象概念,底层对应着CPU寄存器,缓存,硬件内存,CPU指令优化等。
现在从抽象概念来进行理解,主存,还有工作内存,最终理解可见性。
举个例子
package com.bo.threadstudy.five;
import lombok.extern.slf4j.Slf4j;
import java.util.Timer;
@Slf4j
public class VolatileTest01 {
private static boolean flag = true;
// private static volatile boolean flag = true;
/**
* 模拟线程的不可见性
* @param args
*/
public static void main(String[] args) {
new Thread(() -> {
while(flag){
}
log.debug("t1执行结束");
},"t1").start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
flag = false;
log.debug("t2线程已执行");
},"t1").start();
}
}
结果是这样的。
很明显没有执行完成。
把volatile的注释解开,注释掉原有定义,结果是这样的。
造成这种现象的原因是什么,从主存,工作内存的角度来进行理解。
例子解答
主存中的数据是共享数据,是线程所共享的。而工作内存中则是线程自身私有的。线程在运行过程中,从主存中读取数据至线程的工作内存,然后将工作内存的数据进行处理后,再次返回至主存中。t2进行的就是这个操作。
现在就是问题所在了。
t1也是将主存中的内容读到工作内存中,然后为了提升效率,一直读取的是自身工作内存(告诉缓存)中的数据。就算主存改了后,它获取的数据也是缓存中,原先自己记录的true,此时造成了t1死循环,出不来的现象。
可见性
那么为什么使用volatile就可以解决呢?
volatile可以保证两点,第一点就是当线程每次来试图读取值的时候,会从主存中来进行读取(读屏障),第二点,当线程在工作内存中对某个值进行修改时,会实时将这个修改的值写入至主存中(写屏障)。
但是要明白一点,不是类似这种观察者模式的场景,当线程A把某个值改了后,然后通知其它线程这个值已经改动过,不管原先读取的什么,必须现在立刻马上按照现在改过的值来,不是这个情况。
真正的场景,是当线程试图从缓存或主存中获取值时,才会知道这个新改动的结果。如果我不从主存中拉取,用的还是旧值,它是主动拉取然后知道的,不是被动通知的。这也就是保证不了原子性的原因。
从主存拉取的场景,工作空间被清除的时候(调用Synchronized),或者初次获取值时(在while循环中,每次获取新值,也会从主存拉取)。
并不是每次一旦涉及到这个变量,就从主存拉取,性能太低了。
原子性与可见性比较
volatile可以保证可见性,即线程修改后可以实时写入至主存,以及线程读取值时,会从主存来读取数据,保证了其它线程的修改对自身可见。常适用于一个写线程,多个读线程的场景。
但是原子性它是保证不了的。举个例子,两个线程i++,和i--,线程2在执行过程中,当数据被线程1改动后,在指令层面是无法实时知道线程这个值被改变过,也就会存在指令交错现象,出现问题。
使用volatile解决两阶段终止
这个重新写一下吧。复习是真的重要,这块我都忘了我原先写了个啥了,其实就是模拟个场景,开启了一个主线程的监控线程,如果像结束监控线程,可以根据状态来让线程执行完成。
老师在stop中加了打断,不加也行,只要退出执行就行,我觉得他忘删代码了。
package com.bo.threadstudy.five;
import lombok.extern.slf4j.Slf4j;
/**
* 两阶段终止模式volatile版
* 什么是两阶段终止模式来着
* 在一个线程中,对另一个线程完成优雅的终结
* 这个例子饿目的就是设置一个监控线程,当主线程让它停止时可以停了
*/
@Slf4j
public class TwoEndTest02 {
public static void main(String[] args) {
TwoEndThread twoEndThread = new TwoEndThread();
twoEndThread.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
twoEndThread.stop();
}
}
/**
* 其实就是封装一个线程,然后在其余线程里创建线程,也可以优雅的结束改线程
*/
@Slf4j
class TwoEndThread{
private Thread thread;
private volatile boolean flag = true;
//这个任务的目的就是为了监控
public TwoEndThread() {
this.thread = new Thread(() -> {
while(flag){
//执行监控线程的任务
log.debug("监控线程正在执行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("监控线程已经被打断执行");
}
}
log.debug("监控线程操作结束");
});
}
public void start(){
thread.start();
}
public void stop(){
flag = false;
}
}
犹豫模式
这个模式很简单,我直接放到这儿了。
Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回
应用场景的话,想想单例的双重校验锁。
package com.bo.threadstudy.five;
import lombok.extern.slf4j.Slf4j;
/**
* 犹豫模式(balking),如果另一个线程已经做过某件事,就不需要再做一次了
*/
@Slf4j
public class BalkingTest {
public static void main(String[] args) {
MonitorStatus monitorStatus = new MonitorStatus();
monitorStatus.start();
monitorStatus.start();
}
}
@Slf4j
class MonitorStatus{
private volatile boolean status = false;
public void start(){
log.debug("开始启动");
synchronized (this){
if(status == true){
log.debug("MonitorStatus已经启动过");
return ;
}
status = true;
}
}
}