1. synchronized关键字的使用
synchronized关键字是对Java中的对象加锁,主要有3种使用形式
- 修饰实例(普通)方法 ,锁的是当前的实例对象;
- 修饰静态方法,锁的是当前类的Class对象,即使是不同的示例,他们共用的也是一把锁;
- 修饰代码块,所的是sybchronized括号里面的对象,这个对象可以是指定的普通实例对象,也可以是Class对象。
1.1. 修饰实例方法
修饰实例(普通)方法 ,锁的是当前的实例对象.
public class SyncExample {// synchronized 修饰实例方法,锁的是当前对象 thispublic synchronized void instanceMethod() {System.out.println(Thread.currentThread().getName() + " acquired lock on instance method");try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " released lock on instance method");}public static void main(String[] args) {SyncExample example1 = new SyncExample();SyncExample example2 = new SyncExample();// 线程测试实例方法锁new Thread(example1::instanceMethod, "Thread-1").start();new Thread(example2::instanceMethod, "Thread-2").start(); // 不同对象,互不影响// 执行结果:
// Thread-1 acquired lock on instance method
// Thread-2 acquired lock on instance method
// Thread-1 released lock on instance method
// Thread-2 released lock on instance method}
}
执行结果:
Thread-1 acquired lock on instance method
Thread-2 acquired lock on instance method
Thread-1 released lock on instance method
Thread-2 released lock on instance method
再看一个例子:
public class SyncExample {// synchronized 修饰实例方法,锁的是当前对象 public synchronized void instanceMethod() {System.out.println(Thread.currentThread().getName() + " acquired lock on instance method");try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " released lock on instance method");}public static void main(String[] args) {SyncExample example1 = new SyncExample();// 线程测试实例方法锁new Thread(example1::instanceMethod, "Thread-1").start();new Thread(example1::instanceMethod, "Thread-2").start(); // 同一个对象,阻塞// 执行结果:
// Thread-1 acquired lock on instance method
// Thread-1 released lock on instance method
// Thread-2 acquired lock on instance method
// Thread-2 released lock on instance method}
}
// 执行结果:
// Thread-1 acquired lock on instance method
// Thread-1 released lock on instance method
// Thread-2 acquired lock on instance method
// Thread-2 released lock on instance method
1.2. 修饰静态方法
由于staticMethod是static
的,并且被synchronized
修饰,所以它锁定的是SyncExample
类的Class
对象,这意味着所有SyncExample
类的实例共享同一个锁。
public class SyncExample {// synchronized 修饰实例方法,锁的是Class对象 public static synchronized void staticMethod() {System.out.println(Thread.currentThread().getName() + " acquired lock on instance method");try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " released lock on instance method");}public static void main(String[] args) {SyncExample example1 = new SyncExample();SyncExample example2 = new SyncExample();// 线程测试实例方法锁// 线程1测试实例方法锁Thread thread1 = new Thread(() -> {staticMethod();});// 线程2测试实例方法锁Thread thread2 = new Thread(() -> {staticMethod();});thread1.start();thread2.start();}
}
执行结果
1.3. 修饰代码块
在这个代码中,我们创建了两个线程thread1
和thread2
,它们分别调用example1
和example2
的blockMethod
方法。由于每个SyncExample
实例都有自己的blockLock
对象,所以这两个线程可以同时执行各自的blockMethod
方法,不会互相阻塞。这展示了同步代码块可以根据需要锁定不同的对象,从而实现更细粒度的控制。
public class SyncExample {private final Object blockLock = new Object();// synchronized 修饰代码块,锁的是指定的对象public void blockMethod() {synchronized (blockLock) { // 锁住 blockLock 对象System.out.println(Thread.currentThread().getName() + " acquired lock on block");try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " released lock on block");}}public static void main(String[] args) {SyncExample example1 = new SyncExample();SyncExample example2 = new SyncExample();// 线程1测试同步代码块锁Thread thread1 = new Thread(() -> {example1.blockMethod();});// 线程2测试同步代码块锁Thread thread2 = new Thread(() -> {example2.blockMethod();});thread1.start();thread2.start();}
}
如果是下面的代码呢?
public class SyncExample {private final Object blockLock = new Object();// synchronized 修饰代码块,锁的是指定的对象public void blockMethod() {synchronized (blockLock) { // 锁住 blockLock 对象System.out.println(Thread.currentThread().getName() + " acquired lock on block");try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " released lock on block");}}public static void main(String[] args) {SyncExample example1 = new SyncExample();// 线程1测试同步代码块锁Thread thread1 = new Thread(() -> {example1.blockMethod();});// 线程2测试同步代码块锁Thread thread2 = new Thread(() -> {example1.blockMethod();});thread1.start();thread2.start();}
}
2. synchronized的锁原理
2.1. 同步代码块
synchronized
代码块是由一对 monitorenter
和 monitorexit
指令实现的,Monitor
对象是同步的基本实现单元。
public void foo(Object lock) {synchronized (lock) {lock.hashCode();}}// 上面的 Java 代码将编译为下面的字节码public void foo(java.lang.Object);Code:0: aload_11: dup2: astore_23: // 上面的 Java 代码将编译为下面的字节码4: aload_15: invokevirtual java/lang/Object.hashCode:()I8: pop9: aload_210: monitorexit11: goto 1914: astore_315: aload_216: monitorexit17: aload_318: athrow19: returnException table:from to target type4 11 14 any14 17 14 any
synchronized
在修饰同步代码块时,是由 monitorenter
和 monitorexit
指令来实现同步的。进入 monitorenter
指令后,线程将持有 Monitor
对象,退出 monitorenter
指令后,线程将释放该 Monitor
对象。
2.2. 同步方法
synchronized
修饰同步方法时,会设置一个 ACC_SYNCHRONIZED
标志。当方法调用时,调用指令将会检查该方法是否被设置 ACC_SYNCHRONIZED
访问标志。如果设置了该标志,执行线程将先持有 Monitor
对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该 Mointor
对象,当方法执行完成后,再释放该 Monitor
对象。 究其根源使用的还是monitorenter
和 monitorexit
。
public static synchronized void target(){}
上面的 Java 代码将编译为下面的字节码
3. synchronized可重入原理
synchronized
可重入的原理是基于 Java 中的每个对象都有一个锁(或者说是监视器锁,Monitor),以及每个线程都有一个锁计数器。
- 锁对象:当一个线程执行到一个
synchronized
同步代码块或者同步方法时,它需要先获取到该代码块或方法对应的对象锁。如果该锁已经被其他线程占用,则当前线程会阻塞,直到该锁被释放。 - 线程的锁计数器:每个线程都有一个锁计数器,用于记录该线程持有的锁的数量。当线程首次进入一个
synchronized
同步代码块时,它会尝试获取锁,如果成功,锁计数器会递增。 - 可重入性:如果同一个线程再次尝试获取同一个对象的锁(即再次进入
synchronized
同步代码块或者调用synchronized
同步方法),因为该线程已经持有该对象的锁,所以它可以再次成功获取锁,并且锁计数器再次递增。这就是synchronized
的可重入性。 - 释放锁:当线程执行完
synchronized
同步代码块或方法后,它会释放锁,锁计数器会递减。只有当锁计数器减到0时,锁才会被真正释放,其他线程才有机会获取该锁。 - 避免死锁:
synchronized
的可重入性避免了死锁的发生。如果一个synchronized
方法在其执行过程中调用了另一个synchronized
方法,并且这两个方法都需要同一个对象的锁,那么可重入性确保了同一个线程可以再次获取到锁,从而避免了死锁。 - 锁的释放:当线程执行完所有的
synchronized
同步代码块或方法后,锁计数器会减到0,此时锁会被释放,其他线程可以获取该锁。
4. synchronized锁升级
4.1. Java对象头
在 JDK1.6 JVM 中,对象实例在堆内存中被分为了三个部分:对象头、实例数据和对齐填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组长度三部分组成。 Mark Word 记录了对象和锁有关的信息。Mark Word 在 64 位 JVM 中的长度是 64bit,我们可以一起看下 64 位 JVM 的存储结构是怎么样的。如下图所示:
- 锁状态:2位,用于标识锁的状态。
- 是否偏向锁:1位,表示对象是否启用偏向锁。
- 分代年龄:4位,用于记录对象的分代年龄,用于垃圾收集。
- 线程ID:偏向锁时记录持有偏向锁的线程ID。
- Epoch:偏向锁时记录一个时间戳,用于记录偏向锁的有效期。
- hashCode:无锁时存储对象的哈希码。
- 其他:根据锁状态的不同,Mark Word 中还可能包含其他信息,如轻量级锁时指向栈中锁记录的指针,重量级锁时指向互斥量(重量级锁)的指针等
锁升级功能主要依赖于 Mark Word 中的锁标志位和是否偏向锁标志位,synchronized
同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。 Java 1.6 引入了偏向锁和轻量级锁,从而让 synchronized
拥有了四个状态:
- 无锁状态(unlocked)
- 偏向锁状态(biasble)
- 轻量级锁状态(lightweight locked)
- 重量级锁状态(inflated)
4.2. 锁升级过程
锁升级是 JVM 为了提高锁的性能而采取的一种优化策略,主要经历了以下几个阶段:
- 偏向锁:当一个线程尝试获取锁时,如果该锁是偏向锁,并且当前线程是第一次获取该锁,JVM 会在 Mark Word 中记录该线程的ID,并标记为偏向锁状态。这样,当该线程再次获取锁时,就无需进行任何同步操作,直接获取锁
- 轻量级锁:如果另一个线程尝试获取同一个偏向锁,JVM 会先撤销偏向锁,然后将锁升级为轻量级锁。轻量级锁使用 CAS 操作尝试获取锁,如果成功,则在栈中创建一个锁记录,用于存储 Mark Word 的备份,并且将对象本来的MarkWord种存储的信息换位指向自己的LR(LOCK RECORD)指针。
- 重量级锁:如果轻量级锁竞争失败,即多个线程同时通过 CAS 尝试获取锁,JVM 会将轻量级锁升级为重量级锁。此时,Mark Word 会指向一个 Monitor 对象,线程获取锁失败后会被放入 Monitor 的 EntryList 集合中等待。
5. synchronized的锁优化
除了锁升级优化,Java 还使用了编译器对锁进行优化。 以下是一些主要的编译器锁优化技术:
- 锁消除(Lock Elision):
锁消除是 JVM 提供的一种高级优化技术,它允许编译器在确定锁对象不会引起线程安全问题时,减少不必要的加锁操作,从而提升程序性能。这种优化对开发者是透明的,由 JVM 自动进行。例如,如果一个方法内部创建了一个局部变量,并且该变量仅在当前方法栈帧中存在,不会被其他线程并发访问,JVM 就可以进行锁消除的优化
- 自适应锁(Adaptive Locking):
自适应锁优化是指 JVM 根据过去锁获取的行为自适应地选择是使用自旋锁还是挂起线程。如果一个锁通常被持有的时间很短,JVM 可能会选择自旋;如果一个锁通常被持有的时间很长,JVM 则可能会选择挂起线程。这种优化可以在运行时根据锁的使用模式动态调整,以实现更好的性能
- 锁粗化(Lock Coarsening):
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
锁粗化是将多个相邻的同步块合并成一个较大的同步块的过程,这些同步块使用相同的锁对象。如果编译器不能通过锁消除来减少锁的开销,它可能会通过锁粗化来减少开销。这种方法可以减少锁的获取和释放次数,从而减少指令数量和内存总线上的同步流量,但可能会增加锁的持有时间,从而增加其他线程被阻塞的时间
参考:
1. Java并发核心机制 | JAVACORE
2. https://zhuanlan.zhihu.com/p/82857579
3. Java对象结构详解【MarkWord 与锁的实现原理】 - Java程序员进阶 - 博客园
4. https://zhuanlan.zhihu.com/p/290991898