您的位置:首页 > 编程语言 > Java开发

Java并发编程(二)------Java并发机制底层实现原理

2019-07-16 23:54 441 查看

Java并发机制的底层实现原理

术语了解
缓存行:缓存的最小操作单位。
原子操作:不可被中断的一个或一系列操作。
CAS(Compare and Swap),比较并设置。用于在硬件层面上提供原子性操作。CAS操作需要输入二个数值,一个旧值(进行操作前的值)和一个新值,在操作期间比较旧值是否发生变化,没有发生变才替换为新值,发生了变化则不进行交换。

1. volatile

volatile是轻量级的synchronized,在多处理器开发中它保证了变量的可见性.即当一个线程修改共享变量时,另外一个线程能读到这个修改的值.
如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的.
volatile具有可见性,有序性,不具有原子性

1.1 volatile在CPU中的实现原理

//Java代码如下
instance = new Sington();  //instance 是volatile变量
//转换为汇编代码如下:
0x01a3deld: movb $0x0,ox1104800(%esi);0x01a3de24: lock add1 $0x0,(%esp);

有volatile变量修饰的共享变量进行写操作时会多出第二行汇编代码,查阅IA-32架构软件开发者手册会发现,Lock前缀指令在多核处理器下会引发二件事情:

  1. 将当前处理器缓存行的数据写回到系统内存.
  2. 这个写回内存的操作会使在其他CPU里缓存该内存地址的数据无效.

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

1.2 volatile的实现原则

由1.1小节可知,volatile的实现规则即要达到上面的效果,具体二条规则为:

  • Lock前缀指令会引起处理器缓存回写到内存.
  • 一个处理器缓存回写到内存会导致其他处理器的缓存无效.

2. synchronized

synchronized具有有序性,可见性,原子性
synchronized实现同步的基础---Java中的每一个对象都可以作为锁,具体表现为:

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

2.1 Java对象头(synchronized的锁存放位置)

synchronized的锁是存放在Java对象头里面的.准确的说是对象头里面的Mark Word里面.
如果对象是非数组类型,则用二个字宽存储对象头;如果对象是数组内型,则虚拟机用三个字宽存储对象头.(Java对象为一个数组时对象头还必须有一块用于记录数组长度的数据。因为Java数组元数据中没有数组大小的记录)
Java对象头里面有:

  • Mark Word.一个字宽,用于存储对象的hashCode或锁信息.
  • Class Metadata Address.一个字宽,存储对象类型数据的指针.
  • Array Length.一个字宽,用于存储数组的长度(前提对象是数组).
    其中Mark Word的数据会随锁标志位的变化而变化

2.2 锁的升级与对比

锁一共有四中状态,从低到高为:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态.这些状态会随着竞争而升级,但是锁只能升级而不能降级.

2.2.1 偏向锁

大多数情况锁不仅不存在多线程竞争,而且大多由同一线程多次获取,为了降低获取锁的成本引入了偏向锁.
偏向锁是一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁的时候,持有偏向锁的线程才会释放锁。

  • 当一个线程获取锁成功时,会在对象头的Mark Word及栈帧中的锁记录里存储锁偏向的线程ID.以后线程在进入和退出同步块时不需要使用CAS进行加锁和解锁.
  • 只有当其它线程竞争时,持有偏向锁的线程才会释放锁.
  • 偏向锁的撤销需要等待全局安全点.(在这个时间点上没有正在执行的字节码)

以上为偏向锁的特点.

  1. 偏向锁的获取
    检查对象头的Mark Word里是否存储了当前线程的线程ID.是则表示线程已经获得了锁;不是则检查Mark Word中的偏向锁标识是否被设置为1.
    若设置为1(表示当前锁状态是偏向锁),则使用CAS将对象头的偏向锁指向当前线程.
    若没有设置为1(表示当前锁状态不是偏向锁),则使用CAS竞争锁;

  2. 偏向锁的撤销
    首先暂停持有偏向锁的线程,并判断该线程是否活着,若线程不处于活动状态,则将对象头了的Mark Word设置为无锁状态.
    若线程仍然活着,则将持有偏向锁的栈中的锁记录及对象头的Mark Word,要么偏向其它线程,要么恢复到无锁状态,要么标记对象不适合作为偏向锁,最后唤醒暂停的线程。

  1. 关闭偏向锁
    通过JVM参数 -XX:BiasedLockingStartupDelay=0 来关闭延迟启动.
    通过JVM参数 -XX:-UseBiasedLocking=false关闭偏向锁,关闭后默认进入轻量级锁状态.

2.2.2 轻量级锁

  1. 轻量锁加锁
    线程在执行同步块前,JVM先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的Mark Word复制到锁记录中.
    线程尝试将对象头的Mark Word替换为指向栈中锁记录的指针,若成功,当前线程获得锁.
    若失败,表示其它线程在竞争锁,当前线程便尝试使用自旋来获取锁.
  2. 轻量锁解锁
    使用原子的CAS操作将当前线程栈帧中的锁记录存储的Mark Word替换回到对象头.
    若成功,表示没有线程在竞争锁.
    若失败,表示当前锁存在竞争,锁便会膨胀为重量级锁.Mark Word中的锁状态变为指向重量级锁的指针.

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞了),锁一旦升级为重量级锁便不会恢复.当锁处于重量级锁状态时,其它线程试图访问时度会被阻塞.

2.2.3 锁的优缺点对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行速度较长。

3. 原子操作

3.1 处理器如何实现原子操作

(1)使用缓存锁定来保持原子性

(2)使用总线锁保持原子性

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

3.2 Java如何实现原子操作

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

使用CAS实现的线程安全的计数器代码。CAS实现原子操作的问题:

  1. ABA问题
    CAS是根据旧值有没有发生变化来更新新值的,所以为了解决ABA问题,可以在变量前加入版本号A->B->A就变成了1A->2B->3A.
  2. 循环时长开销大
  3. 只能保持一个共享变量的原子操作
    jdk1.5后开始,提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里面进行CAS操作。

(2)使用锁机制实现原子操作

JVM内部实现了很多种锁机制,偏向锁,互斥锁,轻量级锁。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Java Mark CAS