`synchronized`关键字的锁升级过程是Java为了提高锁的性能,减少在无竞争或多线程轻度竞争情况下的开销而设计的一套机制。这一过程主要涉及以下四个阶段:
1. 无锁状态:当一个对象刚创建时,并没有锁与其关联,处于无锁状态。
2. 偏向锁(Biased Locking):
-初始化:当第一个线程访问同步代码块或方法时,JVM会将对象头的MarkWord设置为偏向锁,并记录这个线程的ID。
-偏向:如果后续的访问仍然是由这个线程发起的,无需进行同步操作,直接执行代码即可,因为锁已经偏向于这个线程
-撤销:如果有其他线程尝试访问这个同步块,偏向锁将被撤销,并进入轻量级锁状态。撤销时会有一定的开销,包括检查偏向锁标识、CAS操作尝试清除偏向锁等。
3.轻量级锁(Lightweight Locking):
-自旋:当有第二个线程尝试获取锁时,偏向锁被撤销,转换为轻量级锁。线程会在自己的栈帧中创建一个称为Lock Record的空间,用于存储锁的Mark Word的拷贝。
-CAS操作:然后通过CAS操作尝试将对象头的Mark Word替换为指向Lock Record的指针。如果成功,线程获得锁;失败,则说明存在竞争,线程将自旋一段时间,不断尝试CAS操作直到成功或达到自旋上限。
-自旋失败:如果自旋超过一定次数(自旋阈值)仍未获得锁,轻量级锁将升级为重量级锁。
4.重量级锁(Heavyweight Locking):
-监视器锁:当多个线程竞争激烈,轻量级锁无法有效处理时,锁升级为重量级锁。此时,JVM会调用操作系统的互斥量(mutex)来实现线程阻塞和唤醒,这会导致线程挂起和恢复,开销较大。
-阻塞与唤醒:未获取到锁的线程会被阻塞,进入等待队列,而持有锁的线程执行完毕后,会唤醒队列中的下一个等待线程。
public class Synchronized2Example { // 对象锁示例 public synchronized void method1() { System.out.println("Method 1 executing by " + Thread.currentThread().getName()); try { Thread.sleep(2000); // 模拟耗时操作,增加锁竞争的可能性 } catch (InterruptedException e) { e.printStackTrace(); } } // 代码块锁示例,锁定的是object实例 private Object object = new Object(); public void method2() { synchronized (object) { // 这里使用的是对象锁 System.out.println("Method 2 executing by " + Thread.currentThread().getName()); try { Thread.sleep(2000); // 模拟耗时操作 } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { Synchronized2Example example = new Synchronized2Example(); Thread t1 = new Thread(() -> example.method1(), "Thread-1"); Thread t2 = new Thread(() -> example.method2(), "Thread-2"); t1.start(); t2.start(); } }
运行这段程序,可以看到两个线程分别尝试访问这两个同步方法或代码块。虽然这个示例并不能直观展示锁升级的过程,但它可以帮助理解synchronized
如何保证线程间的同步。
锁升级的整个过程是不可逆的,一旦升级到重量级锁,就不会再降级回轻量级锁或偏向锁。这种设计旨在根据竞争程度动态调整锁策略,以达到最佳性能平衡。
注:
下面是对各个锁的解释
1.偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取
该锁就可以直接获取到了,也就是支持锁重入
2.轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有
第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
3.如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
4.自旋锁∶自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。