Java 多线程的内存模型

Java 多线程的内存模型

JMM是是Java的内存模型,和JVM的内存模型是两回事(Java Runtime Data Area)

内存模型图如下

内存图

当多线程进行数据交互的时候,比如线程A修改了共享变量,线程B读取读,A修改完变量在自己的工作内存中,B是看不到(或者说感知不到A的修改),只有A的工作区协会到主内存,B再从主内存读取自己的工作区才能进一步操作,因为指令重排序的存在,这个写读的顺序可能被打乱,所以JMM需要提供原子性,可见性,有序性的保证。

原子性

一个操作不能被打断,要么执行成功,要么执行失败。
在Java里 对基本数据类型的访问都是原子性操作(默认是64位机器,32位的机器可对long,double 这种占用8个字节的基本数类型是分2次加载然后合并的)。
对非原子性操作的数据,多线程的访问是不安全的。

操作系统层面

1.处理器自动保证基本内存操作的原子性
处理器会自动保证基本的内存操作的原子性(处理器从系统内存读或者写入单字节的时候是原子的,也就是说当一个处理器访问的时候,其他的处理器是不能访问这个字节的内存地址的)

2.使用总线锁保证原子性
总线锁是使用cpu提供的lock#信号,当一个cpu在总线上输出lock信号的时候,其他的cpu的请求会被阻塞(这里有一个问题,当读取的值跨多个缓存行的时候总线锁是不生效的, 比如在Jav在读写long或者double的时候,cpu一次读取不全,会读2次合并值,这就不是一个原子操作)

3.使用缓存协议保证原子性

mesi缓存一致性协议,是因为这套方案把一个缓存行标记四种不同的状态.

  1. modified: 某个缓存行被某个cpu独占,这个cpu对该缓存行修改后的标记的,这个缓存行指向的值是这个cpu上的最新值。(这里cpu不会进行回写的操作,所主内存里不是最新的值)
  2. exclusive:这列也表示某个缓存行呗cpu独占,但是这里在于,cpu执行了写会操作,直接把最新值刷到内存里边了。
  3. shared:这里缓存行的副本会被多个cpu持有
  4. invalid:缓存行无效,也就是空缓存

有了缓存一致性为什么要有volatile

既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字

可见性

JMM的可见性的保证有两种方式synchronized,和volatile来保证的

synchronized

Monitors机制(监视器,或者管程机制),
在执行到monitorenter的时候,其实使用的是Mutex Lock,MutexLock(pthread_mutex_lock),这个东西是互斥锁,当有一个线程获取到资源的时候,将会阻塞他线程,
直到当前资源变为可用为止。

MutexLock存在的问题

  1. 会引用户态到内核态的转变
  2. 切换线程上下文的时候,需要保护和恢复寄存的数据
  3. 内核线程执行完成以后会有额外的工作(检查是否需要调度,恢复寄存数据)
 public void sayHello();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #5                  // Field lock:Ljava/lang/Object;
         3: dup
         4: astore_1
         5: monitorenter
         6: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc           #7                  // String hello word
        11: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             6    16    19   any
            19    22    19   any
      LineNumberTable:
        line 18: 0
        line 19: 6
        line 20: 14
        line 21: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  this   Lcom/concurrent/testsynchronized/TestSynchronized;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class com/concurrent/testsynchronized/TestSynchronized, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

volatile

volatile是jdk 中的一个关键字,在jdk1.5中增强了volatile语义,如果有想了解的具体的请看

volatile可以保证变量的内存可见性。

volatile的特性

1.volatile可以保证变量的内存可⻅性,
2.volatile可以修饰数组,但是数组内部的元素不能拥有volatile的特性(数组内部的元
素享受不到 final,volatile等)
3.⼀个线程对volatile变量写以后对另外⼀个线程可⻅(happen-before 规则)
4.volatile不可以保证符合操作的原⼦性
(⽐如 i=i+1;i=j; )
5.volatile 在⼀定的场景下可以保证有序性。
6.volatile变量写会将之前所有变量的操作都会刷新回主存,其他core的缓存失效,不管
这些变量是否volatile,也就是说只要操作在volatile写之前。另⼀个线程的volatile
读发⽣在volatile写之后的话,这些操作读线程也能看到(⽐如 int a ,int b
,volaitile int c)。
总结成两句话来说:
 1.保证内存可⻅性,但不保证原⼦性
 2.禁⽌指令重排序

volatile在Java中实现机制

jvm中使用的4种屏障
loadLoad: 读读, 这样的 比如A loadload B在   b及后续读取操作要读取的数据被访问前,保证a要读取的数据被读取完毕。   
storeStore:写写,比如A storestore B,
在b及后续写入操作执行前,A的值都可见。
LoadStore:读后写,比如A loadStore B,
在b及后续的写入操被刷出前,A的值都可见
storeload:写后读,比如A StoreLoad B,
在b及后续读取操作要读取的数据被访问前,保证写入A的值可见(这个指令的开销最大,要psuh cpu的缓存buffer);
happen-before规则,第三条,一个线程对volatile变量写以后对另外一个线程可见,当volatile变量写的时候回添加storeStore 指令,在volaitile变量后的变量的值也会刷新到对其他线程可见,
当一个线程读
注:其实final也用内存屏障来保证禁止指令重排序

volatile在系统层面实现的机制

public class VolatileOne {
 static volatile VolatileOne volatileOne;
 public static void main(String[] args) {
 volatileOne = new VolatileOne();
 }
}
 0x0000000114f102d3: lea 0x1f8(%r15),%rdi
 0x0000000114f102da: movl $0x4,0x270(%r15)
 0x0000000114f102e5: callq 
 0x000000010e346d70 ; {runtime_call}
 0x0000000114f102ea: vzeroupper 
 0x0000000114f102ed: movl $0x5,0x270(%r15)
 //这⾥ lock了 ⼀下 使用了 lock add 的cpu指令
 0x0000000114f102f8: lock addl $0x0,(%rsp)
 0x0000000114f102fd: cmpl $0x0,-0x6613597(%rip

final

final 以不变应万变
  final 语义:
    final 写之前: 添加store_store 内存屏障(这里都是抽象的概念,不同的cpu之间的支持不一样,x86和Intel i5之间的支持都不同)
    读会添加load_laod指令(都是一个意思,就是抽象的概念)
    load_load: 相当pull 把数据拉过来
    load_store: 遇到读操作时,他就会先检测读操作之后的任何写操作
    store_load: 遇到写的操作时,他就会先检测读操作之后的任何写操作
    store_store: push 推过去
   final 不能重排序原因是因为在return之前添加了一store_store内存屏障。
    注:
     *  1.JMM禁止编译器将写final域的操作重排序到构造函数外
     *  2.编译器会在final域的写入之后,构造函数return前,
     *  插入一个StoreStore屏障,这个指令会
     *  禁止处理器把final域的写重排序到构造函数之外

有序性

volatile

惊不惊喜,意不意外又是我

在Java编程中指令重排序有两种情况,
1.编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执⾏
顺序.(如果有疑问请看下边不中)
2.处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执⾏顺
序。
 a.数据依赖性,3种情况,
1.写后读
2.写后写
3.读后写
这三种情况,要重排序两个操作的执⾏顺序,程序的执⾏结果将会被改变。

下面是happens-before原则

程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作。
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
《深入理解Java虚拟机第12章》

这里说一下volatile变量规则,它标志着volatile保证了线程可见性。简单的来说一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的,也就是说读的线程一定会看到最新的值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值