JMM内存模型

Volatile

什么是 Volatile

能够保证线程可见性,当一个线程修改共享变量时,能够保证对另外一个线程可见性,

但是注意他不能够保证共享变量的原子性问题。

Volatile的特性

可见性

能够保证线程可见性,当一个线程修改共享变量时,能够保证对另外一个线程可见性,

但是注意他不能够保证共享变量的原子性问题。

public class Mayikt extends Thread {
    /**
     * lock 锁 汇编的指令 强制修改值,立马刷新主内存中 另外线程立马可见刷新主内存数据
     */
    private static volatile boolean FLAG = true;

    @Override
    public void run() {
        while (FLAG) {

        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Mayikt().start();
        Thread.sleep(1000);
        FLAG = false;
    }
}

顺序性

程序执行程序按照代码的先后顺序执行。

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程,要么失败。

CPU多核硬件架构剖析

CPU每次从主内存读取数据比较慢,而现代的CPU通常涉及多级缓存,CPU读主内存

按照空间局部性原则加载 局部快到缓存中。

图片.png

为什么会产生可见性的原因

因为我们CPU读取主内存共享变量的数据时候,效率是非常低,所以对每个CPU设置

对应的高速缓存 L1、L2、L3 缓存我们共享变量主内存中的副本。

相当于每个CPU对应共享变量的副本,副本与副本之间可能会存在一个数据不一致性的问题。

比如线程线程B修改的某个副本值,线程A的副本可能不可见。导致可见性问题。

JMM内存模型

Java内存模型定义的是一种抽象的概念,定义屏蔽java程序对不同的操作系统的内存访问差异。

主内存

存放我们共享变量的数据

工作内存

每个CPU对共享变量(主内存)的副本。堆+方法区

JMM八大同步规范

图片.png

(1)lock(锁定):作用于 主内存的变量,把一个变量标记为一条线程独占状态

(2)unlock(解锁):作用于 主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

(3)read(读取):作用于 主内存的变量,把一个变量值从主内存传输到线程的 工作内存中,以便随后的load动作使用

(4)load(载入):作用于 工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中

(5)use(使用):作用于 工作内存的变量,把工作内存中的一个变量值传递给执行引擎

(6)assign(赋值):作用于 工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量

(7)store(存储):作用于 工作内存的变量,把工作内存中的一个变量的值传送到 主内存中,以便随后的write的操作

(8)write(写入):作用于 工作内存的变量,它把store操作从工作内存中的一个变量的值传送到 主内存的变量中

Volatile汇编lock指令

  1. 将当前处理器缓存行数据立刻写入主内存中。

  2. 写的操作会触发总线嗅探机制,同步更新主内存的值。

Volatile的底层实现原理

通过汇编lock前缀指令触发底层锁的机制

锁的机制两种:总线锁(老机器一般都是这个)/MESI缓存一致性协议

主要帮助我们解决多个不同cpu之间三级缓存之间数据同步

总线锁

当一个cpu(线程)访问到我们主内存中的数据时候,往总线总发出一个Lock锁的信号,其他的线程不能够对该主内存做任何操作,变为阻塞状态。该模式,存在非常大的缺陷,就是将并行的程序,变为串行,没有真正发挥出cpu多核的好处。

MESI协议

1.M 修改 (Modified) 这行数据有效,数据被修改了,和主内存中的数据不一致,数据只存在于本Cache中。

2.E 独享、互斥 (Exclusive) 这行数据有效,数据和主内存中的数据一致,数据只存在于本Cache中。

3.S 共享 (Shared) 这行数据有效,数据和主内存中的数据一致,数据存在于很多Cache中。

4.I 无效 (Invalid) 这行数据无效。

E:独享:当只有一个cpu线程的情况下,cpu副本数据与主内存数据如果

保持一致的情况下,则该cpu状态为E状态 独享。

S:共享:在多个cpu线程的情况了下,每个cpu副本之间数据如果保持一致

的情况下,则当前cpu状态为S

M:如果当前cpu副本数据如果与主内存中的数据不一致的情况下,则当前cpu状态

为M

I: 总线嗅探机制发现 状态为m的情况下,则会将该cpu改为i状态 无效

该cpu缓存主动获取主内存的数据同步更新。

总线:维护解决cpu高速缓存副本数据之间一致性问题。

如果状态是M的情况下,则使用嗅探机制通知其他的CPU工作内存副本状态为I无效状态,则 刷新主内存数据到本地中,从而多核cpu数据的一致性。

为什么Volatile不能保证原子性

public class VolatileAtomThread extends Thread {

    private static volatile int count;

    public static void create() {
        count++;
    }

    public static void main(String[] args) {
        ArrayList<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Thread tempThread = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    create();
                }
            });
            threads.add(tempThread);
            tempThread.start();
        }
        threads.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(count);

    }
}

Volatile为了能够保证数据的可见性,但是不能够保证原子性,及时的将工作内存的数据刷新主内存中,导致其他的工作内存的数据变为无效状态,其他工作内存做的count++操作等于就是无效丢失了,这是为什么我们加上Volatile count结果在小于10000以内。

JMM中的重排序及内存屏障

public class ReorderThread {
    private static int a = 0, b = 0;
    private static int x = 0, y = 0;


    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        while (true) {
            i++;
            a = 0;
            b = 0;
            x = 0;
            y = 0;

            // a=1 x=b (x=0,y=1, y=0,x=1 x=0 y=0 x=1 ,y=1)
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {

                    a = 1;
                    x = b;
                }
            });
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {

                    b = 1;
                    y = a;
                }
            });
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
            System.out.println("第" + i + "次(" + x + "," + y + ")");
            if (x == 0 & y == 0) {0
                break;
            }
        }
    }
}

什么是重排序

Java内存模型允许编译器和处理器对指令代码实现重排序提高运行的效率,只会对不存在的数据依赖的指令实现重排序,在单线程的情况下重排序保证最终执行的结果与程序顺序执行结果一致性。

重排序产生的原因

当我们的CPU写入缓存的时候发现缓存区正在被其他cpu站有的情况下,为了能够提高CPU处理的性能可能将后面的读缓存命令优先执行。

注意:不是随便重排序,需要遵循as-ifserial语义。

as-ifserial:不管怎么重排序(编译器和处理器为了提高并行的效率)

单线程程序执行结果不会发生改变的。
也就是我们编译器与处理器不会对存在数据依赖的关系操作做重排序。

CPU指令重排序优化的过程存在问题

as-ifserial 单线程程序执行结果不会发生改变的,但是在多核多线程的情况下

指令逻辑无法分辨因果关系,可能会存在一个乱序中心问题,导致程序执行结果错误。

public class ReorderThread {
    private static int a = 0, b = 0;
    private static int x = 0, y = 0;


    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        while (true) {
            i++;
            a = 0;
            b = 0;
            x = 0;
            y = 0;

            // a=1 x=b (x=0,y=1, y=0,x=1 x=0 y=0 x=1 ,y=1)
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {

                    a = 1;
                    x = b;
                }
            });
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {

                    b = 1;
                    y = a;
                }
            });
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
            System.out.println("第" + i + "次(" + x + "," + y + ")");
            if (x == 0 & y == 0) {
                break;
            }
        }
    }
}

内存屏障解决重排序

处理器提供了两个内存屏蔽指令,解决以上存在的问题

1.写内存屏障:在指令后插入Stroe Barrier ,能够让写入缓存中的最新数据更新写入

主内存中,让其他线程可见。

这种强制写入主内存,这种现实调用,Cpu就不会因为性能的考虑对指令重排序。

2.读内存屏障:在指令前插入load Barrier ,可以让告诉缓存中的数据失效,强制

从新主内存加载数据

强制读取主内存,让cpu缓存与主内存保持一致,避免缓存导致的一致性问题。

public class ReorderThread {
    private static int a = 0, b = 0;
    private static int x = 0, y = 0;


    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        while (true) {
            i++;
            a = 0;
            b = 0;
            x = 0;
            y = 0;

            // a=1 x=b (x=0,y=1, y=0,x=1 x=0 y=0 x=1 ,y=1)
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {

                    a = 1;
                    x = b;
                }
            });
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {

                    b = 1;
                    y = a;
                }
            });
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
            System.out.println("第" + i + "次(" + x + "," + y + ")");
            if (x == 0 & y == 0) {
                break;
            }
        }
    }


}


手动插入内存屏障

public class UnSafeUtils {

    public static Unsafe getUnsafe() {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            return null;
        }
    }

}
 Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1;
                    //插入写内存屏障
                    try {
                        // 手动插入一个内存屏障
                        UnSafeUtils.getUnsafe().storeFence();
                    } catch (Exception e) {

                    }
                    x = b;
                }
            });


双重检验锁为什么需要加上volatile

public class Singleton03 {
    private static volatile Singleton03 singleton03;

    public static Singleton03 getInstance() {
        // 第一次检查
        if (singleton03 == null) {
            //第二次检查
            synchronized (Singleton03.class) {
                if (singleton03 == null) {
                    singleton03 = new Singleton03();
                }
            }
        }
        return singleton03;
    }

    public static void main(String[] args) {
        Singleton03 instance1 = Singleton03.getInstance();
        Singleton03 instance2 = Singleton03.getInstance();
        System.out.println(instance1==instance2);
    }
}

注意:因为我们在new操作 singleton03 = new Singleton03(),存在重排序的问题。

可以采用 javap -c 查看字节码

  1. 分配对象的内存空间

memory=allocate();

  1. 调用构造函数初始化

  2. 将对象复制给变量

  3. 第二步和第三步流程存在重排序也有可能先执行我们的,将对象复制给变量,在执行

调用构造函数初始化,导致另外一个线程获取到该对象不为空,但是该改造函数没有初始化,

所以就报错了 。就是另外一个线程拿到的是一个不完整的对象。

volatile存在的伪共享的问题

Cpu会以缓存行的形式读取主内存中数据,缓存行的大小为2的幂次数字节,

一般的情况下是为64个字节。

如果该变量共享到同一个缓存行,就会影响到整理性能。

例如:线程1修改了long类型变量A,long类型定义变量占用8个字节,在由于

缓存一致性协议,线程2的变量A副本会失效,线程2在读取主内存中的数据的时候,

以缓存行的形式读取,无意间将主内存中的共享变量B也读取到内存中,而化主内存

中的变量B没有发生变化。

图片.png

public class FalseShareTest implements Runnable {
    // 定义4和线程
    public static int NUM_THREADS = 4;
    // 递增+1
    public final static long ITERATIONS = 500L * 1000L * 1000L;
    private final int arrayIndex;
    // 定义一个 VolatileLong数组
    private static VolatileLong[] longs;
    // 计算时间
    public static long SUM_TIME = 0l;

    public FalseShareTest(final int arrayIndex) {
        this.arrayIndex = arrayIndex;
    }

    public static void main(final String[] args) throws Exception {
        for (int j = 0; j < 10; j++) {
            System.out.println(j);
            if (args.length == 1) {
                NUM_THREADS = Integer.parseInt(args[0]);
            }
            longs = new VolatileLong[NUM_THREADS];
            for (int i = 0; i < longs.length; i++) {
                longs[i] = new VolatileLong();
            }
            final long start = System.nanoTime();
            runTest();
            final long end = System.nanoTime();
            SUM_TIME += end - start;
        }
        System.out.println("平均耗时:" + SUM_TIME / 10);
    }

    private static void runTest() throws InterruptedException {
        Thread[] threads = new Thread[NUM_THREADS];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new FalseShareTest(i));
        }
        for (Thread t : threads) {
            t.start();
        }
        for (Thread t : threads) {
            t.join();
        }
    }

    public void run() {
        long i = ITERATIONS + 1;
        while (0 != --i) {
            longs[arrayIndex].value = i;
        }
    }

    //    @sun.misc.Contended
    public final static class VolatileLong extends AbstractPaddingObject {
        public volatile long value = 0L;
//        public long p1, p2, p3, p4, p5, p6;
    }
}

使用缓存行填充方案避免为共享

Jdk1.6中实现方案

public final static class VolatileLong{
    public volatile long value = 0L;
    public  long p1, p2, p3, p4, p5, p6;

}

定义p1-6 加上value 一共占用56个字节 ,在加上VolatileLong类中头占用8个字节一共就是占用64个字节。

注意:在Jdk1.7开始对该代码做优化了,会导致p1-p6无效,所以必须要写一个类单独继承。

Jdk1.7中实现方案

public final static class VolatileLong extends AbstractPaddingObject {
    public volatile long value = 0L;
    public  long p1, p2, p3, p4, p5, p6;
}
public class AbstractPaddingObject {
    public  long p1, p2, p3, p4, p5, p6;
}

@sun.misc.Contended

可以直接在类上加上该注解@sun.misc.Contended,启动的时候需要加上该参数-XX:-RestrictContended

  1. ConcurrentHashMap中的CounterCell
图片.png

synchronized 与volatile存在的区别

1.Volatile保证线程可见性,当工作内存中副本数据无效之后,主动读取主内存中数据

2.Volatile可以禁止重排序的问题,底层内存屏障。

3.Volatile不会导致线程阻塞,不能够保证线程安全问题,synchronized 会导致线程阻塞

能够保证线程安全问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,578评论 6 544
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,701评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,691评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,974评论 1 318
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,694评论 6 413
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,026评论 1 329
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,015评论 3 450
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,193评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,719评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,442评论 3 360
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,668评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,151评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,846评论 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,255评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,592评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,394评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,635评论 2 380

推荐阅读更多精彩内容