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;
    }
}