【Java并发编程的艺术】Java并发机制的底层的实现原理

 

  首先Java并发的实现依赖于JVM的实现和CPU的指令

 

【volatile】

定义:

 

Java线程内存模型确保所有线程看到的这个变量的值是一致性的。

 

特点:

 

保证变量修改在线程间可见

 

  1. 如何保证共享变量修改线程范围内可见性的?
  • 大多数都存在的情况下:

  为了提高处理速度,处理器通常不直接操作内存,而是先将系统内存的数据读到内部缓存,然后在对还缓存进行通信,但操作完并不确定什么时候会写入到内存中。

  • 加入volatile关键字后:

  带有volatile修饰的共享变量,进行写操作的时候会多出类似如下的指令:
0x01a3de24: lock addl $0×0,(%esp);
Lock前缀的指令在多核处理器中会引发如下操作:
① 将当前处理器缓存行的数据写回到系统内存
② 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
 

由此可见在前面的基础上:对声明了volatile的变量进行写操作后,JVM会向处理器发送一条Lock前缀的指令,该指令可以确保在声言该信号期间,处理器独占任何共享内存,这个时候将这个变量的所在缓存行的数据写回到系统内存。在多处理器下,为了保证各个处理器缓存是一致的,采用缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是否过期了,当处理器发现自己缓存行对应的内存地址发生了修改,就会将当前处理器缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

【Java并发编程的艺术】Java并发机制的底层的实现原理

 

【synchronized】

  1. 表现形式:

因为Java中的每个对象都可以作为锁,所以:

 

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的Class对象
  • 对于同步方法块,锁是synchronized括号里配置的对象

 

 

JVM锁规范中可以看到synchronized在JVM里的实现原理,JVM基于进入和退出的Monitor对象来实现方法同步和代码块同步。但是具体的实现细节还是不一样的。代码块同步使用的是monitorenter和monitorexit指令实现的,方法的同步同样可以使用该指令实现。

带有synchronized修饰的代码块,经过编译后monitorenter指令插入到同步代码块的开始位置,monitorexit是插入到方法结束处和异常处,JVM会保证monitorenter和monitorexit配对存在。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,会尝试获取对象所对应的monitor的所有权,尝试获取对象锁。

 

 

【原子操作】

  1. 处理器如何实现原子操作?
  • 第一个机制通过总线锁保证原子性

  总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被堵塞住,那么该处理器可以独占共享内存。总线锁把CPU和内存之间的通信锁住了,会使得锁定期间,其他处理器不能操作其他内存地址的数据,总线锁定的开销比较大。

  • 第二个机制通过缓存锁来保证原子性

  缓存锁定是指内存区域如果被缓存在处理器的缓存行汇总,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声 言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处 理器回写已被锁定的缓存行的数据时,会使缓存行无效  

 2. 什么情况下不会使用缓存锁定?

  • 第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行 (cache line)时,则处理器会调用总线锁定。
  • 第二种情况是:有些处理器不支持缓存锁定。

 3. Java中如何实现原子操作?

(1)使用循环CAS实现原子操作

  CAS(compare and swap):CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

 

(2)CAS实现原子操作的三大问题
1)ABA问题
2)循环时间长开销大
3)只能保证一个共享变量的原子操作
(3)使用锁机制实现原子操作
除了偏向锁,JVM实现锁的方式都用了循环 CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时 候使用循环CAS释放锁。