并发机制初识
# 并发机制初识
java代码编译成java字节码后,会被加载到JVM中执行,并转换成汇编指令在CPU上执行。因此java所有并发机制依赖于JVM的实现和CPU指令。
本章节主要介绍:
- volatile底层处理器实现原理
- synchronized的几种锁
# 1.volatile
可见性
共享变量的可见性指的是,当一个线程修改一个共享变量后,其它的线程能够读取到这个修改的值。
# volatile修饰的变量如何保证可见性?
为了提高处理效率,CPU不会直接和内存进行通信,而是通过CPU—缓存—内存的方式进行。对于多核处理器而言,每个处理器都有自己专门的缓存区,所有缓存区共享一个系统内存。
在对volatile修饰的变量执行写操作时,生成的汇编代码会多一个Lock指令,这个Lock指令只要做了两件事:
- 当前处理器缓存行的数据会写回到系统内存中
- 其它CPU缓存的该地址的数据失效
其它CPU会使用嗅探技术,保证CPU内部缓存、其它CPU内部缓存、系统内存的数据在总线上保持一致。
# volatile使用优化
缓存行填充
多个变量占据的字节小于缓冲行的大小,那么CPU会将多个变量读到同一个高速缓存行。
而如果个别处理器支持“缓存行填充”,那么不满足缓存行大小的变量会自动扩充,单独填满占据一个缓存行。

不支持“缓存行填充”的CPU,在读写某个变量时,可能会导致同一个缓存行内的其它变量同时被锁住,如上图CPU1在对head节点操作时,会导致其它多处理器都不能访问缓存行1其它变量。
字节追加:通过扩充volatile变量的字节大小,保证每个节点变量只占据一个缓存行存储。解决了多个共享变量在频繁写的过程中产生的锁定问题。
# 2.synchronized
synchronized用于实现代码同步:每个线程在访问同步代码块时,首先需要获取锁,执行完退出或者抛出异常时需要释放锁。
synchronized用的锁保存在java对象头中,java对象头包括以下几个部分:
- Mark Word:存储对象的锁信息、hashcode。具体会根据锁标志位进行变化,不同的锁对应不同的Mark Word信息。
- Class Metadata:存储对象元信息、数据、地址
- (数组对象)
Java SE 1.6中,锁等级从低到高一共可以分为4种状态:无锁状态、偏向锁状态、轻量级锁、重量级锁。锁可以升级,但不能降级。
# 2.1偏向锁
为了使同一个线程多次访问同步块获取锁时,获取锁的代价更小,引入了偏向锁。
线程第一次获取锁时,在对象头中存储偏向锁的线程ID,以后该线程再次访问时,无需进行CAS操作加锁解锁(修改Mark Word)。
偏向锁撤销:当其它线程想要访问同步块,尝试通过CAS竞争获取偏向锁时,当前持有偏向锁的线程才会释放锁,将偏向锁偏向于其它线程,或是变为无锁状态。
# 2.2轻量级锁
轻量级加锁:线程首先将对象头的Mark Word复制到自己的栈帧中,然后通过CAS操作尝试将对象头的Mark Word修改为指向该锁记录栈帧的指针。如果成功,则当前线程获得轻量级锁,否则会不断自旋尝试竞争获取锁。

轻量级解锁:通过CAS操作将栈帧的锁记录 Displaced Mark Word替换为对象头。如果失败则表示当前存在多个线程同时竞争获取锁和释放锁,膨胀为重量级锁。
自旋操作会消耗CPU,如果当前锁处于重量级锁,那么试图获取锁的所有线程都会被阻塞住。直到获取锁的线程释放锁后,才会唤醒所有被阻塞的线程,进行新一轮的竞争。
# 2.3对比
偏向锁:适用于只有单个线程访问同步块的情况。
轻量级锁:用于不同线程访问同步块的情况。每个线程都会自旋获取锁,性能高,响应时间短。
重量级锁:用于解决锁竞争消耗CPU的情况。锁竞争不消耗CPU,吞吐量高。但涉及线程阻塞和唤醒,进行上下文切换,响应时间长。
# 3.CAS原子操作
CAS无锁算法
如果“当前地址的值”与“先前读到的该地址的值”是一致,那么可以判定人为该变量没有被修改过,可以赋予新的值。否则不做任何修改。
循环CAS用于保证对共享变量操作时,是原子操作。
ABA问题:解决方式通过加版本号,判断当前第二个变量A是否已经被修改过。