Java并发编程实战:深入剖析Synchronized与Lock的底层实现原理
在Java并发编程中,synchronized关键字和Lock接口是实现线程同步、保障线程安全的两种核心机制。它们虽然在功能上类似,但在设计哲学、使用方式和底层实现上存在着显著差异。理解其底层原理,是编写高效、可靠并发程序的关键。
synchronized的底层实现原理
synchronized是Java语言内置的互斥同步锁,其实现深度集成在JVM中,主要依赖于对象头中的Mark Word和监控器(Monitor)机制。
当一个线程尝试进入synchronized修饰的代码块或方法时,JVM会首先检查目标对象的Mark Word。Mark Word是对象头部的一部分,用于存储对象的哈希码、分代年龄、锁状态等信息。在无锁状态下,Mark Word存储的是对象自身的运行时数据。当锁被竞争时,这些比特位会被用来存储指向锁记录的指针或重量级锁的指针。
synchronized的锁升级过程是其性能优化的核心。为了减少获得锁和释放锁带来的性能开销,Java SE 1.6引入了偏向锁、轻量级锁和重量级锁的概念,并规定了锁可以按照“偏向锁 -> 轻量级锁 -> 重量级锁”的方向进行升级,但不可降级。
偏向锁用于优化同一个线程重复获取锁的场景。当一个线程第一次获得锁时,JVM会使用CAS操作将线程ID记录到对象的Mark Word中,并将锁标志位设置为偏向模式。之后该线程再次进入同步块时,只需检查Mark Word中的线程ID是否为当前线程,如果是,则无需进行任何同步操作,直接执行。
轻量级锁用于优化多个线程在不同时间点申请锁的场景,即不存在激烈的竞争。在代码进入同步块时,如果锁对象处于无锁状态,JVM会在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word拷贝。然后,JVM会使用CAS操作尝试将对象的Mark Word更新为指向该锁记录的指针。如果更新成功,则该线程获得锁,并将锁标志位设置为00,表示轻量级锁状态。
重量级锁当轻量级锁竞争激烈时(即有多个线程同时竞争),轻量级锁会膨胀为重量级锁。此时,Mark Word中存储的是指向一个操作系统层面的互斥量(mutex)的指针,等待锁的线程会进入阻塞状态,需要操作系统进行线程的调度和上下文切换,因此开销最大。
整个synchronized的加锁、解锁过程均由JVM在底层实现,对开发者透明。其优势在于使用简便,无需手动释放锁,但缺点是功能相对单一,无法实现一些高级功能如尝试非阻塞获取锁、公平锁、中断等待锁的线程等。
Lock接口的底层实现原理
Lock接口是Java SE 5之后在java.util.concurrent.locks包中提供的一种显式锁机制。与synchronized不同,Lock提供了更丰富的功能,其底层实现主要依赖于一个同步器框架——AbstractQueuedSynchronizer(AQS)。
AQS是构建锁和其他同步组件的基础框架,它使用了一个整型的volatile变量(state)来表示同步状态,并通过一个内置的FIFO队列来完成资源获取线程的排队工作。ReentrantLock、Semaphore、CountDownLatch等常用同步工具都是基于AQS构建的。
以ReentrantLock为例,其内部定义了一个继承自AQS的同步器(Sync),并有两个实现:非公平的NonfairSync和公平的FairSync。锁的获取与释放本质上是操作AQS中的state变量。
当一个线程调用lock()方法时:
1. 对于非公平锁,它会首先尝试使用CAS操作将state从0设置为1,如果成功,则将独占锁的线程设置为当前线程。
2. 如果CAS失败(即锁已被占用),则调用AQS的acquire(1)方法。acquire方法会再次尝试获取锁(tryAcquire),如果失败,则会将当前线程封装为一个Node节点,通过CAS操作将其加入到CLH队列的尾部,然后进入自旋或阻塞状态,等待前驱节点释放锁后唤醒自己。
公平锁与非公平锁的实现差异在于tryAcquire方法。公平锁在尝试获取锁时,会先检查AQS的等待队列中是否有比自己等待时间更长的线程(hasQueuedPredecessors()),如果有,则获取失败,直接排队,保证了“先来后到”的公平性。而非公平锁则不论先后,直接尝试抢占。
与synchronized的“自动”管理不同,Lock的加锁(lock())和解锁(unlock())必须由程序员显式地成对出现。这种显式控制带来了更大的灵活性,例如可以响应中断(lockInterruptibly())、尝试获取锁(tryLock())、实现公平性策略等。
Synchronized与Lock的对比与选择
从底层实现上看,synchronized是JVM层面的实现,而Lock是API层面的实现(底层使用AQS + CAS + volatile)。
在性能上,随着Java版本的迭代,synchronized经过不断优化(如锁升级),其性能与Lock相比已相差无几,甚至在低竞争场景下更具优势。但在高竞争、需要高级功能(如公平性、可中断、超时、多个条件变量)的场景下,Lock依然是更好的选择。
选择synchronized还是Lock,取决于具体的应用场景。synchronized语法简洁,不易出错,适合大多数简单的同步需求。而Lock则提供了更精细的控制和更高的灵活性,适合构建复杂的同步策略,如实现一个高性能的连接池或自定义的同步组件。
总之,深入理解synchronized的锁升级和Lock的AQS框架,有助于开发者在不同场景下做出最合适的技术选型,并编写出高效、健壮的并发程序。
839

被折叠的 条评论
为什么被折叠?



