JAVA内存模型

JMM java内存模型

多核CPU并发缓存架构

CPU和内存之间的交互是有多级缓存的
CPU和主存之间会有高速缓存,这个缓存速度非常快,空间也非常小.
在使用时,先把数据从主存存放到高速缓存,CPU使用时主要和高速缓存做交互.

JAVA线程内存模型

JAVA的线程内存模型跟CPU缓存类似,是基于CPU缓存模型来建立的.
JAVA内存模型

看一段代码 :

public class VolatileVisibilityTest {

    // 设置一个共享变量,供两个线程使用
    private static Boolean initFlag = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("waiting data...");
                // 只有当initFlag为true时, 即第二个线程修改数据结束时,才会停止循环
                while (!initFlag){}
                // 停止循环的标志就是打印出这行日志
                System.out.println("------ success --------");
            }
        }).start();

        // 确保第一个线程已经开始进入准备循环
        Thread.sleep(1000);

        new Thread(new Runnable() {
            @Override
            public void run() {
                // 将initFlag改为true
                prepareData();
            }
        }).start();
    }

    public static void prepareData(){
        System.out.println("prepare data...");
        initFlag = true;
        System.out.println("prepare data end...");
    }
}

这个运行结果不是很明显吗, 在线程1等待的时候, 线程2将initFlag改为true, 线程1获取到的值为true跳出循环打印success日志.

但是实际情况是 :

waiting data...
prepare data...
prepare data end...

然后程序就停在这里,没有退出也没有下次日志的输出.

我们回头看一下, JAVA线程内存模型那张图. 线程2将initFlag改为true后, 同步到主存中. 但是线程1中保存的共享变量值仍然是false, 这就导致了两个线程使用的共享变量值不一样.

那怎么解决这个问题呢?
使用volatile关键字修饰initFlag

private static volatile Boolean initFlag = false;

输出结果 :

waiting data...
prepare data...
prepare data end...
------ success --------

volataile是保证线程之间可以感知共享变量的修改的.

JMM数据原子操作

  • read(读取) : 从主存读取数据
  • load(载入) : 将主存读取到的数据写入工作内存
  • use(使用) : 从工作内存读取数据来计算
  • assign(赋值) : 将计算好的值重新赋值到工作内存中
  • store(存储) : 将工作内存数据写入主存
  • write(写入) : 将store过去的变量值赋值给主存中的变量
  • lock(锁定) : 将主存变量枷锁,标识为线程独占状态
  • unlock(解锁) : 将主存变量解锁,解锁后其他线程可以锁定该变量

结合上面的例子, 看看都做了哪些原子操作
没有加volatile时所做的原子操作

JMM缓存不一致性问题

早期CPU为了解决不一致的问题采用的是总线加锁的方法.

  • 总线加锁
    CPU从主存读取数据到高速缓存,会在总线对这个数据加锁,这样其他的CPU没法去读或写这个数据, 直到这个CPU使用完数据释放锁之后其他CPU才能读取该数据.即,将并行的程序,让他串行执行.

  • MESI缓存一致性协议
    多个CPU从主存读取同一个数据到各自的高速缓存, 当其中某个CPU修改了缓存里的数据,该数据会马上同步会主存,其他CPU通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效.

volatile可见性原理

底层实现主要是通过汇编lock前缀指令, 它会锁定这块内存区域的缓存并回写到主内存

  • 会将当前处理器缓存行的数据立即写回到系统内存
  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)

如果volatile没有使用lock将主存中的共享变量锁定,其他CPU会有可能在共享变量修改前再次读取变量
如果volatile没有使用lock

所以volatile在写入到工作内存的时候,提前给共享变量加锁.这样其他CPU察觉到共享变量失效时来取的时候只能等待共享变量修改完成.
加锁以防止在还没有修改完成时被读取

volatile原子性问题

先举个栗子:

public class VolatileAtomicTest {

    private static volatile long num = 0;

    private static void increase(){
        num++;
    }

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println(num); // num = 10 * 1000 ?
    }
}

最终输出应该是多少呢?
运行五次,分别是 10000, 9997, 9980, 10000, 10000
可以看到, 最终结果应该是小于等于10000的.
为什么会这样呢? 不是通过volatile将线程间的数值同步了吗.

我们模拟一下两个线程之间做i++的情况

线程 操作 主存i值 工作内存i值
t1 i++ 0 1
t2 i++ 0 1
t1 sotre 1 1
t2 i失效 1 1(失效)
t2 read 1 1

这是两个线程互相失效的情况下出现的问题, 可以看到i++操作做了两次. 主存中的i本应该的2的, 但是因为t1在存储之前,t2已经做了i++操作导致t2的结果被失效,因此结果也就比预期的2少.

volatile有序性原理

先看问题:

public class VolatileSerialTest {

    private static int x = 0, y = 0;

    public static void main(String[] args) throws InterruptedException {
        Set<String> resultSet = new HashSet<>();
        Map<String, Integer> resultMap = new HashMap<>();

        for (int i = 0; i < 10000000; i++) {
            x = 0;
            y = 0;
            resultMap.clear();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    int a = y;
                    x = 1;
                    resultMap.put("a", a);
                }
            });

            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    int b = x;
                    y = 1;
                    resultMap.put("b", b);
                }
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();

            resultSet.add("[a=" + resultMap.get("a") + ", b=" + resultMap.get("b") + "]");
        }
        System.out.println(resultSet);

    }
}

最后输出结果应该会是什么呢?
期初我认为 [a=1,b=0] [a=0,b=1] [a=0,b=0]都是可能会出现的.
那么运行结果:

[[a=1, b=0], [a=1, b=1], [a=0, b=0], [a=0, b=1]]

可以看到中间出现了[a=1, b=1]. 这是我们预料之外的输出,为什么会有这个输出呢? 按照执行顺序,想要a=1就必须先执行y = 1;再之前的int b = x;也应该已经被执行了,同时执行int a = y;x = 1;还没开始,也就是怎么看都不会出现[a=1, b=1]结果.

CPU编译过程中会对程序进行重排序,在执行过程中可能会出现下面这种执行顺序 :

x = 1;
y = 1;
int a = y;
int b = x;

如果想要杜绝这种方法出现, 就在x,y之前加上volatile修饰.
volatile会给语句加上lock这种内存屏障的语义.cpu在执行具有内存屏障的语义时不会进行重排序.


   转载规则


《JAVA内存模型》 echi1995 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
并发 并发
并发控制在java里,同步控制怎么做?加锁. 专业术语叫同步器 synchronize(基于JVM底层,基于C++, 底层行为不可控) AbstractQueueSynchronizer(不利用任何JVM内置锁), 基于java可变行为去
下一篇 
String.contains()的实现 String.contains()的实现
前段时间有一个需求是, 有一个二进制文件, 在二进制文件中有一段是一张png图片. 现在已经有png文件二进制文件头和文件尾, 需要做的是在读取的byte[]数组中查找到这个文件头和文件尾的位置,并截出这段数组. 思前想后也想不到什么好的方
  目录