synchronized关键字学习总结


1. 锁的基本概念

1.1 锁的类型与作用

锁是并发编程中用于控制多个线程对共享资源访问的机制。在Java中,锁可以分为内置锁和显式锁两大类。

  • 内置锁:通过synchronized关键字实现,是一种隐式锁,自动加锁和解锁。
  • 显式锁:通过Lock接口实现,如ReentrantLock,需要手动加锁和解锁。
    锁的主要作用是保证线程安全,防止多个线程同时访问共享资源,从而避免数据不一致的问题。

1.2 synchronized关键字

synchronized是Java中实现同步的一种关键字,它可以用来修饰方法或者代码块。

  • 当修饰方法时,锁是当前实例对象,进入同步方法的线程必须先获得对象锁。
  • 当修饰代码块时,锁是括号内指定的对象,进入同步代码块的线程必须先获得该对象的锁。
    synchronized通过对象内部的Monitor对象来实现同步,其中涉及到monitorenter和monitorexit指令。

1.3 Lock接口与实现

Lock接口是Java并发包java.util.concurrent.locks提供的一套显示锁机制。

  • 常见的Lock实现有ReentrantLock、ReadWriteLock等。
  • Lock接口比synchronized提供了更灵活的锁定机制,如尝试非阻塞地获取锁(tryLock)、可中断地获取锁(lockInterruptibly)以及支持公平性等。
  • Lock接口的实现通常基于CAS(Compare-And-Swap)操作,是一种无锁的非阻塞算法。
  1. 锁的升级原理

    2.1 锁状态与升级过程

    Java中的锁状态有四种:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁状态的升级过程是单向的,从低到高。

    • 无锁状态:初始状态,没有线程执行同步代码。
    • 偏向锁状态:当线程获得偏向锁后,锁会偏向该线程,其他线程在尝试获取锁时会检查是否为偏向线程。
    • 轻量级锁状态:当有其他线程尝试获取偏向锁时,偏向锁会升级为轻量级锁,此时会通过CAS操作尝试获取锁。
    • 重量级锁状态:当CAS操作无法成功获取锁时,轻量级锁会升级为重量级锁,此时会涉及到操作系统的互斥量(Mutex)。

    2.2 锁升级的条件

    锁的升级条件主要取决于锁的竞争程度。

    • 如果一个锁几乎没有竞争,那么它可能会长时间处于偏向锁状态。
    • 如果有轻微竞争,可能会升级为轻量级锁。
    • 如果竞争激烈,频繁发生CAS操作失败,那么锁会升级为重量级锁。

    2.3 锁升级的影响

    锁升级对性能有重要影响。

    • 偏向锁和轻量级锁的开销较小,适用于竞争不激烈的场景。
    • 重量级锁的开销较大,但可以有效地管理高竞争环境下的线程同步。
    • 锁升级过程中的CAS操作和互斥量的使用都会影响系统的性能,因此需要根据实际情况选择合适的锁策略。

2. synchronized的锁升级机制

2.1 无锁状态

在无锁状态,对象的Mark Word中不会存储线程信息,此时对象没有被任何线程锁定。JVM优化此时的访问,尽可能避免锁的开销,提高性能。

2.2 偏向锁

偏向锁是JVM为了减少无竞争情况下的锁开销而引入的一种锁状态。当一个线程获取偏向锁后,对象的Mark Word中会存储偏向的线程ID。在后续的执行中,如果仍然是同一个线程访问该同步代码块,JVM就可以判断出来,并允许该线程无锁地执行同步代码。

  • 锁获取:首次线程获取偏向锁时,会通过CAS操作将对象头的Mark Word中的线程ID替换为当前线程的ID。
  • 锁释放:偏向锁的释放不是主动进行的,而是在遇到其他线程竞争时才会发生。偏向锁的撤销需要等待全局安全点,撤销过程中会将偏向锁升级为轻量级锁。

2.3 轻量级锁

轻量级锁是为了减少线程阻塞而设计的。当偏向锁撤销后,或者多个线程交替执行同步代码块时,锁会升级为轻量级锁。

  • 锁获取:线程尝试获取轻量级锁时,会先在当前线程的栈帧中创建一个Lock Record,然后将对象头的Mark Word复制到Lock Record中。接着,使用CAS操作尝试将对象头的Mark Word更新为指向Lock Record的指针。
  • 锁释放:释放轻量级锁时,也是通过CAS操作将对象头的Mark Word恢复到原始状态。

2.4 重量级锁

当轻量级锁无法满足性能需求时,会升级为重量级锁。此时,未获取到锁的线程会被阻塞,并进入等待状态,直到持有锁的线程释放锁。

  • 锁获取:线程尝试获取重量级锁时,如果锁已经被其他线程持有,则会被阻塞并放入Monitor的等待队列中。当锁被释放时,线程会被唤醒并尝试重新获取锁。
  • 锁释放:持有锁的线程执行完同步代码块后,会释放锁,并通过Monitor唤醒等待队列中的线程,让它们尝试获取锁。

锁升级的过程是动态的,JVM会根据当前的竞争情况选择合适的锁策略。在无竞争或低竞争的情况下,偏向锁和轻量级锁能够显著提高程序的并发性能;而在高竞争的情况下,重量级锁则提供了可靠的线程同步机制。这种设计使得Java的synchronized关键字在不同的场景下都能表现出良好的性能。

3. 锁升级原理

3.1 偏向锁的获取与撤销

偏向锁是为了在无多线程竞争的情况下减少锁操作的开销。当一个线程访问同步代码块时,如果锁对象的Mark Word中存储的是偏向模式(锁标志位为1),并且线程ID与当前线程ID相匹配,则无需进行任何额外的同步操作。如果线程ID不匹配,JVM会通过CAS操作尝试将Mark Word中的线程ID替换为当前线程ID,如果成功,则该线程获得偏向锁。若遭遇竞争,偏向锁会撤销并升级为轻量级锁。

  • 偏向锁的撤销通常发生在多个线程竞争同一锁对象时。JVM会等待全局安全点(所有线程都处于暂停状态)来撤销偏向锁,并将Mark Word重置为无锁状态或升级为轻量级锁状态。
  • 根据性能测试,偏向锁在单线程访问同步代码块的场景下,可以显著提高性能,因为避免了重量级锁的开销。

3.2 轻量级锁的获取与释放

轻量级锁是为了减少线程因获取重量级锁而产生的挂起和唤醒的开销。当偏向锁升级为轻量级锁后,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record),并使用CAS操作尝试将对象的Mark Word替换为指向Lock Record的指针。

  • 如果CAS操作成功,当前线程即获得轻量级锁。此时,Mark Word的锁标志位变为00,表示对象处于轻量级锁状态。
  • 轻量级锁的释放同样通过CAS操作,将对象的Mark Word恢复到原来的偏向锁状态或无锁状态。如果CAS操作失败,说明存在锁竞争,轻量级锁会进一步升级为重量级锁。

3.3 重量级锁的获取与释放

重量级锁是依赖于操作系统的Mutex Lock实现的。当轻量级锁无法满足性能需求时,会升级为重量级锁。此时,JVM会将对象的Mark Word替换为指向Monitor对象的指针,线程的获取锁操作将变为对Monitor的进入和退出操作。

  • 线程在尝试获取重量级锁时,如果锁已被其他线程持有,则会被阻塞并放入Monitor的等待队列中。线程只有在Monitor的计数器为0时才能成功获取锁,进入临界区。
  • 重量级锁的释放涉及到将Monitor的计数器减1,并唤醒等待队列中的线程。如果计数器减至0,持有锁的线程会释放锁,其他线程可以尝试获取锁。这一过程涉及到用户态和内核态的切换,因此性能开销较大。

4. 锁升级的影响因素

4.1 线程竞争

线程竞争是影响锁升级的主要因素之一。在Java中,当多个线程尝试同时访问同一把锁时,就会发生竞争。这种竞争会导致锁状态从偏向锁逐步升级到轻量级锁,最终可能升级为重量级锁。锁的升级过程是单向的,不可逆,目的是为了提高获得锁和释放锁的效率。

  • 竞争程度:线程竞争的激烈程度直接影响锁的升级。如果一个锁频繁地被多个线程竞争,那么它更有可能从偏向锁升级到轻量级锁,甚至重量级锁。这种升级是为了适应更高的并发需求,但同时也会增加系统的开销。

  • 锁持有时间:线程持有锁的时间长短也会影响锁的升级。如果一个线程持有锁的时间很短,那么偏向锁和轻量级锁由于其无阻塞的特性,可以提供更好的性能。相反,如果持有时间长,重量级锁可能会更适合,因为它可以减少上下文切换的开销。

4.2 JVM参数设置

JVM提供了一系列的参数来控制锁的行为,这些参数的设置会影响锁的升级。

  • -XX:+UseBiasedLocking:这个参数控制是否启用偏向锁。偏向锁可以提高单线程访问同步资源的性能,但如果系统是高并发的,偏向锁的撤销可能会带来额外的性能损耗。

  • -XX:BiasedLockingStartupDelay:这个参数可以设置偏向锁的启动延迟时间,以避免在JVM启动初期由于线程竞争导致的不必要的偏向锁撤销。

  • -XX:-UseSpinning:自旋锁是轻量级锁的一种实现方式,通过设置这个参数可以禁用自旋,减少在获取锁时的CPU消耗。

4.3 系统资源与性能

系统资源的可用性和性能也会影响锁的升级。

  • CPU核心数:多核CPU可以更好地处理线程竞争,减少锁升级到重量级锁的可能性。在单核CPU上,线程竞争更容易导致锁升级。

  • 内存大小:足够的内存可以减少垃圾回收的频率,从而减少由于GC导致的锁升级。内存不足可能会导致频繁的GC,进而影响锁的状态。

  • 垃圾回收器:不同的垃圾回收器对性能的影响不同。例如,G1垃圾回收器在处理大堆内存时性能较好,而CMS垃圾回收器则更适合低延迟的应用场景。选择合适的垃圾回收器可以减少锁升级的频率。

在实际应用中,理解和监控锁的状态对于优化系统性能至关重要。通过分析线程竞争情况、调整JVM参数以及优化系统资源配置,可以有效地控制锁的升级,从而提高Java应用程序的性能。

5. 锁的优化策略

5.1 锁消除与锁粗化

锁消除和锁粗化是Java虚拟机在即时编译期间对锁进行的两种优化策略,旨在减少锁操作的开销,提高程序的运行效率。

  • 锁消除:通过逃逸分析,JVM能够判断一个对象是否在多个线程间共享。如果一个对象在方法中被创建,并在该方法中通过synchronized锁进行保护,且没有被传递到其他线程,那么JVM在编译期间会消除这个对象上的锁。例如,StringBuffer的append方法在单线程环境下可以被优化,因为StringBuffer对象没有被共享。
  • 锁粗化:当一系列连续的锁操作被检测到时,JVM可能会将这些操作合并为一个更大范围的锁,以减少频繁的锁获取和释放所带来的性能损耗。例如,在循环中对同一个对象加锁和解锁多次,JVM可能会将锁的范围扩展到整个循环体之外,从而只需要一次锁操作。

5.2 自旋锁

自旋锁是一种避免线程挂起的锁策略,当一个线程尝试获取一个已经被占用的锁时,它会在原地进行自旋(忙等待),直到锁被释放。

  • 自旋锁的优点是避免了线程状态的切换,减少了上下文切换的开销,适用于锁持有时间短的场景。
  • 自旋锁的缺点是如果锁被长时间持有,自旋线程会占用CPU资源,导致系统吞吐量下降。因此,自旋锁通常与自适应自旋结合使用,根据锁的争夺情况动态调整自旋的次数。
  • 自适应自旋锁:JVM会根据历史锁的争夺情况来决定自旋的次数,如果历史数据显示锁的争夺不激烈,那么会增加自旋的次数;反之,则减少自旋次数,甚至直接挂起线程。

5.3 锁分段

锁分段是一种将数据结构分解成多个段,每个段独立加锁的策略,常见于Java中的ConcurrentHashMap实现。

  • 锁分段可以显著提高并发程序的性能,因为它允许多个线程同时操作不同段的数据,减少了线程间的相互阻塞。
  • 锁分段的实现通常需要维护一个锁数组,每个数组元素对应数据结构的一个段。当一个线程需要操作数据时,它只需要获取对应段的锁。
  • 锁分段的挑战在于如何合理地划分数据段,以及如何管理锁数组,以确保数据结构的一致性和线程安全。

6. 锁的其他实现

6.1 ReentrantLock

ReentrantLock 是 java.util.concurrent.locks 包下的一个类,提供了与 synchronized 相比更灵活的锁定机制。

  • 特性:ReentrantLock 支持尝试非阻塞地获取锁(tryLock()),可中断地获取锁(lockInterruptibly()),以及超时获取锁(tryLock(long timeout, TimeUnit unit)),这些是 synchronized 所不具备的。
  • 性能:在高并发环境下,ReentrantLock 的性能通常优于 synchronized,因为它的实现避免了线程上下文切换的开销。
  • 可重入性:ReentrantLock 同样支持可重入性,即同一线程可以多次获得同一把锁。
  • 条件变量:ReentrantLock 提供了条件变量(Condition),允许线程在某些条件下等待或者被唤醒,这是 synchronized 所没有的。
  • 公平性:ReentrantLock 可以选择是否公平地分配锁,而 synchronized 默认是非公平的。

6.2 ReadWriteLock

ReadWriteLock 是一种允许多个读操作同时进行,但写操作是排他的锁。

  • 组成:ReadWriteLock 由两个锁组成,一个读锁和一个写锁,允许多个线程同时读取,但写入时需要独占锁。
  • 性能:在读写操作频繁且写操作相对较少的场景下,ReadWriteLock 可以显著提高性能,因为它减少了读操作之间的不必要等待。
  • 应用场景:适用于读多写少的场景,如缓存系统,可以提高系统的并发性能。

6.3 StampedLock

StampedLock 是一种新颖的锁机制,提供了三种模式的锁定:乐观读锁、悲观读锁和写锁。

  • 乐观读:乐观读锁是一种非阻塞读锁,它允许多个线程同时进行读操作,只有当尝试获取写锁时,才会尝试降级或升级锁状态。
  • 悲观读:悲观读锁是一种阻塞读锁,适用于写操作频繁的场景,可以减少写操作时的等待时间。
  • 写锁:写锁是排他的,任何时候只能有一个线程持有写锁。
  • 性能:StampedLock 在某些场景下可以提供比 ReentrantLock 更好的性能,因为它减少了锁的争用和上下文切换。
  • 灵活性:StampedLock 提供了更多的锁操作选项,允许开发者根据具体场景选择最合适的锁定策略。

2. Java 中 Synchronized 的锁升级原理

2.1 锁状态变化

Synchronized锁的状态变化是从无锁状态到偏向锁、轻量级锁、再到重量级锁的过程,这一过程是单向的,不可逆。

  • 无锁状态:初始状态下,对象没有被锁定,线程可以自由访问。
  • 偏向锁:当线程获得偏向锁后,锁会偏向于该线程,减少锁的竞争开销。
  • 轻量级锁:当多个线程竞争时,偏向锁会升级为轻量级锁,此时线程会通过CAS操作尝试获取锁。
  • 重量级锁:当轻量级锁无法满足需求时,会进一步升级为重量级锁,此时线程会被操作系统挂起,直到获取锁。

2.2 偏向锁原理

偏向锁的设计初衷是为了减少无竞争情况下的锁开销,它通过在对象头和线程栈帧中记录偏向的线程ID来实现。

  • 线程获得偏向锁:当线程首次访问同步代码块时,会在对象头和当前线程的栈帧中记录偏向的线程ID。
  • 偏向锁的撤销:当有其他线程尝试获取偏向锁时,偏向锁会撤销,并尝试升级为轻量级锁。

2.3 轻量级锁原理

轻量级锁是为了减少线程阻塞而设计的,它通过CAS操作来尝试获取锁,避免了线程的立即挂起。

  • CAS操作:轻量级锁通过CAS操作尝试将对象头的Mark Word替换为指向线程栈帧中锁记录的指针。
  • 自旋等待:如果CAS操作失败,线程会进行自旋等待,即让当前线程空转一段时间,然后再次尝试获取锁。
  • 锁升级:如果自旋等待达到一定次数仍然没有获取到锁,轻量级锁会升级为重量级锁。

2.4 重量级锁原理

重量级锁是依赖于操作系统的互斥量(Mutex)实现的,当轻量级锁无法满足性能需求时,会升级为重量级锁。

  • 线程阻塞:重量级锁会导致未获取到锁的线程被阻塞,并进入等待状态。
  • 锁的获取与释放:线程在获取重量级锁后,可以执行同步代码块,执行完成后释放锁,唤醒等待队列中的线程。

2.5 锁升级的影响

锁升级过程对性能有重要影响,合理的锁策略可以减少锁的竞争,提高系统的并发性能。

  • 性能考量:锁升级过程中,系统会根据当前的竞争情况选择合适的锁策略,以保证在保证正确性的同时获得良好的性能。
  • 优化策略:JVM通过锁消除、锁粗化等优化策略,进一步提高了synchronized的性能。

文章作者: ring2
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ring2 !
  目录