《Java并发编程的艺术》--并发机制的底层实现原理

前言

在Java的并发编程中最重要的就是两个关键字volatilesynchronized,其中volatile可以说是轻量级的synchronized,它可以保证共享变量的可见性,而且由于不需要切换线上下文,所以执行成本比synchronized更低。下边就来看一下volatile和synchronized的异同。

volatile

在Java语言规范中对volatile的定义如下:

Java编程语言中允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁来确保单独获取这个变量。

首先明确一下volatile的作用,被声明为volatile声明的变量再被修改后会立即将当前处理器缓存行的数据写回到系统内存,同时这个写回操作会将其他CPU中缓存了该内存地址的数据无效。通过这种机制很好的保证了volatile声明的变量对于多线程而言是可见的,也就是说对于多线程而言,所使用的是同一个volatile变量。

例如:

volatile boolean flag = true

那么flag变量就会在多个线程中都是同样的状态,只要有一个线程对其进行了修改,那么就会立即从线程缓存中刷新回系统内存中,同时若是其他线程也有对flag变量的缓存,那么也需要从系统内存中重新获取更新后的值。

synchronized

synchronized是Java并发编程中极为重要的一个角色,由于初期其进行同步工作开销很大,所以也称为重量级锁,在JavaSE1.6之后进行了优化,为了减少加解锁带来的性能消耗,引入了偏向锁和轻量级锁,进行了优化提升。

synchronized的具体使用形式有三种:

  1. 对于普通同步方法,锁就是当前实例对象

  2. 对于静态同步方法,锁就是当前类的Class对象(这个可以看JVM的类加载机制)《深入理解JVM–类加载机制总结》

  3. 对于同步方法块,锁就是synchronized括号里边配置的对象

以上是synchronized的三种使用形式。

synchronized中同步的实现主要依赖于Monitor对象的进入和退出。在代码同步块中,synchronized会在代码同步块前插入monitotenter指令,在同步块结束处或者是异常处插入monitorexit指令,当一个monitor被持有之后,他将处于锁定状态,当线程运行到monitorenter指令时会尝试获取对象所对应的monitor的所有权。

Java中的锁

在Java SE 1.6之后为了优化synchronized,又引入了两个锁,分别是偏向锁和轻量级锁,这样就一共有四种锁状态,依次是:

  1. 无锁状态
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

这几种锁的状态会随着竞争情况逐渐升级,锁可以升级但是不能降级,也就是说当偏向锁升级为轻量级锁后,不能再降级成偏向锁。

偏向锁

由于发现在大多是情况下,锁不仅不存在多线程竞争,而且还总是由同一线程多次获得,为了让线程获得锁的代价更低,引入了偏向锁。偏向锁的特性是若是成功获得锁,在工作完成之后不主动释放锁,除非有另一个线程和他争夺锁,这时才释放锁,将锁的所有权让给其他线程。

更详细的说就是,当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里边存储偏向锁的线程ID,以后该线程在进入和退出同步块是不需要进行CAS操作来加解锁,只需要测试一下对象头中的Mark Word中是否存储着指向该线程的偏向锁。若是测试成功,表示线程已经获得了锁;若是测试不成功,则需要在测试一下Mark Word中偏向锁的标志是否设置为1(表示当前已经处于偏向锁状态),若是没有设置,则使用CAS锁竞争机制来竞争锁,若是设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

注:关于CAS可以看这篇文章CAS锁

轻量级锁

线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

重量级锁

因为自旋会消耗CPU,为了避免无用的自旋(获得锁的线程被阻塞了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下时,其他线程试图获取锁是都会被阻塞,当持有锁的线程释放锁之后会唤醒这些线程。

三种锁的对比

优点 缺点 适用场景
偏向锁 加解锁不需要额外操作,速度较快 若是发生线程间的锁竞争,会产生锁撤销的消耗 适用于只有一个线程访问同步块的场景
轻量级锁 竞争的线程不会阻塞,程序响应速度块 不断的自旋会消耗CPU资源 追求响应时间,同步块执行速度很快
重量级锁 线程竞争不使用自旋,CPU消耗少 线程会阻塞,相应慢 追求吞吐量,同步块执行时间较长

原子操作的实现

原子操作也就是说这个操作是不可以在进行细分的,必须一次性全部执行完成,不可以执行一部分之后被中断去执行另一个操作。

处理器

对于处理器而言,原子操作也就是说在同一时间只能有一个处理器对数据进行处理,而且这个操作是原子性的,不可分割的。常见的有两种实现方式:一种是通过总线锁来保证原子性,另一种是通过缓存锁来保证原子性。

  • 总线锁:总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,这样发出信号的处理器就可以独占内存,保证操作的原子性。

  • 缓存锁:缓存锁是为了优化总线锁而设计出来的。因为总线锁在被锁住期间,其他的处理器是无法处理其他的数据的,只能等待锁释放开。但是若是对缓存进行加锁就可以减少这个影响。他是指内存区域如果被缓存在处理器的缓存行中,并且咋Lock操作期间被锁定,那么当他执行锁操作写回到内存时,处理器直接修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

Java

在Java中采用了锁机制循环CAS的方式来保证原子性

  • 循环CAS:基本思路就是通过循环进行CAS操作直到成功为止

  • 锁机制:锁机制保证只有获得锁的线程才能操作锁定的内存区域,但是Java中的多个锁,除了偏向锁以外,JVM实现锁的方式都是循环CAS。

猜你喜欢

转载自blog.csdn.net/wqc_csdn/article/details/58066569