多线程与高并发编程进阶(二)

前言:
前文多线程与高并发入门中,已经介绍了多线程编程的目的以及实际应用中可能会遇到的问题,本文接着叙述关于多线程并发机制的底层原理–volatile以及synchronized;
一般来说,Java代码从编写到最后的执行会经历以下的过程:Java源代码(.java文件)–<即时编译器>–Java字节码(.class文件)–<类加载器加载>–汇编指令(CPU执行);Java中的并发机制依赖于JVM以及CPU指令,这些具体的实现原理将在下面进行分析;

在Java的并发机制中,比较典型也比较常见的两个关键字:volatile以及synchronized;volatile保证了共享变量在多线程中的可见性,对于可见性,可以简单地理解为当一个线程修改过volatile关键字修饰的共享变量之后,其他的线程再次访问该共享变量的值时,访问的是之前线程修改后的最新值;而对于synchronized关键字来说,在JDK1.6之前,它是比较重(阻塞以及上下文切换),很多人称其为重量级锁;

对于多个线程访问同一个共享变量,如果想要保证共享变量的准确一致的更新,一般采用的是使用排它锁(互斥锁)来保证对共享变量操作的线程安全;但是,使用volatile关键字来修饰共享变量,在某些情况下,比synchronized关键字更加方便,更加轻量,因为volatile关键字修饰的字段可以保证该字段的内存可见性;


volatile的实现原理
通过对volatile变量写操作对应的汇编指令分析可以发现,比普通变量的写操作会多出两行汇编指令,其中有一条指令带有lock前缀,查看IA-32架构软件开发者手册可以发现,带有lock前缀的指令在多核处理器下会做两件事情:

  1. 将当前处理器的缓存行中的数据写回系统内存(缓存锁定)
  2. 缓存行写回系统内存的同时会将其他处理器中缓存了对应内存地址的数据无效(利用缓存一致性协议–MESI控制协议)

**注:**对于普通的共享变量来说,何时将更新的值从缓存中写回系统内存是不确定的,并且即使写回系统内存,但是不会更新其他线程中对应的缓存值,所以会出现数据不一致的问题,而volatile修饰的共享变量,会在对其进行写操作时,通过lock前缀指令将缓存中的数据写回系统内存中,并且通过缓存一致性协议,可以将其他线程中对应的共享变量的值置为无效,使得其他线程必须从系统内存中再次读取最新的值进行后续的操作;

IA-32以及Intel 64处理器能够嗅探其他处理器访问系统内存和自己的内部缓存,处理器通过嗅探技术保证自己的内部缓存、系统内存以及其他处理器的内部缓存中的数据在总线上保持一致;
比如,一个处理器通过嗅探检测到其他处理器打算写内存地址,并且该地址处于S状态(共享状态),那么正在嗅探的处理器就会将自己对应的缓存行置为无效,在下次访问相同内存地址时,就会强制执行缓存行填充,从系统内存中读取最新的数据;


synchronized的实现原理:

  1. Java中每一个对象都可以作为锁,都与一个monitor与之关联
  2. Java中synchronized关键字的三种使用:
    ①普通同步方法:锁对应当前实例对象
    ②静态同步方法:锁对应当前类的Class对象
    ③同步方法块:锁对应显式指定的对象
  3. 锁的实现以及相关的信息
    ①锁的实现:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,两者的实现细节有所不同,同步代码块是通过monitorenter和monitorexist指令实现,同步代码块编译之后,会在在代码块的开始位置插入monitorenter指令,代码块中方法结束处和异常处插入monitorexist指令,JVM保证monitorenter和monitorexist指令必须成对出现;
    ②锁的相关信息:在Java对象头中的Mark Word中,会存放对象的HashCode、分代年龄以及锁标记位,在运行期间,Mark Word中存储的数据会随着锁标志位的变化而变化,具体如下图所示:
    多线程与高并发编程进阶(二)_第1张图片
  4. synchronized的优化——偏向锁
    **锁的四种状态:**无锁、偏向锁、轻量级锁以及重量级锁(锁的级别递增),并且锁的级别只能升不能降;

偏向锁:
**引入偏向锁的原因:**大多数情况下,锁都是有同一个线程获取的,为了降低由于不必要的锁的获取和释放导致的代价,引入偏向锁;
**偏向锁的实现原理:**当一个线程访问同步块并获取锁时,会在对象头和栈帧中存储锁偏向的线程ID,之后线程再进入和退出同步代码块时就不需要使用CAS操作来加锁和解锁,只需要检测对象头中的Mark Word中是否存储指向当前线程的偏向锁,如果是,则直接进入同步块,否则,需要测试Mark Word中偏向锁的标志是否设置为1,如果设置为1,则尝试使用CAS将对象头的偏向锁指向当前线程,否则,使用CAS竞争锁;
**偏向锁的撤销:**等到竞争出现时,才会释放锁,即当其他线程尝试竞争偏向锁时,持有偏向锁的线程才回释放锁;
**偏向锁的关闭:**可以使用JVM参数关闭偏向锁开启的延迟:-XX:BiasedLockingStartupDelay=0;使用JVM参数关闭偏向锁:-XX:UseBiasedLocking=false,程序进入轻量级状态;


轻量级锁:
**轻量级锁的加锁:**线程执行同步块之前,JVM会现在当前线程中的栈帧中创建用于存储锁记录的空间,并将对象中的Mark Word复制到锁记录中,接着线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,替换成功,当前线程获得锁,否则,表示其他线程竞争锁,当前线程尝试使用自旋获取锁;
**轻量级锁的解锁:**解锁时,会使用CAS操作将原来的Mark Word替换回对象头,成功表明没有竞争发生,否则,锁膨胀为重量级锁;

你可能感兴趣的:(Java多线程,多线程与并发编程)