Java并发编程三大特性 对象的内存布局 volatile底层实现

120 阅读5分钟

可见性(visibility)

概念:可见性是指一个线程对共享变量进行修改,另外的线程可以立即得到修改后的新值

a.前置知识

  • 一台机器的多颗CPU可以共享主存,但是CUP的一个核同一时刻只能运行一个线程
  • CPU中有三级缓存,分别为L1,L2,L3
  • L1和L2位于CPU核的内部,L3位于CPU的内部
  • 缓存以缓存行(cache line)为单位来读取数据,根据空间局部性和时间局部性,一行是64字节
  • 寄存器找数据,会先从L1-L2-L3去找,如果没有,才会去主存里面找

b.缓存一致性协议

  • 因为L1和L2的缓存是在核的内部,与其他核是不共享的,所以线程之间可见需要用到缓存一致性协议
  • MESI就是最常见的一种缓存一致性协议
  • 但是多线程修改数据时,如果数据位于同一缓存行的情况下,反而会因为缓存一致性协议互相干扰
  • 解决这个问题我们可以使用填充写法,使缓存行对齐
  • 比如JDK1.7中的LinkedBlockingQueue和Disruptor就用到了这种写法
  • 在1.8中,@Contended注解可以保证数据和其他数据不在同一缓存行
  • 注意需要在jvm中加-XX:-RestricContended这个参数,而且只有1.8有作用

c.volatile

  • MESI保证的是CPU中cache(缓存)的一致性,也就是L1,L2,L3,所以我们依然需要volatile
  • volatile修饰的数据,会要求每个线程在缓存L1中改完值后,马上同步到主存
  • 同步主存后,再通知给所有用到这个值的线程刷新缓存,这样就实现了所有CPU线程之间的可见性
  • 实现原理是通过每个CPU不同的缓存一致性协议实现的

有序性(ordering)

概念:有序性就是程序执行的顺序按照代码的先后顺序执行 as-if-serial:看上去像序列化执行

a.前置知识

  • 指令重排:为了提高执行效率,CPU在不影响单线程的最终一致性的情况下,指令可能会乱序执行,也就是指令重排

b.乱序问题

  • CPU存在指令重排,所以会产生乱序,单线程没问题,但是在多线程的情况下,会产生难以察觉的错误
  • 当new对象时,会在内存中申请一块内存空间,此时,对象为半初始化状态,成员变量的值都是默认值
  • 之后调用构造方法,成员变量会被设为初始值,最后和引用建立关联
  • 指令重排序会导致this状态逸出构造方法,也就是对象处于半初始化状态时,就和引用建立了关联
  • 所以在构造函数中启动线程,使用当前对象的成员变量的话,可能会出现成员变量为默认值的情况

c.解决乱序

CPU级别

通过内存屏障解决,内存屏障是一条特殊的指令,每种不同的CPU屏障的指令都不一样

JVM级别

在JVM中不是通过CPU屏障指令实现的内存屏障

JVM规范中要求每个实现JVM的虚拟机,都必须实现JVM的4个内存屏障(Load:读指令,Store:写指令)

内存屏障

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的第二个作用是防止指令重排,实现原理是内存屏障 在这里插入图片描述

底层细节
  • hotspot对JVM内存屏障的实现直接使用了LOCK
  • LOCK 用于在多处理器中执行指令时对共享内存的独占使用
  • 它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效
  • 另外还提供了有序的指令无法越过这个内存屏障的作用

原子性(atomicity)

概念:原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败 要实现原子性操作,必须依赖与锁

a.前置知识

  • 用户态:用户态也就是我们的应用程序,只能访问用户能够访问的指令
  • 内核态:内核态是执行在内核空间,可以访问所有的指令
  • 重量级锁:JVM与操作系统之间的申请锁和返回锁,都需要经过用户态到内核态的转换,通过这种方式申请到的锁,叫重量级锁
  • 对于操作系统来说,JVM也仅仅是工作在用户态,锁(lock)这个资源是要通过操作系统才能申请到的
  • 用户态到内核态的调用是一个0x80的执行过程(0x80的执行过程暂时忽略)
  • 单核CUP的情况下,汇编指令都是原子性操作
  • 多核CPU的情况下,只有汇编手册中指定的某几条指令才是原子性操作
  • 在多核CPU中,想要指令是原子性操作得加lock上锁

b.对象的内存布局

  • 跟虚拟机的实现有关系 ,主要以hotspot的实现为主
  • MarkWord:8字节:主要用于记录synchronized锁信息,GC,HashCode等信息
  • 类型指针(class pointer):4字节:默认开启指针压缩是4个字节,没开启是8个字节
  • 实例数据(instance data):字节数以实际情况为主,需要注意的是,引用类型的指针也存在压缩和未压缩的情况
  • 对齐(padding):8字节对齐,补字节数,保证整个对象的字节必须要能被8整除

c.可跳转链接:synchronized底层实现