一、MySQL锁类型全景图
1. 按锁粒度划分
| 锁类型 | 作用范围 | 冲突概率 | 性能影响 |
|---|---|---|---|
| 表锁 | 整张表 | 高 | 大 |
| 行锁 | 单行记录 | 低 | 小 |
| 页锁 | 数据页(B+树节点) | 中 | 中 |
2. 按锁模式划分
- 共享锁(S锁):
SELECT ... LOCK IN SHARE MODE - 排他锁(X锁):
SELECT ... FOR UPDATE - 意向锁:快速判断表级冲突(IS/IX锁)
二、InnoDB锁实现原理
1. 行锁底层机制
- 记录锁(Record Lock):锁定索引记录
-- 对id=1的记录加X锁 SELECT * FROM users WHERE id = 1 FOR UPDATE; - 间隙锁(Gap Lock):锁定索引区间
-- 锁定(5,10)区间防止插入 SELECT * FROM users WHERE id > 5 AND id < 10 FOR UPDATE; - 临键锁(Next-Key Lock):记录锁+间隙锁组合
2. 锁升级条件
- 单事务锁定超过
innodb_lock_wait_timeout(默认50s) - 锁数量超过
innodb_buffer_pool_size的5%
三、实战应用场景
场景1:高并发订单处理
-- 1. 开启事务 START TRANSACTION; -- 2. 检查库存并加锁(防止超卖) SELECT stock FROM products WHERE id = 1001 FOR UPDATE; -- 3. 业务逻辑判断 IF stock > 0 THEN UPDATE products SET stock = stock - 1 WHERE id = 1001; INSERT INTO orders(...) VALUES(...); END IF; -- 4. 提交释放锁 COMMIT;
场景2:批量更新防死锁
-- 按固定顺序加锁(如主键升序) SELECT * FROM accounts WHERE id IN (3,1,2) ORDER BY id ASC FOR UPDATE;
场景3:读写分离优化
-- 使用S锁允许并发读 SELECT * FROM articles WHERE id = 5 LOCK IN SHARE MODE;
四、锁问题诊断工具
1. 查看锁状态
SHOW ENGINE INNODB STATUS; -- 查看锁等待 SELECT * FROM performance_schema.events_waits_current; -- 当前等待事件
2. 死锁分析
-- 开启死锁日志 SET GLOBAL innodb_print_all_deadlocks = ON;
3. 锁等待超时设置
SET SESSION innodb_lock_wait_timeout = 30; -- 单位:秒
五、高级锁技巧
- 悲观锁:
FOR UPDATE+ 短事务 - 乐观锁:版本号+重试机制
UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 1001 AND version = 2; - 间隙锁规避:使用
READ COMMITTED隔离级别
六、性能优化建议
- 索引设计:确保锁能命中索引(否则退化为表锁)
- 事务拆分:大事务拆分为小事务
- 隔离级别:根据业务选择
READ COMMITTED或REPEATABLE READ
一、乐观锁核心思想
核心原则:假设并发冲突概率低,操作时不加锁,提交时检测数据是否被修改
类比场景:多人协作编辑文档,保存时提示"文件已被他人修改,请合并变更"
二、实现原理
1. 版本号机制
public class Data { private int value; private int version; // 版本号字段 // getter/setter... }
工作流程:
- 读取数据时获取当前版本号(如version=1)
- 修改数据前检查版本号是否变化
- 提交时执行原子性更新:
UPDATE table SET value=newValue, version=version+1 WHERE id=recordId AND version=oldVersion
2. CAS(Compare-And-Swap)
AtomicInteger atomicInt = new AtomicInteger(0); boolean success = atomicInt.compareAndSet(0, 1); // 预期值0→新值1
硬件支持:通过CPU指令(如x86的CMPXCHG)实现原子操作
三、具体实现步骤
1. 数据库乐观锁(MyBatis示例)
// 1. 实体类添加版本字段 public class Product { private Long id; private String name; private Integer stock; private Integer version; // 乐观锁版本 } // 2. Mapper更新语句 @Update("UPDATE product SET stock=#{stock}, version=version+1 WHERE id=#{id} AND version=#{version}") int updateWithVersion(Product product); // 3. 业务逻辑 public void deductStock(Long productId) { Product product = productMapper.selectById(productId); product.setStock(product.getStock() - 1); int rows = productMapper.updateWithVersion(product); if (rows == 0) { throw new OptimisticLockException("库存更新冲突"); } }
2. Java并发工具实现
// 使用AtomicStampedReference解决ABA问题 AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(100, 0); // 更新操作 int[] stampHolder = new int[1]; int oldValue = atomicRef.get(stampHolder); if (!atomicRef.compareAndSet(oldValue, oldValue - 10, stampHolder[0], stampHolder[0] + 1)) { System.out.println("操作失败,值已被修改"); }
四、典型应用场景
| 场景 | 实现方式 | 优势 |
|---|---|---|
| 电商库存扣减 | 数据库版本号 | 避免超卖 |
| 计数器累加 | AtomicLong | 无锁高性能 |
| 状态机变更 | CAS+自旋 | 避免死锁 |
| 分布式系统 | Redis WATCH/MULTI | 跨进程协调 |
五、与悲观锁的对比
| 特性 | 乐观锁 | 悲观锁 |
|---|---|---|
| 并发假设 | 冲突概率低 | 冲突概率高 |
| 加锁时机 | 提交时检测 | 操作前加锁 |
| 实现复杂度 | 需处理重试逻辑 | 直接阻塞 |
| 适用场景 | 读多写少 | 写多读少 |
| 典型实现 | CAS/版本号 | synchronized |
六、注意事项
-
ABA问题:
- 现象:值从A→B→A,CAS无法感知中间变化
- 解决方案:使用
AtomicStampedReference带版本戳
-
自旋开销:
while (!atomicRef.compareAndSet(...)) { // 可添加退避策略 Thread.yield(); } -
适用边界:
- 冲突率>20%时性能急剧下降
- 长时间计算操作不适合(易导致频繁重试)
synchronized工作原理深度解析
1. 基础概念与使用方式
synchronized是Java中最基本的线程同步机制,用于解决多线程环境下的共享资源竞争问题。它主要有三种使用方式:
-
实例方法同步:锁住当前对象实例
javaCopy Code
public synchronized void method() { // 同步代码 } -
静态方法同步:锁住当前类的Class对象
javaCopy Code
public static synchronized void staticMethod() { // 同步代码 } -
同步代码块:可以指定锁对象
javaCopy Code
public void method() { synchronized(lockObject) { // 同步代码 } }
2. 底层实现机制
2.1 对象头与Mark Word
每个Java对象在内存中由三部分组成:
-
对象头 (Header)
-
实例数据 (Instance Data)
-
对齐填充 (Padding)
其中对象头包含Mark Word和Klass Pointer:
-
Mark Word (32/64位):存储对象的hashCode、GC分代年龄、锁状态等信息
-
Klass Pointer:指向对象类型数据的指针
Mark Word在不同锁状态下的结构:
|
锁状态 |
存储内容 |
标志位 |
|---|---|---|
|
无锁 |
对象hashCode + 分代年龄 |
001 |
|
偏向锁 |
持有线程ID + Epoch + 分代年龄 |
101 |
|
轻量级锁 |
指向栈中锁记录的指针 |
00 |
|
重量级锁 |
指向监视器(Monitor)的指针 |
10 |
2.2 监视器(Monitor)机制
每个Java对象都与一个Monitor相关联,Monitor主要包含以下组件:
-
Owner:持有锁的线程
-
Entry Set:等待获取锁的线程集合
-
Wait Set:调用wait()后进入等待状态的线程集合
3. 锁升级过程
JVM为了优化同步性能,设计了锁升级机制:
3.1 偏向锁 (Biased Locking)
-
适用场景:只有一个线程访问同步块
-
实现原理:在Mark Word中记录线程ID
-
优点:无竞争时几乎无开销
-
触发条件:首次获取锁时
3.2 轻量级锁 (Lightweight Locking)
-
适用场景:多线程交替执行,无真正竞争
-
实现原理:
-
在当前线程栈帧中创建锁记录(Lock Record)
-
通过CAS将Mark Word复制到锁记录
-
尝试用CAS将Mark Word替换为指向锁记录的指针
-
-
优点:避免线程阻塞切换
-
触发条件:发现偏向锁的线程ID不匹配时
3.3 重量级锁 (Heavyweight Locking)
-
适用场景:多线程激烈竞争
-
实现原理:
-
向操作系统申请互斥量(mutex)
-
竞争失败的线程进入阻塞状态
-
-
缺点:涉及用户态到内核态的切换,开销大
-
触发条件:轻量级锁自旋失败后
4. 字节码层面实现
同步代码块编译后会生成monitorenter和monitorexit指令:
public void syncMethod() { synchronized(this) { System.out.println("Hello"); } }
编译后的字节码:
0: aload_0 // 加载this引用 1: dup // 复制栈顶值 2: astore_1 // 存储到局部变量1 3: monitorenter // 进入监视器 4: getstatic #2 // 获取System.out 7: ldc #3 // 加载"Hello" 9: invokevirtual #4// 调用println 12: aload_1 // 加载监视器对象 13: monitorexit // 正常退出监视器 14: goto 22 // 跳转到方法结束 17: astore_2 // 异常处理开始 18: aload_1 // 加载监视器对象 19: monitorexit // 异常退出监视器 20: aload_2 // 加载异常对象 21: athrow // 抛出异常 22: return // 方法返回
5. 性能优化技术
5.1 自旋锁 (Spin Lock)
-
线程不立即阻塞,而是循环尝试获取锁
-
JDK1.6引入自适应自旋:根据上次成功获取锁的自旋时间动态调整
5.2 锁消除 (Lock Elimination)
-
JIT编译器通过逃逸分析,发现同步块不可能被共享时,会消除锁
-
示例:
public String concat(String s1, String s2) { StringBuffer sb = new StringBuffer(); // 局部变量,不会逃逸 sb.append(s1).append(s2); return sb.toString(); }
5.3 锁粗化 (Lock Coarsening)
-
将连续的多个同步块合并为一个更大的同步块
-
减少频繁获取/释放锁的开销
6. 与ReentrantLock对比
|
特性 |
synchronized |
ReentrantLock |
|---|---|---|
|
实现方式 |
JVM内置 |
Java代码实现 |
|
可中断 |
不支持 |
支持 |
|
公平锁 |
非公平 |
可配置 |
|
条件变量 |
单一 |
多个 |
|
锁绑定多个条件 |
不支持 |
支持 |
|
性能 |
JDK6后优化好 |
高竞争时更好 |
|
代码复杂度 |
简单 |
需要显式释放 |
7. 最佳实践
-
锁粒度:尽量减小同步代码块的范围
-
锁对象:使用专门的对象而非业务对象
-
避免嵌套:防止死锁发生
-
性能监控:关注锁竞争情况(JConsole/VisualVM)
-
升级选择:高竞争场景考虑ReentrantLock
8. 常见问题
Q1: 为什么synchronized是可重入的?
A: 监视器记录锁的持有线程和进入次数,同一线程可重复获取
Q2: synchronized和volatile的区别?
A: volatile保证可见性和有序性,但不保证原子性;synchronized三者都保证
Q3: 偏向锁为什么需要撤销?
A: 当检测到有多个线程竞争时,需要撤销偏向锁升级为更高级别的锁
ReentrantLock 原理深度解析
1. 基本概念与核心特性
ReentrantLock是Java并发包(java.util.concurrent.locks)中提供的可重入互斥锁实现,相比synchronized具有更丰富的功能:
-
可重入性:同一线程可以多次获取同一把锁
-
公平性选择:支持公平锁和非公平锁两种模式
-
可中断:支持获取锁时响应中断
-
超时机制:可以尝试在指定时间内获取锁
-
条件变量:支持多个条件队列
2. 核心实现原理
2.1 AQS(AbstractQueuedSynchronizer)基础
ReentrantLock的核心实现依赖于AQS框架,AQS维护了一个volatile int state变量和一个FIFO线程等待队列:
javaCopy Code
// AQS中的关键字段 private volatile int state; // 锁状态 private transient volatile Node head; // 队列头 private transient volatile Node tail; // 队列尾
2.2 锁状态管理
ReentrantLock通过AQS的state字段记录锁状态:
-
state = 0:锁未被占用
-
state > 0:锁被占用,数值表示重入次数
2.3 公平锁与非公平锁
ReentrantLock内部有两个实现类:
-
NonfairSync:非公平锁实现(默认)
-
FairSync:公平锁实现
关键区别在于获取锁时的行为:
javaCopy Code
// 非公平锁尝试获取 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { // 直接尝试CAS setExclusiveOwnerThread(current); return true; } } // ...重入处理 } // 公平锁尝试获取 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && // 检查队列中是否有等待线程 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // ...重入处理 }
3. 关键方法实现
3.1 lock() 方法流程
-
尝试通过CAS修改state获取锁
-
成功则设置当前线程为独占线程
-
失败则调用AQS.acquire()加入等待队列
3.2 unlock() 方法流程
-
减少state值
-
如果state变为0,释放锁并唤醒等待队列中的线程
4. 条件变量实现
ReentrantLock通过ConditionObject实现条件变量:
javaCopy Code
public Condition newCondition() { return sync.newCondition(); }
每个Condition维护一个独立的条件等待队列,await()和signal()操作涉及:
-
将线程从锁队列转移到条件队列
-
从条件队列移回锁队列
5. 与synchronized的性能对比
|
特性 |
ReentrantLock |
synchronized |
|---|---|---|
|
实现方式 |
Java代码实现 |
JVM内置 |
|
锁获取方式 |
显示调用lock()/unlock() |
隐式获取/释放 |
|
可中断 |
支持 |
不支持 |
|
公平性 |
可配置 |
非公平 |
|
条件变量 |
支持多个 |
单一 |
|
性能 |
高竞争时更好 |
JDK6后优化良好 |
6. 最佳实践
-
必须释放锁:在finally块中调用unlock()
-
避免嵌套:防止死锁
-
合理选择公平性:公平锁适合保证顺序但性能较低
-
考虑使用tryLock:避免长时间阻塞
-
条件变量使用:复杂等待/通知场景
7. 典型使用示例
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock(); // 使用tryLock:避免长时间阻塞
try {
while (!conditionMet) {
condition.await();
}
// 执行临界区代码
} finally {
lock.unlock();
}
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerExample {
private final Queue<Integer> queue = new LinkedList<>();
private final int CAPACITY = 5;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public static void main(String[] args) {
ProducerConsumerExample example = new ProducerConsumerExample();
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
example.produce(i);
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
example.consume();
Thread.sleep(500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
public void produce(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == CAPACITY) {
System.out.println("队列已满,生产者等待...");
notFull.await();
}
queue.add(value);
System.out.println("生产: " + value + " 队列大小: " + queue.size());
notEmpty.signal();
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
notEmpty.await();
}
int value = queue.poll();
System.out.println("消费: " + value + " 队列大小: " + queue.size());
notFull.signal();
} finally {
lock.unlock();
}
}
}
8. 常见问题解答
Q1: 为什么ReentrantLock是可重入的?
A: 通过记录当前持有线程和重入次数实现,每次重入state值增加1
Q2: 公平锁和非公平锁如何选择?
A: 公平锁保证FIFO顺序但吞吐量低,非公平锁可能插队但性能更高
Q3: ReentrantLock与synchronized在JVM层面有何区别?
A: synchronized依赖JVM的monitor机制,ReentrantLock完全用Java代码实现基于AQS

753

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



