1、Java内存模型简介
JMM即Java Memory Model ,它定义了主存、工作内存等抽象概念,底层对应着CUP寄存器、缓存、硬件内存、CUP指令优化等。
JMM体现在以下几个方面:
- 原子性:保证指令不会受到线程上下文切换的影响
- 可见性:保证指令不会受到缓存的影响
- 有序性:保证指令不会受到CUP指令优化的影响
2、可见性
可见性问题-退不出的循环,代码如下:
@Slf4j(topic = "c.Test01")
public class Test01 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
// 循环体
}
});
t.start();
TimeUnit.SECONDS.sleep(1);
log.debug("线程t退出循环...");
run = false;
}
}
上面的代码通过一个布尔变量控制循环结束,但是我们在主线程中改变了布尔值,循环并不会退出,这是啥原因呢?
简单分析:
- 初始状态,t线程从主存中获取布尔值true
- 因为t线程要频繁从主存中读取run的值,JIT编译器会将run的值缓存至内存中的高速缓存中,减少对主存中run的访问,提高效率
- 1s之后,主线程改变了run的值,并同步到主存;而t线程还是从缓存中读取run的值,结果永远是之前的值
3、解决
3.1、volatile
volatile(关键字),它可以用来修饰成员变量和静态成员变量,用来避免线程从工作内存中获取变量的值,必须到主存中获取值,线程操作volatile修饰的变量都是直接操作主存中的变量值。
代码如下:
@Slf4j(topic = "c.Test02")
public class Test02 {
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
// 循环体
}
});
log.debug("t线程开始运行...");
t.start();
TimeUnit.SECONDS.sleep(1);
log.debug("线程t退出循环...");
run = false;
}
}
3.2、synchronized
synchronized不仅可以保证线程的原子性,还可以保证线程的可见性。代码如下:
@Slf4j(topic = "c.Test03")
public class Test03 {
static boolean run = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
synchronized (lock) {
// 循环体
}
}
});
log.debug("t线程开始运行...");
t.start();
TimeUnit.SECONDS.sleep(1);
log.debug("线程t退出循环...");
run = false;
}
}
上面2中方式都可以解决多个线程间变量的可见性问题,那么怎么用哪一种呢?
4、可见性与原子性
-
volatile适用情况:可见性保证的是多个线程之间,一个线程对volatile修饰的变量的修改对另外一个线程 可见,并不能保证原子性,仅适用于一个线程写,多个线程读的情况。
-
synchronized语句块既可以保证代码块的原子性,也同时可以保证代码块内变量的可见性。但缺点是synchronized属于重量级操作,性能相对更低。
5、两阶段终止
之前我们用线程的打断标记实现了两阶段终止模式,这里我们用volatile来改造,代码如下:
// 监控程序
@Slf4j(topic = "c.TwoPhraseTermination")
public class TwoPhraseTermination {
// 监控线程
private Thread monitorThread;
// 结束标记
private boolean stop = false;
// 启动监控线程
public void start() {
monitorThread = new Thread(() -> {
Thread current = Thread.currentThread();
// 判断是否被打断
while (true) {
if (stop) {
log.debug("清理工作...");
break;
}
try {
// 工作内容
// 模拟花费的时间
TimeUnit.SECONDS.sleep(1);
log.debug("执行监控记录");
} catch (InterruptedException e) {
// 异常处理
}
}
});
monitorThread.start();
}
// 停止监控线程
public void stop() {
stop = true;
// 如果线程在睡眠,可立即结束线程
monitorThread.interrupt();
}
}
// 测试
@Slf4j(topic = "c.Test01")
public class Test01 {
public static void main(String[] args) throws InterruptedException {
TwoPhraseTermination tpt = new TwoPhraseTermination();
tpt.start();
TimeUnit.MILLISECONDS.sleep(3500);
log.debug("停止监控线程...");
tpt.stop();
}
}
6、犹豫模式
Balking(犹豫)模式用于一个线程发现另外一个线程或者本线程已经做了某一件相同的的事,那么其他线程或者本线程无需在做,直接结束返回。
已上面的监控为例,代码如下:
// 监控
@Slf4j(topic = "c.Hesitation")
public class Hesitation {
// 监控线程
private Thread monitorThread;
// 结束标记
private boolean stop = false;
// 唯一性标志
private boolean isStarting = false;
// 启动监控线程
public void start() {
synchronized (this) {
// 判断监控线程是否已经启动,如果已启动,终止重新启动
if (isStarting) {
return;
}
monitorThread = new Thread(() -> {
Thread current = Thread.currentThread();
// 判断是否被打断
while (true) {
if (stop) {
log.debug("清理工作...");
break;
}
try {
// 工作内容
// 模拟花费的时间
TimeUnit.SECONDS.sleep(1);
log.debug("执行监控记录");
} catch (InterruptedException e) {
// 异常处理
}
}
}, "monitor");
monitorThread.start();
isStarting = true;
}
}
// 停止监控线程
public void stop() {
stop = true;
// 如果线程在睡眠,可立即结束线程
monitorThread.interrupt();
}
}
// 测试
@Slf4j(topic = "c.Test01")
public class Test01 {
public static void main(String[] args) throws InterruptedException {
TwoPhraseTermination tpt = new TwoPhraseTermination();
tpt.start();
TimeUnit.MILLISECONDS.sleep(3500);
log.debug("停止监控线程...");
tpt.stop();
}
}
这里我们用synchronized,而没有用volatile,因为volatile不能保证代码块的原子性。
但是synchroized又有性能问题,是不是有更好的解决方案呢?
答案是的,单例模式可以很好的解决这个问题,稍后等我们学习的时候在讲解。