锁基本知识
1. 什么是锁
1.1 锁的基本概念
在多线程编程中,锁是一种同步机制,用于控制多个线程对共享资源的访问。通过锁机制,可以确保在同一时刻只有一个线程能够访问被保护的资源,从而避免数据竞争和不一致性问题。
1.2 synchronized的三种应用方式
graph TD
A[synchronized应用方式] --> B[实例方法]
A --> C[代码块]
A --> D[静态方法]
B --> B1[锁定当前实例对象this]
C --> C1[锁定括号内指定的对象]
D --> D1[锁定当前类的Class对象]
- 作用于实例方法:当前实例加锁,进入同步代码前要获得当前实例的锁
- 作用于代码块:对括号里配置的对象加锁
- 作用于静态方法:当前类加锁,进入同步代码前要获得当前类对象的锁
1.3 三种应用方式举例
1.3.1 实例方法同步(对象锁)
public class InstanceMethodSync {
// 实例方法同步,锁的是当前对象this
public synchronized void method1() {
System.out.println(Thread.currentThread().getName() + " - method1 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - method1 end");
}
public synchronized void method2() {
System.out.println(Thread.currentThread().getName() + " - method2 start");
System.out.println(Thread.currentThread().getName() + " - method2 end");
}
}
1.3.2 代码块同步(指定对象锁)
public class CodeBlockSync {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " - method1 with lock1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method2() {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " - method2 with lock2");
}
}
public void method3() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " - method3 with this");
}
}
}
1.3.3 静态方法同步(类锁)
public class StaticMethodSync {
// 静态方法同步,锁的是当前类的Class对象
public static synchronized void staticMethod1() {
System.out.println(Thread.currentThread().getName() + " - staticMethod1 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - staticMethod1 end");
}
public static synchronized void staticMethod2() {
System.out.println(Thread.currentThread().getName() + " - staticMethod2 start");
System.out.println(Thread.currentThread().getName() + " - staticMethod2 end");
}
// 等价于上面的静态同步方法
public static void staticMethod3() {
synchronized (StaticMethodSync.class) {
System.out.println(Thread.currentThread().getName() + " - staticMethod3 with class lock");
}
}
}
核心要点:
- 所有的普通同步方法用的都是同一把锁——实例对象本身(this)
- 所有的静态同步方法用的也是同一把锁——类对象本身(Class)
- 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象
2. 字节码分析synchronized
2.1 反编译工具使用
# 基本反编译
javap -c ***.class
# 详细信息反编译
javap -v ***.class # verbose输出附加信息
2.2 synchronized同步代码块的字节码实现
public class LockSyncDemo {
private final Object object = new Object();
public void m1() {
synchronized (object) {
System.out.println("---------hello synchronized code block---------");
}
}
}
字节码分析:
flowchart TD
A[synchronized代码块] --> B[monitorenter指令]
B --> C[执行同步代码]
C --> D[monitorexit指令1 - 正常退出]
C --> E[monitorexit指令2 - 异常退出]
F[指令特点] --> F1[1个monitorenter]
F --> F2[2个monitorexit]
F2 --> F3[保证无论正常还是异常都能释放锁]
2.3 synchronized普通同步方法的字节码实现
特点:
- 使用
ACC_SYNCHRONIZED
访问标志 - 调用指令检查该标志是否被设置
- 如果设置了,执行线程先持有monitor锁,然后执行方法
- 方法完成时释放monitor(无论正常完成还是异常完成)
2.4 synchronized静态同步方法的字节码实现
特点:
- 同时具有
ACC_STATIC
和ACC_SYNCHRONIZED
访问标志 - 通过这两个标志区分该方法是否为静态同步方法
graph TD
A[synchronized实现方式] --> B[同步代码块]
A --> C[实例同步方法]
A --> D[静态同步方法]
B --> B1[monitorenter/monitorexit指令]
C --> C1[ACC_SYNCHRONIZED标志]
D --> D1[ACC_STATIC + ACC_SYNCHRONIZED标志]
3. ObjectMonitor底层架构与机制
3.1 ObjectMonitor与Java对象的关系
ObjectMonitor是JVM中实现synchronized关键字的核心机制,它是一个C++对象,负责管理Java对象的锁状态。
3.1.1 对象头与Monitor的关联
graph TB
A[Java对象实例] --> B[对象头 Object Header]
A --> C[实例数据 Instance Data]
A --> D[对齐填充 Padding]
B --> E[Mark Word 8字节]
B --> F[Class Pointer 8字节]
E --> G[锁状态信息]
E --> H[GC分代年龄]
E --> I[HashCode]
E --> J[线程ID/指针]
F --> K[指向类元数据]
G --> L{当前锁状态}
L -->|无锁 01| M[存储HashCode + Age]
L -->|偏向锁 01| N[存储线程ID + Epoch]
L -->|轻量级锁 00| O[指向栈中Lock Record]
L -->|重量级锁 10| P[指向ObjectMonitor]
P --> Q[ObjectMonitor实例]
Q --> R[_owner: 持有锁的线程]
Q --> S[_EntryList: 阻塞队列]
Q --> T[_WaitSet: 等待队列]
Q --> U[_recursions: 重入次数]
style A fill:#e3f2fd
style E fill:#fff3e0
style P fill:#ffcdd2
style Q fill:#c8e6c9
对象头详细结构:
- Mark Word(8字节):存储对象的hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等
- Class Pointer(8字节):指向对象所属类的元数据指针(在64位JVM中)
Monitor关联机制:
- 轻量级锁:Mark Word指向线程栈帧中的Lock Record
- 重量级锁:Mark Word指向堆中的ObjectMonitor对象
- 锁升级:根据竞争情况,锁会从偏向锁→轻量级锁→重量级锁逐步升级
3.1.2 Mark Word中的锁状态
Mark Word 64位结构详解:
锁状态 | 锁标志位(lock) | 偏向锁标志(biased_lock) | Mark Word位域分布 | 位域说明 |
---|---|---|---|---|
无锁状态 | 01 | 0 | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | 25位未使用 + 31位对象HashCode + 1位未使用 + 4位GC年龄 + 1位偏向锁标志0 + 2位锁标志01 |
偏向锁状态 | 01 | 1 | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | 54位线程ID + 2位偏向时间戳 + 1位未使用 + 4位GC年龄 + 1位偏向锁标志1 + 2位锁标志01 |
轻量级锁状态 | 00 | - | ptr_to_lock_record:62 | lock:2 | 62位指向栈中Lock Record的指针 + 2位锁标志00 |
重量级锁状态 | 10 | - | ptr_to_heavyweight_monitor:62 | lock:2 | 62位指向ObjectMonitor对象的指针 + 2位锁标志10 |
GC标记状态 | 11 | - | GC相关信息:62 | lock:2 | 62位GC标记信息 + 2位锁标志11 |
锁状态转换详解:
stateDiagram-v2
[*] --> 无锁状态
无锁状态 --> 偏向锁 : 第一个线程访问
偏向锁 --> 轻量级锁 : 其他线程竞争
轻量级锁 --> 重量级锁 : 竞争激烈/自旋失败
偏向锁 --> 无锁状态 : 偏向锁撤销
轻量级锁 --> 无锁状态 : 锁释放
重量级锁 --> 无锁状态 : 锁释放
无锁状态 : lock=01, biased_lock=0
无锁状态 : 存储HashCode + GC年龄
偏向锁 : lock=01, biased_lock=1
偏向锁 : 存储线程ID + Epoch
轻量级锁 : lock=00
轻量级锁 : 指向Lock Record
重量级锁 : lock=10
重量级锁 : 指向ObjectMonitor
各状态详细说明:
锁状态 | 锁标志位 | 偏向锁标志 | Mark Word内容 | 说明 |
---|---|---|---|---|
无锁 | 01 | 0 | HashCode(31) + Age(4) | 对象初始状态 |
偏向锁 | 01 | 1 | ThreadID(54) + Epoch(2) + Age(4) | 只有一个线程访问 |
轻量级锁 | 00 | - | Lock Record指针(62) | 多线程交替访问 |
重量级锁 | 10 | - | ObjectMonitor指针(62) | 多线程同时竞争 |
GC标记 | 11 | - | GC相关信息(62) | 垃圾回收时使用 |
3.2 ObjectMonitor结构图
graph TB
A[Java对象] --> B[对象头 Object Header]
B --> C[Mark Word]
B --> D[Class Pointer]
C --> E{锁状态}
E -->|无锁 01| F[hashCode + age]
E -->|偏向锁 01| G[thread ID + epoch]
E -->|轻量级锁 00| H[指向栈中Lock Record]
E -->|重量级锁 10| I[指向ObjectMonitor]
I --> J[ObjectMonitor C++对象]
J --> K[_header: 保存对象原始Mark Word]
J --> L[_object: 指向持有锁的Java对象]
J --> M[_owner: 当前持有锁的线程]
J --> N[_WaitSet: 等待队列]
J --> O[_EntryList: 阻塞队列]
J --> P[_count: 重入次数]
N --> Q[调用wait的线程]
O --> R[等待获取锁的线程]
style A fill:#e1f5fe
style J fill:#fff3e0
style M fill:#f3e5f5
style N fill:#e8f5e8
style O fill:#fff8e1
3.3 ObjectMonitor核心字段
class ObjectMonitor {
private:
volatile markOop _header; // 保存对象原始的mark word
void* volatile _object; // 指向持有锁的对象
void* volatile _owner; // 指向拥有该monitor的线程
volatile jlong _previous_owner_tid; // 前一个拥有者的线程ID
volatile intptr_t _recursions; // 重入次数
ObjectWaiter* volatile _EntryList; // 处于block状态的线程队列
ObjectWaiter* volatile _cxq; // 竞争队列
ObjectWaiter* volatile _WaitSet; // 处于wait状态的线程队列
volatile int _waiters; // wait状态的线程数
volatile int _WaitSetLock; // 保护WaitSet的锁
};
3.4 ObjectMonitor工作机制
flowchart TD
A[线程尝试获取锁] --> B{_owner是否为null?}
B -->|是| C[设置_owner为当前线程]
C --> D[获取锁成功]
B -->|否| E{_owner是否为当前线程?}
E -->|是| F[_recursions++]
F --> G[重入成功]
E -->|否| H[加入_EntryList队列]
H --> I[线程阻塞等待]
J[锁释放] --> K{_recursions是否为0?}
K -->|否| L[_recursions--]
L --> M[继续持有锁]
K -->|是| N[_owner设为null]
N --> O[唤醒_EntryList中的线程]
O --> P[被唤醒线程竞争锁]
style C fill:#c8e6c9
style F fill:#c8e6c9
style H fill:#ffcdd2
style N fill:#fff9c4
3.5 管程(Monitor)概念
管程定义: 管程(Monitors,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。
核心特性:
- 互斥性:同一时刻只有一个线程可以执行管程中的代码
- 同步性:提供条件变量支持线程间的协调
- 封装性:将共享数据和操作封装在一起
3.6 为什么任何对象都可以作为锁?
graph LR
A[任何Java对象] --> B[都有对象头]
B --> C[对象头包含Mark Word]
C --> D[Mark Word可存储锁信息]
D --> E[可关联ObjectMonitor]
E --> F[实现锁功能]
style A fill:#e3f2fd
style F fill:#e8f5e8
原因分析:
- 对象头结构:每个Java对象都有对象头,包含Mark Word
- Mark Word功能:可以存储锁状态、线程ID等信息
- Monitor关联:重量级锁时,Mark Word指向ObjectMonitor
- JVM支持:JVM为每个对象提供了成为锁的基础设施
这种设计的优势:
- 简化编程模型:任何对象都可以作为同步的载体
- 灵活性:可以选择合适的对象作为锁
- 一致性:所有对象都遵循相同的锁机制
4. 乐观锁和悲观锁
4.1 悲观锁(狼性锁)
定义: 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
特点:
- synchronized关键字和Lock的实现类都是悲观锁
- 适合写操作多的场景
- 先加锁可以保证写操作时数据正确
- 显式锁定之后再操作同步资源
4.1.1 synchronized悲观锁示例
public class PessimisticLockDemo {
private int count = 0;
private final Object lock = new Object();
// 悲观锁:认为每次访问都会有并发问题,所以每次都加锁
public void increment() {
synchronized (lock) {
// 先获取锁,再执行业务逻辑
System.out.println(Thread.currentThread().getName() + " 获取到锁,开始执行");
// 模拟业务处理时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
System.out.println(Thread.currentThread().getName() + " 执行完毕,count = " + count);
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
4.1.2 ReentrantLock悲观锁示例
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 悲观地认为会有竞争,先获取锁
try {
System.out.println(Thread.currentThread().getName() + " 获取到锁");
// 业务逻辑
count++;
System.out.println("count = " + count);
// 模拟处理时间
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 确保锁被释放
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
}
// 尝试获取锁的示例
public boolean tryIncrement() {
if (lock.tryLock()) {
try {
count++;
System.out.println(Thread.currentThread().getName() + " 通过tryLock获取锁成功,count = " + count);
return true;
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取锁失败");
return false;
}
}
}
4.2 乐观锁(佛系锁)
定义: 认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。
实现机制:
- 通过无锁编程实现
- 更新数据时判断是否有其他线程更新了数据
- 如果没有被更新,成功写入修改的数据
- 如果已被更新,根据实现方式执行不同操作(放弃修改、重试抢锁等)
判断规则:
- 版本号机制(Version)
- CAS算法(Compare-and-Swap)- 最常用
4.2.1 AtomicInteger乐观锁示例
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockDemo {
private AtomicInteger count = new AtomicInteger(0);
// 乐观锁:使用CAS操作,不加锁
public void increment() {
int current;
int next;
do {
current = count.get(); // 获取当前值
next = current + 1; // 计算新值
System.out.println(Thread.currentThread().getName() +
" 尝试CAS操作:current=" + current + ", next=" + next);
// 模拟一些处理时间,增加CAS失败的可能性
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (!count.compareAndSet(current, next)); // CAS操作,失败则重试
System.out.println(Thread.currentThread().getName() +
" CAS成功,最终值:" + count.get());
}
// 简单的原子操作
public void simpleIncrement() {
int result = count.incrementAndGet();
System.out.println(Thread.currentThread().getName() +
" 原子递增,结果:" + result);
}
public int getCount() {
return count.get();
}
}
4.2.2 版本号机制乐观锁示例
public class VersionOptimisticLock {
private volatile int value = 0;
private volatile int version = 0;
// 获取当前值和版本号
public ValueVersion getValue() {
return new ValueVersion(value, version);
}
// 乐观锁更新:基于版本号
public boolean updateValue(int expectedVersion, int newValue) {
synchronized (this) {
if (version == expectedVersion) {
// 版本号匹配,可以更新
value = newValue;
version++; // 版本号递增
System.out.println(Thread.currentThread().getName() +
" 更新成功:value=" + value + ", version=" + version);
return true;
} else {
// 版本号不匹配,更新失败
System.out.println(Thread.currentThread().getName() +
" 更新失败:期望version=" + expectedVersion +
", 实际version=" + version);
return false;
}
}
}
// 乐观锁递增操作
public void optimisticIncrement() {
ValueVersion current;
boolean success;
do {
current = getValue(); // 获取当前值和版本
int newValue = current.value + 1;
System.out.println(Thread.currentThread().getName() +
" 尝试更新:当前value=" + current.value +
", version=" + current.version + ", 新value=" + newValue);
// 模拟一些处理时间
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
success = updateValue(current.version, newValue);
if (!success) {
System.out.println(Thread.currentThread().getName() + " 检测到冲突,重试...");
}
} while (!success); // 失败则重试
}
// 值和版本号的封装类
public static class ValueVersion {
public final int value;
public final int version;
public ValueVersion(int value, int version) {
this.value = value;
this.version = version;
}
}
}
4.2.3 数据库乐观锁示例
import javax.persistence.*;
import java.math.BigDecimal;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.OptimisticLockingFailureException;
// 使用数据库版本号实现乐观锁
@Entity
public class Account {
@Id
private Long id;
private BigDecimal balance;
@Version // JPA乐观锁注解
private Integer version;
// getters and setters
}
@Service
public class OptimisticAccountService {
@Autowired
private AccountRepository accountRepository;
// 乐观锁转账
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
try {
// 不加锁,直接查询
Account fromAccount = accountRepository.findById(fromId)
.orElseThrow(() -> new RuntimeException("账户不存在"));
Account toAccount = accountRepository.findById(toId)
.orElseThrow(() -> new RuntimeException("账户不存在"));
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
// 乐观地执行更新,如果版本号不匹配会抛出异常
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(fromAccount); // 可能抛出OptimisticLockingFailureException
accountRepository.save(toAccount);
} catch (OptimisticLockingFailureException e) {
// 乐观锁冲突,可以选择重试或抛出异常
throw new RuntimeException("并发冲突,请重试", e);
}
}
}
4.3 乐观锁vs悲观锁对比
graph LR
A[锁的分类] --> B[悲观锁]
A --> C[乐观锁]
B --> B1[特点: 狼性锁]
B --> B2[适用: 写操作多]
B --> B3[实现: synchronized/Lock]
B --> B4[机制: 先加锁再操作]
C --> C1[特点: 佛系锁]
C --> C2[适用: 读操作多]
C --> C3[实现: CAS/版本号]
C --> C4[机制: 无锁编程]
使用场景:
- 悲观锁:适合写操作多的场景,先加锁保证写操作数据正确
- 乐观锁:适合读操作多的场景,不加锁特点使读操作性能大幅提升
5. 公平锁和非公平锁
5.1 基本概念
锁类型 | 定义 | 实现方式 |
---|---|---|
公平锁 | 多个线程按照申请锁的顺序来获取锁,类似排队买票,先来先得 | new ReentrantLock(true) |
非公平锁 | 多个线程获取锁的顺序并不按照申请锁的顺序,可能后申请的线程优先获取锁 | new ReentrantLock(false) 或new ReentrantLock() |
5.2 ReentrantLock演示代码
package com.juc.lock.ReentrantLock;
import java.util.concurrent.locks.ReentrantLock;
public class SaleTickDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {for (int i = 0; i < 55; i++) ticket.sale();}, "a").start();
new Thread(() -> {for (int i = 0; i < 55; i++) ticket.sale();}, "b").start();
new Thread(() -> {for (int i = 0; i < 55; i++) ticket.sale();}, "c").start();
}
}
class Ticket {
private int number = 50;
ReentrantLock lock = new ReentrantLock(); // 默认非公平锁
// ReentrantLock lock = new ReentrantLock(true); // 公平锁
// ReentrantLock lock = new ReentrantLock(false); // 非公平锁
public void sale() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() +
"卖出第:\t" + number-- + "\t还剩下:" + number);
}
} finally {
lock.unlock();
}
}
}
5.3 为什么默认使用非公平锁?
5.3.1 性能优势分析
graph LR
A[线程释放锁] --> B{有线程正在运行?}
B -->|公平锁| C[必须唤醒队列中的线程]
B -->|非公平锁| D[运行中的线程可直接获取]
C --> E[线程切换开销]
D --> F[避免线程切换]
E --> G[性能较低]
F --> H[性能较高]
style D fill:#c8e6c9
style F fill:#c8e6c9
style H fill:#c8e6c9
style C fill:#ffcdd2
style E fill:#ffcdd2
style G fill:#ffcdd2
5.3.2 性能对比测试
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.CountDownLatch;
public class FairVsUnfairLockTest {
private static final int THREAD_COUNT = 10;
private static final int OPERATIONS_PER_THREAD = 100000;
// 非公平锁(默认)
private static final ReentrantLock unfairLock = new ReentrantLock(false);
// 公平锁
private static final ReentrantLock fairLock = new ReentrantLock(true);
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
System.out.println("测试非公平锁性能:");
testLockPerformance(unfairLock, "非公平锁");
counter = 0; // 重置计数器
System.out.println("\n测试公平锁性能:");
testLockPerformance(fairLock, "公平锁");
}
private static void testLockPerformance(ReentrantLock lock, String lockType)
throws InterruptedException {
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(THREAD_COUNT);
long startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
startLatch.await(); // 等待所有线程准备就绪
for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
lock.lock();
try {
counter++;
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endLatch.countDown();
}
}, lockType + "-Thread-" + i).start();
}
startLatch.countDown(); // 开始测试
endLatch.await(); // 等待所有线程完成
long endTime = System.currentTimeMillis();
System.out.println(lockType + " 执行时间: " + (endTime - startTime) + "ms");
System.out.println(lockType + " 最终计数: " + counter);
}
}
5.3.3 为什么非公平锁性能更好?
1. 减少线程切换开销
// 非公平锁场景
public class UnfairLockAdvantage {
private final ReentrantLock lock = new ReentrantLock(false);
public void businessMethod() {
lock.lock();
try {
// 业务逻辑执行很快
doQuickOperation();
} finally {
lock.unlock();
// 释放锁后,如果当前线程立即再次需要锁
// 非公平锁允许它直接获取,避免线程切换
}
// 可能立即再次需要锁
anotherQuickOperation();
}
private void anotherQuickOperation() {
lock.lock();
try {
// 如果是非公平锁,当前线程可能直接获取到锁
// 避免了唤醒其他等待线程的开销
doAnotherQuickOperation();
} finally {
lock.unlock();
}
}
private void doQuickOperation() {
// 快速操作
}
private void doAnotherQuickOperation() {
// 另一个快速操作
}
}
2. 更高的吞吐量
- 非公平锁:刚释放锁的线程很可能再次获得锁,CPU缓存命中率更高
- 公平锁:严格按照FIFO顺序,可能导致频繁的线程切换
3. 适用场景分析
特性 | 非公平锁 | 公平锁 |
---|---|---|
性能 | 高吞吐量 | 较低吞吐量 |
公平性 | 可能饥饿 | 严格公平 |
延迟 | 低延迟 | 可能高延迟 |
适用场景 | 高并发、性能敏感 | 需要严格顺序 |
5.3.4 什么时候使用公平锁?
import java.util.Queue;
import java.util.LinkedList;
import java.util.concurrent.locks.ReentrantLock;
// 需要严格顺序的场景
public class FairLockUseCase {
private final ReentrantLock fairLock = new ReentrantLock(true);
private final Queue<String> taskQueue = new LinkedList<>();
// 任务处理必须按照提交顺序
public void processTaskInOrder() {
fairLock.lock();
try {
if (!taskQueue.isEmpty()) {
String task = taskQueue.poll();
System.out.println(Thread.currentThread().getName() +
" 处理任务: " + task);
// 处理任务...
}
} finally {
fairLock.unlock();
}
}
public void submitTask(String task) {
fairLock.lock();
try {
taskQueue.offer(task);
System.out.println("任务已提交: " + task);
} finally {
fairLock.unlock();
}
}
}
总结:
- 默认非公平锁是基于性能考虑的明智选择
- 大多数应用场景下,非公平锁提供更好的性能
- 只有在明确需要公平性时才使用公平锁
- JVM和操作系统调度本身就不保证绝对公平,非公平锁更符合实际情况
6. 可重入锁(递归锁)
6.1 基本概念
可重入锁定义: 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提:锁对象是同一个对象),不会因为之前已经获取过还没释放而阻塞。
名词解释:
- 可:可以
- 重:再次
- 入:进入
- 锁:同步锁
- 进入什么:进入同步域(同步代码块/方法或显式锁锁定的代码)
一句话总结: 一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁。
6.2 可重入锁的种类
graph TD
A[可重入锁类型] --> B[隐式锁]
A --> C[显式锁]
B --> B1[synchronized关键字]
B --> B2[默认可重入]
B --> B3[JVM自动管理]
C --> C1[ReentrantLock]
C --> C2[手动管理]
C --> C3[需要显式lock/unlock]
6.3 synchronized的重入实现机理
核心机制: 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
执行流程:
flowchart TD
A[monitorenter执行] --> B{计数器是否为0?}
B -->|是| C[设置持有线程为当前线程]
C --> D[计数器+1]
B -->|否| E{持有线程是否为当前线程?}
E -->|是| F[计数器+1 - 重入]
E -->|否| G[等待锁释放]
H[monitorexit执行] --> I[计数器-1]
I --> J{计数器是否为0?}
J -->|是| K[释放锁]
J -->|否| L[继续持有锁]
详细说明:
-
monitorenter时:
- 如果计数器为零:设置持有线程为当前线程,计数器加1
- 如果计数器不为零且持有线程是当前线程:计数器加1(重入)
- 否则等待锁释放
-
monitorexit时:
- 计数器减1
- 计数器为零时代表锁已被释放
6.4 synchronized可重入锁示例
6.4.1 同步方法的重入
public class SynchronizedReentrantDemo {
// 外层同步方法
public synchronized void method1() {
System.out.println(Thread.currentThread().getName() + " 进入method1");
method2(); // 调用另一个同步方法
System.out.println(Thread.currentThread().getName() + " 退出method1");
}
// 内层同步方法
public synchronized void method2() {
System.out.println(Thread.currentThread().getName() + " 进入method2");
method3(); // 继续调用同步方法
System.out.println(Thread.currentThread().getName() + " 退出method2");
}
// 最内层同步方法
public synchronized void method3() {
System.out.println(Thread.currentThread().getName() + " 进入method3");
System.out.println(Thread.currentThread().getName() + " 退出method3");
}
public static void main(String[] args) {
SynchronizedReentrantDemo demo = new SynchronizedReentrantDemo();
new Thread(() -> {
demo.method1();
}, "Thread-A").start();
new Thread(() -> {
demo.method1();
}, "Thread-B").start();
}
}
6.4.2 同步代码块的重入
public class SynchronizedBlockReentrant {
private final Object lock = new Object();
public void outerMethod() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 外层获取锁");
innerMethod(); // 重入同一把锁
System.out.println(Thread.currentThread().getName() + " 外层释放锁");
}
}
public void innerMethod() {
synchronized (lock) { // 重入成功
System.out.println(Thread.currentThread().getName() + " 内层获取锁");
// 模拟业务处理
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 内层释放锁");
}
}
}
详细优点:
- 避免死锁:同一线程可以多次获取同一把锁,避免自己阻塞自己
- 简化编程:不需要担心在持有锁的情况下调用其他同步方法
- 支持递归:递归调用同步方法时不会产生死锁
- 提高性能:减少了不必要的锁竞争和线程阻塞
7. 死锁及其排查
7.1 死锁定义
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去。
7.2 死锁产生的主要原因
mindmap
root((死锁产生原因))
系统资源不足
硬件资源有限
内存不足
进程运行推进顺序不合适
获取锁的顺序不当
时序问题
资源分配不当
分配策略问题
优先级设置不合理
7.3 死锁的经典场景
sequenceDiagram
participant T1 as 线程1
participant L1 as 锁A
participant L2 as 锁B
participant T2 as 线程2
T1->>L1: 获取锁A
T2->>L2: 获取锁B
T1->>L2: 尝试获取锁B (等待)
T2->>L1: 尝试获取锁A (等待)
Note over T1,T2: 互相等待,形成死锁
7.4 死锁排查方法
7.4.1 命令行方式
# 1. 查找Java进程
jps -l
# 2. 分析线程堆栈
jstack 进程编号
7.4.2 图形化工具
# 启动JConsole
jconsole
7.5 死锁预防策略
graph TD
A[死锁预防策略] --> B[避免嵌套锁]
A --> C[避免无限期等待]
A --> D[使用定时锁]
A --> E[统一锁顺序]
B --> B1[尽量使用单一锁]
C --> C1[设置超时时间]
D --> D1[tryLock with timeout]
E --> E1[所有线程按相同顺序获取锁]
预防措施:
- 避免嵌套锁:尽量不要在持有一个锁的情况下去获取另一个锁
- 避免无限期等待:使用带超时的锁获取方法
- 统一锁顺序:所有线程都按照相同的顺序获取多个锁
- 使用并发工具类:使用java.util.concurrent包下的工具类
总结
本文详细介绍了Java中锁的基本知识,包括:
- 锁的基本概念:通过8锁案例深入理解synchronized的锁机制
- 字节码实现:从底层角度分析synchronized的实现原理
- ObjectMonitor机制:理解为什么任何对象都可以作为锁
- 锁的分类:
- 乐观锁vs悲观锁:基于对并发冲突的不同假设
- 公平锁vs非公平锁:基于获取锁顺序的不同策略
- 可重入锁:支持同一线程多次获取同一把锁
- 死锁问题:产生原因、排查方法和预防策略
理解这些锁的基本知识对于编写高质量的多线程程序至关重要,它们是构建并发安全应用程序的基础。