本章我们来讲一讲并发机制的一些底层实现原理。
volatile:
在之前文章中我们粗略的讲述了一下volatile关键字,今天我们来全面展开聊聊volatile关键字的底层实现原理。
volatile在官方中的定义如下:
volatile
是一个类型修饰符,用于声明变量。当一个变量被声明为 volatile
时,它会具备以下特性:
- 可见性保证:对
volatile
变量的写操作会立即刷新到主内存,读操作会从主内存读取最新值,确保所有线程能看到一致的变量状态。 - 禁止指令重排序:编译器和处理器不能对
volatile
变量的读写操作进行重排序,以保证程序的执行顺序符合代码的语义。
volatile变量修饰的共享变量进行写操作的时候转化为汇编语言时候我们发现添加了一个汇编指令Lock
而这个Lock则是volatile变量具有可见性和屏蔽指令重排序的关键点所在,Lock指令在多核处理器下会引发两件事。
1. 将当前处理器缓存行的数据写回到系统内存
2.这个协会内存的操作会使得其他cpu里缓存了该内存地址的数据无效
在java内存模型中为了提高速度当一个线程修改完数据之后将结果放在缓存寄存器中,但数据不知道何时会写到内存这就会导致其他线程查看的数据是旧数据。而Lock指令则是采用了禁止缓冲区的操作,将当前线程的缓冲区禁用,从而使得只能将数据写入到内存中去。由于所有的处理器是通过嗅探机制来检查是否有其他线程往内存区写数据,当检测到之后就会自动将本线程的缓冲区给失效
同时在volatile修饰的变量写操作前后都会添加叫做内存屏障的东西从而保证了指令无法进行重排序也解决了指令重排序的问题
synchronized:
在多线程并发编程中synchronized一直是元老级角色,很多人都会叫他重量级锁,但实际上在jdk1.6之后就对其进行了各种优化使其变得不那么重型。在聊synchronized底层之前先看一下基本功能,synchronized实际上表现为以下三种形式
1.对于普通同步方法,锁的是当前实例对象
2.对于静态同步方法,锁是当前类的Class对象。
3.对于同步方法块,锁是synchronized括号里配置的对象
当一个线程想要执行对应的方法的时候必须获得正确的锁,那么锁到底在哪里呢同时里面存储什么信息呢?
在jvm汇编指令可以看出当拿到锁之前会有一个monitorenter的指令还有一个monitorexit的指令这两个指令都会尝试获取对象对应的monitor的所有权及对象的锁
对象的锁信息其实是在对象头中存储的,在虚拟机中数组类型用了3个字宽来存储对象头信息非数组类型则是两个字宽
而所有锁的信息则是存放在mark word当中,mark word内存储的数据会随着所标志位变化而变化
mark word中默认是存放着对象的hashcode,分代年龄和锁标记位。
上表中我们就可以知道实际上synchronized的优化就是进行了锁升级策略--无锁,偏向锁,轻量锁,重量锁。需要注意的是锁升级是单向的一单锁升级后就无法回退。
偏向锁:在一些研究表明大多数情况下锁不仅不存在多线程竞争,而且总是只有一个线程来访问。那么基于此就设计了偏向锁--用来指向拥有锁线程的线程id。当一个线程访问同步代码块的时候会在当前锁对象的头的锁记录中采用CAS存储着当前线程的线程ID,那么当下次访问锁的时候只需要比较线程ID是否正确即可获得锁,比较的过程则是cas比较。一单出现竞争之后就会撤销偏向锁从而升级为轻量锁。当其他线程进行访问的时候会先查看当前锁线程对应的线程是否还活着如果不再存活那么将对象头设置为无锁,如果还活着就会进行撤销操作,会直接暂停当前线程然后将锁进行撤销也就是mark down中的字段置为空。转而升级为轻量锁之后在恢复线程执行
轻量级锁:轻量级锁则是采用cas乐观锁的方式来进行加锁的,当一个线程获得锁的时候会将在自己的栈帧中创建一个锁记录的存放空间,同时拷贝对象头的mark word复制到当前空间中,并且将对象头的mark word设置为指向当前线程的存放空间的指针,这个过程是采用cas来进行设置的,如果设置成功当前线程则获取锁设置失败则再次自旋来获取锁。当进行解锁的时候则是将原来的存放空间的数据放回mark word当中这个过程也是cas操作。如果自旋次数超过 JVM 设定的阈值(默认 10 次,可通过 -XX:PreBlockSpin
参数调整),或自旋线程数超过 CPU 核心数的一半,锁会升级为重量级锁