锁
乐观锁 vs. 悲观锁
这是两种广义的并发控制思想。
-
悲观锁 (Pessimistic Lock)
- 理念:总认为数据会被其他线程修改,所以在操作数据前先加锁,阻止其他线程访问。
- 实现:Java中的
synchronized关键字和Lock的实现类(如ReentrantLock)都是悲观锁。 - 适用场景:写操作多的场景。先加锁能保证数据写入的正确性。
-
调用示例: ```java // synchronized public synchronized void testMethod() { // 操作同步资源 }
// ReentrantLock private ReentrantLock lock = new ReentrantLock(); public void modifyPublicResources() { lock.lock(); // 操作同步资源 lock.unlock(); } ```
-
乐观锁 (Optimistic Lock)
- 理念:总认为数据不会被其他线程修改,所以不加锁。只在更新数据时,检查在此期间数据是否被其他线程修改过。
- 实现:通过无锁编程实现,最常用的是CAS (Compare And Swap) 算法。Java的原子类(如
AtomicInteger)就是基于CAS实现的。 - 适用场景:读操作多的场景。不加锁的特性可以大幅提升读取性能。
- 调用示例:
java private AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.incrementAndGet(); // 原子自增操作
CAS (Compare And Swap)
CAS是一种无锁算法,它包含三个操作数: 1. 内存值 V 2. 预期值 A 3. 新值 B
操作流程:当且仅当内存值V等于预期值A时,才用新值B原子性地更新V的值。否则,不做任何操作并通常进行重试(这个重试过程也叫自旋)。
源码分析 (AtomicInteger.incrementAndGet)
1. 底层调用Unsafe类的getAndAddInt方法。
2. 该方法在一个do-while循环中:
* 首先,读取当前内存中的值v (getIntVolatile)。
* 然后,调用compareAndSwapInt尝试将内存值从v更新为v + delta。
* 如果更新失败(意味着在读取v之后,有其他线程修改了内存值),循环继续重试,直到成功为止。
CAS的三个主要问题及解决方案
1. ABA问题:一个值从A变为B又变回A,CAS检查时会误认为没有变化。
* 解决:使用版本号机制。JDK 1.5+ 提供了AtomicStampedReference来解决。
2. 循环时间长开销大:如果自旋长时间不成功,会给CPU带来巨大开销。
3. 只能保证一个共享变量的原子操作:对多个变量操作时,无法保证原子性。
* 解决:将多个变量封装成一个对象,然后使用AtomicReference进行CAS操作。
自旋锁 vs. 适应性自旋锁
-
背景:线程的阻塞和唤醒需要操作系统切换CPU状态,开销较大。如果同步代码块执行时间很短,这个开销可能得不偿失。
-
自旋锁 (Spin Lock)
- 理念:当一个线程请求锁但锁被占用时,该线程不被挂起(不放弃CPU),而是执行一个忙循环(即“自旋”),等待锁的释放。
- 优点:避免了线程切换的开销。
- 缺点:占用CPU时间。如果锁被占用时间长,会浪费处理器资源。
- 实现:其原理也是CAS。
AtomicInteger的do-while循环就是一个自旋操作。 - 限制:自旋有次数限制(默认10次),超过限制仍未获得锁,线程就会被挂起。
-
适应性自旋锁 (Adaptive Spin Lock)
- 理念:自旋的时间(次数)不再是固定的,而是动态调整的。
- 调整依据:根据前一次在同一个锁上的自旋时间以及锁拥有者的状态来决定。
- 规则:
- 如果上一次自旋成功获取了锁,且持有锁的线程正在运行,虚拟机会认为这次自旋也很可能成功,并允许更长时间的自旋。
- 如果对于某个锁,自旋很少成功,那么后续可能会省略自旋过程,直接阻塞线程。
锁状态:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
这四种状态是针对synchronized关键字的锁优化机制,锁的状态会随着竞争的激烈程度单向升级(不能降级)。
基础概念
- Java对象头 (Object Header):
synchronized的锁信息就存储在这里。- Mark Word:存储对象的HashCode、分代年龄和锁标志位。其内容会根据锁状态而改变。
- Klass Pointer:指向对象所属类的元数据指针。
- Monitor:每个Java对象都关联一个Monitor。
synchronized通过获取对象的Monitor来实现同步。Monitor依赖于底层操作系统的Mutex Lock (互斥锁),这涉及到用户态和内核态的切换,开销大,因此被称为重量级锁。
四种锁状态
| 锁状态 | 锁标志位 | Mark Word 存储内容 | 解决的问题 |
|---|---|---|---|
| 无锁 | 01 | 对象HashCode、分代年龄、偏向锁标志(0) | - |
| 偏向锁 | 01 | 偏向的线程ID、偏向时间戳、偏向锁标志(1) | 只有一个线程访问同步块,减少不必要的CAS |
| 轻量级锁 | 00 | 指向栈中锁记录(Lock Record)的指针 | 多个线程交替执行同步块,通过自旋避免阻塞 |
| 重量级锁 | 10 | 指向重量级锁(Monitor)的指针 | 多线程同时竞争,通过阻塞-唤醒机制处理 |
-
无锁 (No Lock)
- 不锁定资源,所有线程都能访问,但只有一个能修改成功。
- 通过CAS循环尝试修改共享资源。
-
偏向锁 (Biased Lock)
- 目标:在无多线程竞争的情况下,消除同步开销。
- 流程:当一个线程首次获取锁时,将线程ID记录在Mark Word中。之后该线程进出同步块时,只需检查Mark Word中的线程ID是否是自己,无需CAS操作。
- 升级:当有其他线程尝试获取该锁时,偏向锁撤销,并升级为轻量级锁。
-
轻量级锁 (Lightweight Lock)
- 目标:在存在竞争但竞争不激烈(线程交替执行)的情况下,通过自旋提高性能,避免线程阻塞。
- 加锁流程:
- 线程在自己的栈帧中创建锁记录(Lock Record)。
- 将锁对象的Mark Word拷贝到锁记录中。
- 通过CAS尝试将锁对象的Mark Word更新为指向锁记录的指针。
- 成功则获取锁;失败则说明存在竞争,开始自旋。
- 升级:如果自旋超过一定次数,或有新的线程加入竞争,锁会升级为重量级锁。
-
重量级锁 (Heavyweight Lock)
- 机制:依赖操作系统的Mutex Lock。
- 流程:除了持有锁的线程,其他所有等待的线程都会进入阻塞状态,等待被唤醒。性能开销最大。
公平锁 vs. 非公平锁
-
公平锁 (Fair Lock)
- 理念:线程按照申请锁的先后顺序获取锁,先到先得,就像排队一样。
- 优点:所有等待的线程最终都能获取锁,不会“饿死”。
- 缺点:吞吐量相对较低,因为除了队首的线程,其他线程都会阻塞,线程唤醒开销大。
-
非公平锁 (Non-fair Lock)
- 理念:线程尝试获取锁时,不看等待队列,直接尝试抢占。失败后才进入队列等待。
- 优点:吞吐量更高。因为新来的线程可能正好遇到锁释放,无需进入队列等待和唤醒,减少了开销。
- 缺点:可能导致等待队列中的线程长时间获取不到锁(“饿死”)。
ReentrantLock中的实现
* ReentrantLock默认是非公平锁,但可以通过构造函数指定为公平锁 (new ReentrantLock(true))。
* 其内部实现FairSync和NonfairSync的唯一区别在于lock()方法。
* 公平锁在尝试获取锁之前,会多一个hasQueuedPredecessors()的判断,检查自己是否是等待队列的第一个节点。如果不是,就乖乖排队。非公平锁则没有这个检查,会直接尝试获取锁。
可重入锁 vs. 非可重入锁
-
可重入锁 (Reentrant Lock) / 递归锁
- 理念:同一个线程在持有锁的情况下,可以再次获取该锁而不会被阻塞。
- 作用:避免在一个线程的嵌套方法调用中发生死锁。
- 实现:Java中的
synchronized和ReentrantLock都是可重入锁。它们内部通过一个计数器实现:线程每获取一次锁,计数器加1;每释放一次锁,计数器减1。只有当计数器为0时,锁才被真正释放。 - 示例:
java public synchronized void methodA() { // ... methodB(); // 同一个线程可直接调用另一个同步方法 } public synchronized void methodB() { // ... }
-
非可重入锁 (Non-reentrant Lock)
- 理念:一个线程获取锁之后,无法再次获取该锁,否则会造成死锁。
独享锁 vs. 共享锁
-
独享锁 (Exclusive Lock) / 排他锁
- 理念:该锁一次只能被一个线程持有。
- 特点:获取独享锁的线程既可以读数据,也可以写数据。一旦加锁,其他任何线程都不能再获取任何类型的锁。
- 实现:
synchronized和ReentrantLock都是独享锁。
-
共享锁 (Shared Lock)
- 理念:该锁可以被多个线程同时持有。
- 特点:主要用于读操作。一个线程获取共享锁后,其他线程仍然可以获取共享锁,但不能获取独享锁。
- 实现:
ReentrantReadWriteLock中的读锁是共享锁,写锁是独享锁。
ReentrantReadWriteLock(读写锁)
它内部包含两把锁:ReadLock(共享锁)和WriteLock(独享锁)。
* 读-读:可以并发,不互斥。
* 读-写 / 写-读 / 写-写:互斥。
实现原理
* 它利用AQS(AbstractQueuedSynchronizer)中的一个int类型state字段(32位)来同时表示读锁和写锁的状态。
* 状态分割:将state字段的高16位用于表示读锁的数量,低16位用于表示写锁的重入次数。
* 加锁规则:
* 获取写锁 (tryAcquire):只有在没有其他线程持有读锁或写锁时才能成功。
* 获取读锁 (tryAcquireShared):只有在没有其他线程持有写锁时才能成功。