1.Java内存模型
1>.JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等;
2>.JMM体现在以下几个方面:
①.原子性 - 保证指令不会受到线程上下文切换的影响;
②.可见性 - 保证指令不会受cpu缓存的影响;
③.有序性 - 保证指令不会受cpu指令并行优化的影响;
2.可见性
2.1.问题: 退不出的循环
1>.main线程对run变量的修改对于t线程不可见,导致了t线程无法停止
@Slf4j
public class TestThreadDemo1 {
// 共享变量
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(run){
// ....
}
},"t1").start();
Thread.sleep(1000);
log.info("程序运行结束");
run = false; // 线程t不会如预想的停下来
}
}
分析:
①.初始状态:t线程刚开始从主内存读取了变量"run"的值到工作内存;
②.因为t线程要频繁从主内存中读取变量"run"的值,JIT编译器会将变量"run"的值缓存至(t线程)自己工作内存的高速缓存中,减少对主存中变量"run"的访问,提高效率;
③.1秒之后,main线程修改了变量"run"的值,并同步至主存,但是t线程还是从自己工作内存的高速缓存中读取这个变量的值,结果永远是旧值;
2.2.解决方案: volatile(易变关键字)
1>.volatile关键字可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找/获取变量的值,要求线程必须到主存中获取它的值
,线程操作volatile变量都是直接操作主存中对应的变量;
@Slf4j
public class TestThreadDemo1 {
// 共享变量
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(run){
// ....
}
},"t1").start();
Thread.sleep(1000);
log.info("程序运行结束");
run = false; // 线程t不会如预想的停下来
}
}
###扩展: Synchronized也可以保证变量的可见性
@Slf4j
public class TestThreadDemo1 {
// 共享变量
static boolean run = true;
//对象锁
final static Object OBJ = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
synchronized (OBJ){
if (!run){
break;
}
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.info("程序运行结束");
synchronized (OBJ){
run = false;
}
}
}
2.3.可见性VS原子性
1>.前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程是可见的,但是volatile不能保证原子性,仅适用在一个写线程,多个读线程的情况;
上例从字节码理解是这样的:
2>.比较一下之前我们将线程安全时举的例子:两个线程一个i++
一个 i--
,只能保证看到最新值,不能解决指令交错;
***注意:
①.synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性
.但缺点是synchronized是属于重量级操作,性能相对更低
;
②.如果在前面示例的死循环中加入"System.out.println()"语句会发现即使不加volatile修饰符,线程t也能正确看到其他线程对"run"变量的修改了,想一想为什么?
2.4.设计模式–两阶段终止模式
1>.在一个线程T1中如何"优雅"终止线程T2? 这里的"优雅"指的是给T2 一个料理后事的机会;
2.4.1.错误思路
1>.使用线程对象的stop()方法停止线程;
stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁;
2>.使用System.exit(int)方法停止线程
目的仅是停止一个线程,但这种做法会让整个程序都停止;
2.4.2.解决方案: 利用停止标记
@Slf4j
public class TestTwoPhaseTermination {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
twoPhaseTermination.start();
Thread.sleep(3500);
log.info("停止监控");
twoPhaseTermination.stop();
}
}
@Slf4j
class TwoPhaseTermination {
//监控线程
private Thread monitorThread;
//设置一个线程停止的标记,使用volatile是为了保证该变量在多个线程之间的可见性
private volatile boolean stop = false;
//启动监控线程
public void start() {
monitorThread = new Thread(() -> {
//循环不停执行
while (true) {
//当前线程是否可以停止
if (stop) {
//如果当前线程被打断,可以退出循环
log.info("料理后事");
break;
}
//如果线程没有停止,执行监控操作
try {
Thread.sleep(1000);
log.info("执行监控记录");
} catch (InterruptedException e) {
}
//执行监控操作
}
}, "监控线程");
monitorThread.start();
}
//停止监控线程
public void stop() {
//通过修改volatile变量的值来停止监控线程
stop = true;
//为了防止监控线程在阻塞过程中线程停止的标记被修改了导致监控线程需要阻塞很长时间之后才能停止
//这里还可以再加一段线程打断的代码立刻打断处于阻塞状态的监控线程马上结束运行!!
monitorThread.interrupt();
}
}
2.5.设计模式(同步模式)-- Balking(犹豫)模式
1>.犹豫模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需(重复)再做了,直接结束返回;
2.5.1.实现
@Slf4j
public class TestBalking {
public static void main(String[] args) throws InterruptedException {
Balking balking = new Balking();
balking.start();
Thread.sleep(3500);
log.info("停止监控");
balking.stop();
}
}
@Slf4j
class Balking {
//监控线程
private Thread monitorThread;
//设置一个线程停止的标记
private volatile boolean stop = false;
//标记某个方法是否已经被执行过
private boolean starting = false;
//启动监控线程
public void start() {
synchronized (this){
//监控线程只需要被执行一次即可,无需重复执行
if (starting) {
return;
}
//当监控线程已经执行过了,标记一下
starting = true;
}
monitorThread = new Thread(() -> {
//循环不停执行
while (true) {
//当前线程是否可以停止
if (stop) {
//如果当前线程被打断,可以退出循环
log.info("料理后事");
break;
}
//如果线程没有停止,执行监控操作
try {
Thread.sleep(1000);
log.info("执行监控记录");
} catch (InterruptedException e) {
}
}
}, "监控线程");
monitorThread.start();
}
//停止监控线程
public void stop() {
//通过修改volatile变量的值来停止监控线程
stop = true;
//为了防止监控线程在阻塞过程中线程停止的标记被修改了导致监控线程需要阻塞很长时间之后才能停止
//这里还可以再加一段线程打断的代码立刻打断处于阻塞状态的监控线程马上结束运行!!
monitorThread.interrupt();
}
}