Synchronized
Synchronized 是Java的一个关键字,它能够将代码块或方法锁起来,是一种互斥锁,一次只能允许一个线程进入被锁住的代码块
- 如果 Synchronized 修饰的是实例方法,则对应的锁是对象实例
- 如果 Synchronized 修饰的是静态方法,对应的锁是当前类的 Class 实例
- 如果 Synchronized 修饰的是代码块,对应的锁是传入 Synchronized 的对象实例
Synchronized 的原理
当 Synchronized 修饰的是方法的时候,通过反编译可以看到通过 ACC_SYNCHRONIZED
来标识
当 Synchronized 修饰的是代码块的时候,通过反编译可以看到,是通过 monitorenter
和 monitorexit
指令来控制加加锁和解锁的。
Monitor : 每个对象都会存在一个与之对应的 Monitor 对象,Monitor 对象中存储着当前持有锁的线程和等待加锁的线程队列
monitorenter
和 monitorexit
指令分别会让锁计数器+1或者-1,每一个对象在同一时间只能与一个monitor 相关联,一个 monitor 在同一时间只能被一个线程获取
当某个线程想要获取与一个对象相关联的 monitor 锁的所有权的时候,会触发 monitorenter
指令,会有三种情况:
- monitor 计数器为 0,没有线程获取该 monitor,所以直接将计数器+1即可,其他线程想要获取就要等待
- 如果这个线程已经拿到了这个 monitor 锁的所有权,那么就是锁的重入,计数器+1即可
- 这把锁已经被别的线程获取了,则会等待锁的释放
当某个线程去释放锁的时候,会触发monitorexit
指令,对于释放 monitor 的所有权相对简单,只要将计数器 -1 即可,-1 后不为 0 则为重入了锁,当前线程还会持有锁,-1 后为 0 则释放了锁
JVM 中锁的优化
Jdk1.6 之前,Synchronized 的加锁是依赖底层操作系统的 Mutex 来实现的,需要在用户态和内核态之间进行切换,资源消耗很大,锁的性能相对比较低,所以在 jdk1.6 中对锁引入了很多优化,锁粗化、锁消除、轻量级锁、偏向锁、适应性自旋等
- 锁粗化:减少不必要的紧连在一起的unlock,lock 操作。将多个连续的锁扩展为一个更大范围的锁
- 锁消除:通过逃逸分析,JVM 会判断在一段程序中的同步明显不会逃逸出去从而被其他线程访问到,那 JVM 就把他当做栈上数据对待,认为这些数据是线程独有的不需要加同步
- 轻量级锁:一般情况下,程序中的大部分同步代码都处于无锁竞争状态,即单线程执行环境,在这种情况下,就可以避免调用操作系统的重量级锁,可以用简单的 CAS 来代替
monitorenter
和monitorexit
指令。当存在竞争是,执行 CAS 失败的线程将调用操作系统互斥锁进入阻塞状态,当锁被释放的时候被唤醒 - 偏向锁:为了在无锁竞争的环境下避免在锁获取的时候执行不必要的 CAS 指令
- 适应性自旋:当线程在获取轻量级锁失败的时候,在进入操作系统互斥锁进行阻塞之前,会进行忙等待,然后尝试获取锁,尝试一定次数后仍然没有获取锁时,则调用与该 monitor 相关联的互斥锁进入阻塞等待
在 jdk 1.6 中 Synchronized 锁一共有四种状态:无锁、偏向锁、轻量级锁、重量级锁会随着竞争情况不断升级,升级过程不可逆
轻量级锁
在内存中,对象一般由三部分组成,分别是对象头、实际数据和对齐填充,重点在与对象头,对象头中有一个我们比较关注的部分,MarkWord,MarkWord 中记录了有关锁的信息包括锁标记位,是否为偏向锁等
轻量级的锁的加锁过程:
在线程执行同步块之前,JVM 会现在当前线程的栈帧中创建一个锁记录(Lock Record),用于存储锁对象目前的 MarkWord 拷贝
如果对象没有被锁定,那么锁的标志位为 01,JVM 在执行当前线程时,会先在当前线程的栈帧中创建 LockRecord 用于存储对象的 MarkWord 的拷贝
然后虚拟机使用 CAS 操作将标记字段 MarkWord 拷贝到锁记录中,并将 MarkWord 更新为指向 LockRecord 的指针,如果更新成功了那么线程就拥有了该对象的锁,并且对象的 MarkWord 的锁标记位更新为 00 表示当前为轻量级锁状态
如果更新操作失败,如果 MarkWord 中存在指向当前线程的栈帧的指针,那么就直接使用即可,如果不存在指向当前线程栈帧的指针,说明这个锁已经被其他线程抢占了,这个时候就要锁膨胀进入重量级锁了,并且当前线程会阻塞等待,此时锁标记位为 10,MarkWord 中存储着指向重量级锁的指针
轻量级锁解锁时,会使用原子 CAS 操作将 MarkWord 替换回对象头中,替换失败则表示存在竞争,升级为重量级锁
偏向锁
JVM 会认为只有某个线程会获取锁,在大多数环境下锁是不存在竞争的,而且总是由一个线程多次获取,所以这样多次获取锁再释放锁的开销是没有意义的,为了解决这个问题,在 jdk1.6 中,引入了偏向锁的概念。
当一个线程访问同步块的时候,会在对象头和栈帧中的锁记录中存储获取了该偏向锁的线程的 ID,以后线程在进入同步代码块之前不需要进行 CAS 操作来加锁和解锁,只需要简单判断对象头中是否存储着自己的线程 ID,也就是指向自己线程的偏向锁
锁升级的过程
偏向锁状态下,线程要获取锁时,会在 MarkWord 中直接记录线程 ID,只要线程来执行代码了,会对比线程 ID 是否相等,相等直接获取锁,执行同步代码,不相等则尝试使用 CAS 修改 MarkWord 中的线程 ID ,如果修改成功则获取锁,修改不成功说明有竞争,因此会撤销偏向锁升级为轻量级锁
在轻量级锁状态下, 线程会在栈帧中创建 LockRecord 空间,用于存储锁对象的 MarkWord 的拷贝,在执行同步代码之前,尝试使用 CAS 修改 MarkWord 为指向 LockRecord 的指针,修改成功则获取锁,修改失败则会自旋,自旋一段时间没获取锁,则升级为重量级锁
在重量级锁的状态下,则依赖操作系统的 mutex 指令,需要用户态和内核态的切换,重量级锁用到 Monitor 对象,monitor 中存储着当前持有锁的线程和等待所释放的线程队列。
锁升级的过程是不可逆的
只有一个线程进入临界区,偏向锁
多个线程交替进入临界区,轻量级锁
多线程同时进入临界区,重量级锁