jmm

JMM

先看单例模式上最著名的DCL(double check loading)问题。

public class Singleton {

    private static Singleton singleton;

 

    private Singleton(){}

    

    public static Singleton getInstance(){

        if (singleton == null){

            synchronized(Singleton.class){

                if(singleton == null){

                    singleton = new Singleton();

                }

            }

        }

        return singleton;

    }}

 

 

如果变量不加voliate会出什么问题?

假设此时有两个线程thread1和thread2在进行操作。

 

此时thread执行到new Singleton(),而thread2执行到singleton的判空操作,此时由于synchronized具有指令重排,不具有原子性,将会导致singleton初始化后不完整的问题,最后产生NPE异常。接下来解释为什么会产生初始化不完整问题。

singleton = new Singleton();这行代码到底做了什么事情,大致过程如下:

① 虚拟机遇到new指令,到常量池定位到这个类的符号引用。 

 

② 检查符号引用代表的类是否被加载、解析、初始化过。 

 

③ 虚拟机为对象分配内存。 

 

④ 虚拟机将分配到的内存空间都初始化为零值。 

 

⑤ 虚拟机对对象进行必要的设置。 

 

⑥ 执行方法,成员变量进行初始化。 

 

⑦ 将对象的引用指向这个内存区域。

我们把这个过程简化一下,简化成3个步骤:

a、JVM为对象分配一块内存M 

b、在内存M上为对象进行初始化 

c、将内存M的地址复制给singleton变量

 

因为将内存的地址赋值给singleton变量是最后一步,所以Thread1在这一步骤执行之前,Thread2在对singleton==null进行判断一直都是true的,那么他会一直阻塞,直到Thread1将这一步骤执行完。

但是,以上过程并不是一个原子操作,并且编译器可能会进行重排序,如果以上步骤被重排成:

a、JVM为对象分配一块内存M 

c、将内存的地址复制给singleton变量 

b、在内存M上为对象进行初始化

 

这样的话,Thread1会先执行内存分配,在执行变量赋值,最后执行对象的初始化,那么,也就是说,在Thread1还没有为对象进行初始化的时候,Thread2进来判断singleton==null就可能提前得到一个false,则会返回一个不完整的sigleton对象,因为他还未完成初始化操作。

而voliate能够避免指令重排

 1   public class Singleton {  

 2      private volatile static Singleton singleton;  

 3       private Singleton (){}  

 4       public static Singleton getSingleton() {  

 5       if (singleton == null) {  

 6           synchronized (Singleton.class) {  

 7               if (singleton == null) {  

 8                   singleton = new Singleton();  

 9               }  

 10           }  

 11       }  

 12       return singleton;  

 13       }  

 14   }  

 

 

 

 

而上述内容牵扯到jmm,接下来就是对jmm内容的讲解。

 

硬件层的并发优化基础知识

 

 

 

 

线程1、2公共使用同一个CacheLine

x、y在同一个CacheLine

x、y都是volatile

如果线程1不断修改x,线程2不断修改y,那么修改的时候线程1就要不断通知线程2更新x、线程2就要不断通知线程1更新y

这样的不断通知不断重新读取很浪费性能

这就叫伪共享

 

Cache Line 是 CPU 和主存之间数据传输的最小单位。当一行 Cache Line 被从内存拷贝到 Cache 里,Cache 里会为这个 Cache Line 创建一个条目。 

弄懂伪共享问题的前提是:cpu如果想读取x的信息时,会把一整个缓存行的信息读取到cpu的缓存行中,即把x和y的数据一起读入。

伪共享问题:左边上运行的线程想更新L3中缓存变量X,同时右边上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果左边的进程获得了所有权,缓存子系统将会使右边中对应的缓存行失效。此时会通知右边的cpu把x的值进行更改,右边的cpu则去L3中获取重新。右边获得了所有权然后执行更新操作,左边就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。并发的修改在一个缓存行中的多个独立变量,看起来是并发执行的,但实际在CPU处理的时候,是串行执行的,并发的性能大打折扣。

 

解决办法:使用各种各样的一致性协议(例如MESI协议)

在Java中提供的办法:填充法 和 Contended 注解

填充法的原理如下:

https://blog.csdn.net/weixin_43553694/article/details/104534422?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.nonecase

 

多线程会有上面的伪共享的问题,如果在缓存读取数据到CacheLine时,两个volatile的数被读取到不同的CacheLine中的话,就不需要一直通知另一个线程更新数据了,因为另一个线程根本没有这个数据

那么如何让两个数据一定在不同的CacheLine呢,方法就是Cache Line对齐

 

一般一个CacheLine是64位,也就是8个long,我们可以把x定义为long,并同时定义7个没有用的long变量,这样这8个数就在同一个CacheLine中

之后再定义y,y自然也就在下一个CacheLine中了

乱序问题

CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系注解:没有依赖关系,指两条指令没有任何关系,例如 int a=8;a++;此时两条指令存在依赖关系。Int a=8 int b=9,两条指令就是没有任何依赖关系

 

https://www.cnblogs.com/liushaodong/p/4777308.html

写操作也可以进行合并

https://www.cnblogs.com/liushaodong/p/4777308.html

 当cpu执行存储指令时,它会首先试图将数据写到离cpu最近的L1_cache, 如果此时cpu出现L1未命中,则会访问下一级缓存。速度上L1_cache基本能和cpu持平,其他的均明显低于cpu,L2_cache的速度大约比cpu慢20-30倍,而且还存在L2_cache不命中的情况,又需要更多的周期去主存读取。其实在L1_cache未命中以后,cpu就会使用一个另外的缓冲区,叫做合并写存储缓冲区。这一技术称为合并写入技术。在请求L2_cache缓存行的所有权尚未完成时,cpu会把待写入的数据写入到合并写存储缓冲区,该缓冲区大小和一个cache line大小,一般都是64字节。这个缓冲区允许cpu在写入或者读取该缓冲区数据的同时继续执行其他指令,这就缓解了cpu写数据时cache miss时的性能影响。

JUC/029_WriteCombining

乱序执行的证明:JVM/jmm/Disorder.java

原始参考:https://preshing.com/20120515/memory-reordering-caught-in-the-act/

乱序的证明

package jmm;

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

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for(;;) {
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread one = new Thread(new Runnable() {
                public void run() {
                    //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
                    //shortWait(100000);
                    a = 1;
                    x = b;
                }
            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            one.start();other.start();
            one.join();other.join();
            String result = "" + i + " (" + x + "," + y + "";
            if(x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                //System.out.println(result);
            }
        }
    }


    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

 

 

 

如何保证特定情况下不乱序?

硬件层面的不乱序:

硬件内存屏障 X86

sfence: store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence:load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。

原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序

 

 

JVM级别如何规范(JSR133)

LoadLoad屏障:
对于这样的语句Load1; LoadLoad; Load2,

在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:

对于这样的语句Store1; StoreStore; Store2,

在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障:

对于这样的语句Load1; LoadStore; Store2,

在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:
对于这样的语句Store1; StoreLoad; Load2,

​ 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

 

 

volatile的实现细节

 

字节码层面
ACC_VOLATILE

 

 

JVM层面
volatile内存区的读写 都加屏障

 

StoreStoreBarrier

 

volatile 写操作

 

StoreLoadBarrier

 

LoadLoadBarrier

 

volatile 读操作

 

LoadStoreBarrier

 

 

OS和硬件层面
https://blog.csdn.net/qq_26222859/article/details/52235930
hsdis - HotSpot Dis Assembler
windows lock 指令实现 | MESI实现

 

synchronized实现细节

字节码层面
ACC_SYNCHRONIZED
monitorenter monitorexit

JVM层面
C C++ 调用了操作系统提供的同步机制

OS和硬件层面
X86 : lock cmpxchg / xxx
https88571740

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值