[Java并发系列] 1.Java并发机制的底层实现
在Java并发实现的机制中,大部分的容器和框架都是依赖于volatile/synchronized/原子操作实现的,了解底层的并发机制,对于并发编程会带来很多帮助
1. synchronized的应用
synchronized在多线程并发编程中已经是一个元老级的存在,通常被称作是重量级锁。既然是常用的一种锁,那么就需要对它的底层实现有深入的了解。
1. synchronized的实现原理
当一个线程在访问同步代码块时,就必须要先获取该代码块中对象的锁,退出或者抛出异常时,就必须要释放锁。synchronized的同步实现,是JVM基于进入和退出同步对象的Monitor对象来实现方法同步和代码块同步的。
每个Monitor对象都有与之关联的monitor,当且仅当monitor被持有后,它才会处于锁定状态。同步代码是使用monitorenter和monitorexit指令实现的。monitorenter指令时在代码编译后插入到同步代码块的开始位置,monitorexit指令则是插入到方法的结束和异常处。当线程执行到monitorenter时,会尝试去获取对象的monitor的所有权,即尝试获取对象的锁。
2. synchronized的使用形式及意义
- 修饰普通方法:锁是当前实例对象
- 修饰静态方法:锁是当前类的Class对象
- 修饰代码块:锁是synchronized括号里面配置的对象
3. 锁的升级
锁共有四种状态:无锁状态->偏向锁状态->轻量级锁状态->重量级锁状态
偏向锁:当线程访问同步块并获取锁时,会在对象头和栈针中的锁记录中存储锁偏向的ID,以后该线程在进入和退出同步块时就不需要在使用CAS操作来进行加锁和解锁操作,而仅仅需要测试一下对象头中的Mark Word字段中存储的线程ID是否是当前线程即可。如果是,那么表示获取锁成功;如果不是,则需要检查对象头中偏向锁的字段是否设置为了1(表示当前锁是偏向锁),如果没有设置,则需要使用CAS来竞争获取锁,如果已经设置了,则尝试使用CAS将对象头的偏向锁ID指向当前线程。
轻量级锁;
- 轻量级锁加锁:线程在执行同步块之前,首先会在自己的线程栈中创建一个用于存储锁记录的空间,并将对象中的Mark Word 赋值到锁记录中。然后线程尝试使用CAS操作将对象头中的Mark Word替换为指向锁记录的指针。如果成功,则表示当前线程获取锁,如果失败,则表示其他线程也在竞争锁,当前线程便使用自循的方式来获取锁。
- 轻量级锁解锁:轻量级锁解锁时,会使用原子CAS操作将锁记录替换会对象头,如果成功,则表示没有竞争发生。如果失败,则表示当前锁存在竞争,锁就会膨胀为重量级锁。
4. 锁的对比
锁类型 | 优点 | 缺点 | |
---|---|---|---|
重量级锁 | 线程竞争不使用自旋,不会浪费CPU | 线程阻塞,响应时间慢 | 追求吞吐量,同步块执行的时间长的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高程序的响应速度 | 如果线程长时间得不到锁,那么自旋就会浪费CPU | 追求响应时间,同步块执行速度快 |
偏向锁 | 加锁和解锁不需要额外的资源消耗 | 如果线程间存在竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步场景 |
2. volatile的应用
volatile是轻量级的synchronized,volatile在多线程开发中保证了共享变量的可见性,所谓可见性,指的是当一个线程修改了共享变量之后,对于其他线程来说,能够读到这个修改的值。
1. volatile的定义及实现原理
volatile定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁获得这个变量。
实现原理:当对声明了volatile的变量进行写操作时,JVM就会向处理器发送一条带有lock前缀的指令,将这个变量所在的缓存行的数据写回到系统内存中。同时,在多处理器的情况下,需要执行缓存一致性协议,即每个处理器都需要通过嗅探总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效,当处理器需要对这个数据进行操作时,再从系统内存中把数据读取到缓存行中。
2. 实现volatile的两条原则
- 带有Lock前缀的指令会引起处理器缓存写回到内存;
- 一个处理器的缓存写回到内存,会导致其他处理器的缓存无效。