java内存模型定义了主存,工作内存等这些抽象概念,底层对应着cpu寄存器,缓存,cpu指令优化等。
由此引出了 原子性,可见性,有序性
一、原子性
保证指令不会受到上下文切换的影响而产生指令交错,锁就是用来解决这个问题的
二、可见性
为了保证指令不会受cpu缓存的影响
2.1 现象描述和解释
先看一个例子
private final static Logger LOGGER = LoggerFactory.getLogger(Test8.class);
static boolean flag=true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (flag){
}
}
});
t1.start();
Thread.sleep(1000);
LOGGER.info("改变标记");
flag=false;
}
上边的代码t1线程当flag=true时会一直循环,主线程1s后改变flag,按预想的t1应该会结束,实际上t1线程不会结束,
这就是可见性问题。
java内存模型中有主内存,每个线程都有自己的工作内存,当一个变量被频繁读取时,jit编译器会将flag的值缓存到工作内存中的高速缓存中,后边从缓存中读取。这样当主线程修改了flag并更新到主内存后t1线程还是从高速缓存获取flag,所以一直不能停止
总结下来就是一个线程对主内存的数据进行了修改,但对另外一个线程不可见
2.2 解决办法
(1) 给共享的变量加一个关键字volatile
,表示容易变化的,这样对这个线程的读取就不会走缓存,一直从工作内存获取。
static volatile boolean flag=true;
(2) synchronized也可以解决可见性问题
获取共享变量值的时候先加锁,这样也能保证可见性
public class Test8 {
private final static Logger LOGGER = LoggerFactory.getLogger(Test8.class);
final static Object lock = new Object();
static boolean flag=true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
synchronized (lock){
if(!flag){
break;
}
}
}
}
});
t1.start();
Thread.sleep(1000);
LOGGER.info("改变标记");
synchronized (lock){
flag=false;
}
}
}
这种解决方式下需要注意对 flag
变量的所有操作都要放在synchronized块中
2.3 简单应用
两阶段终止模式可以使用线程的interrupt方法和打断标记来实现,这种方式需要特殊处理InterruptedException
异常,在异常处理中重新设置打断标记,否则就不能正常停止。
也可以使用volatile关键字来实现,这种方式就不需要特殊处理InterruptedException
异常了
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Monitor monitor = new Monitor();
monitor.start();
// 2秒后主线程中执行停止
Thread.sleep(3000);
monitor.stop();
}
}
//建设器类,有一个线程一直在监控
class Monitor{
static Logger logger = LoggerFactory.getLogger(Monitor.class);
Thread t;
//控制是否停止的标记
private volatile boolean isStop=false;
//开始监控的方法
public void start(){
t = new Thread(new Runnable() {
@Override
public void run() {
while (true){
Thread current =Thread.currentThread();
// 因为isStop被volatile修饰了,所以其他线程的修改可以感知到,
// 这样就可以用来控制线程是否结束
if(isStop){
//被打断了
logger.debug("要结束了,执行结束前的操作");
break;
}
try {
//每隔一秒执行一次监控逻辑
Thread.sleep(1000);
logger.debug("执行监控逻辑");
} catch (InterruptedException e) {
e.printStackTrace();
//这种方式不需要在异常中进行特殊处理
}
}
}
},"t1");
t.start();
}
//停止监视器的方法
public void stop(){
isStop=true;
}
}
2.4 volatile解决可见性问题的原理
volatile的原理是基于内存屏障,
在读取被volatile修饰的变量时会在读取指令之前加入读屏障,
在写入被volatile修饰的变量时会在写指令之后加入写屏障,
读屏障会保证屏障之后对volatile变量的读取都从主内存中读,写屏障会保证屏障之前对volatile变量的修改都会刷新到主内存中。
三、有序性
保证指令不会受cpu指令并行优化(指令重排)的影响
2.1 问题描述
jit编译器会在不影响最终结果的前提下调整指令的执行顺序,在多线程环境下可能就会出现一些问题。
例如创建对象时,在java层面看到的是一句代码
User user = new User()
在字节码指令层面会对应着几个步骤
(1)创建实例,(2)执行构造方法(3)暴露引用
而 2和3的顺序是有可能被调整的,这样在多线程环境下如果这个user是个共享变量,当前线程有可能先执行了3那么其他线程就有可能拿到一个不完整的对象。
2.2 解决办法
变量用volatile关键字修饰
2.3 volatile解析有序性问题原理
在读取被volatile修饰的变量时会在读取指令之前加入读屏障,
在写入被volatile修饰的变量时会在写指令之后加入写屏障,写屏障会保证之前的指令不进行指令重排。
2.4 有序性应用 多线程单例模式
public class App {
private App(){}
//volatile关键字禁止指令重排
private volatile static App app;
public static App getInstance(){
if(app == null) {
//只有第一次创建对象时才会进入同步块并加锁
synchronized (App.class){
//防止多个线程同时进入了第一个if
if(app == null){
app = new App();
}
}
}
return app;
}
}
标签:static,Thread,原子,flag,线程,有序性,volatile,多线程,public
From: https://www.cnblogs.com/chengxuxiaoyuan/p/16966067.html