Java 锁机制详细解析
1. 锁的基本概念与分类
1.1 为什么需要锁
在多线程环境中,当多个线程同时访问共享资源时,可能会导致数据不一致或程序行为异常。锁机制通过强制互斥访问来确保线程安全,保证在同一时刻只有一个线程可以访问共享资源。
1.2 锁的分类
Java中的锁可以分为以下几类:
- 内置锁(synchronized):Java语言内置的同步机制
- 显式锁(Lock接口):java.util.concurrent.locks包提供的锁
- 读写锁:允许多个读操作同时进行,但写操作独占
- 乐观锁与悲观锁:不同的并发控制策略
2. synchronized 内置锁
2.1 实现原理
synchronized是Java关键字,它基于监视器锁(Monitor)实现。每个Java对象都有一个关联的监视器锁,当线程进入synchronized代码块时,会自动获取这个锁,退出时自动释放。
底层实现:
- 在JVM字节码层面,synchronized通过monitorenter和monitorexit指令实现
- 对象头中的Mark Word字段用于存储锁状态信息
- 锁的升级过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
2.2 锁升级过程
2.2.1 偏向锁
- 目的:在无竞争情况下减少同步开销
- 原理:当线程第一次获取锁时,会在对象头和栈帧中记录偏向的线程ID
- 适用场景:只有一个线程访问同步块的场景
2.2.2 轻量级锁
- 目的:在竞争不激烈的情况下减少重量级锁的性能开销
- 原理:当有线程竞争锁时,偏向锁会升级为轻量级锁
- 实现:线程会在自己的栈帧中创建锁记录(Lock Record),通过CAS操作将对象头中的Mark Word替换为指向锁记录的指针
2.2.3 重量级锁
- 目的:处理高竞争情况下的线程同步
- 原理:当轻量级锁竞争失败时,会升级为重量级锁
- 实现:基于操作系统的互斥量(mutex)实现,线程会进入阻塞状态
2.3 使用方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class SynchronizedExample { public synchronized void instanceMethod() { } public static synchronized void staticMethod() { } public void method() { synchronized (this) { } } }
|
3. 显式锁(Lock接口)
3.1 ReentrantLock 可重入锁
ReentrantLock是Lock接口的主要实现,提供了比synchronized更灵活的锁操作。
3.1.1 核心特性
- 可重入性:同一个线程可以多次获取同一把锁
- 可中断:等待锁的线程可以被中断
- 超时机制:可以尝试在指定时间内获取锁
- 公平性:可以选择公平锁或非公平锁
3.1.2 实现原理
ReentrantLock基于抽象队列同步器(AQS)实现,AQS使用一个整型的volatile变量(state)来表示同步状态,通过一个FIFO队列来完成资源获取线程的排队工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample { private final ReentrantLock lock = new ReentrantLock(); private int count = 0; public void increment() { lock.lock(); try { count++; if (count < 10) { increment(); } } finally { lock.unlock(); } } public boolean tryIncrement() { if (lock.tryLock()) { try { count++; return true; } finally { lock.unlock(); } } return false; } public void interruptibleIncrement() throws InterruptedException { lock.lockInterruptibly(); try { count++; } finally { lock.unlock(); } } }
|
3.2 读写锁(ReadWriteLock)
读写锁允许多个读操作同时进行,但写操作是独占的。
3.2.1 实现原理
ReentrantReadWriteLock内部维护了两个锁:读锁和写锁。读锁是共享锁,写锁是独占锁。
- 读锁:当没有线程持有写锁时,多个线程可以同时获取读锁
- 写锁:当没有线程持有读锁或写锁时,才能获取写锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample { private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); private int data = 0; public int read() { readLock.lock(); try { return data; } finally { readLock.unlock(); } } public void write(int value) { writeLock.lock(); try { data = value; } finally { writeLock.unlock(); } } public void processData() { writeLock.lock(); try { data++; readLock.lock(); } finally { writeLock.unlock(); } try { System.out.println("Data: " + data); } finally { readLock.unlock(); } } }
|
4. AQS(AbstractQueuedSynchronizer)原理
AQS是Java并发包中锁机制的基石,许多同步器都是基于AQS构建的。
4.1 核心思想
AQS使用一个volatile int state表示同步状态,通过一个FIFO队列管理获取资源失败的线程。
4.2 主要方法
- **tryAcquire()**:尝试获取资源
- **tryRelease()**:尝试释放资源
- **tryAcquireShared()**:尝试获取共享资源
- **tryReleaseShared()**:尝试释放共享资源
- **isHeldExclusively()**:判断是否被当前线程独占
4.3 实现示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class Mutex { private static class Sync extends AbstractQueuedSynchronizer { protected boolean isHeldExclusively() { return getState() == 1; } public boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } protected boolean tryRelease(int releases) { if (getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0); return true; } } private final Sync sync = new Sync(); public void lock() { sync.acquire(1); } public boolean tryLock() { return sync.tryAcquire(1); } public void unlock() { sync.release(1); } public boolean isLocked() { return sync.isHeldExclusively(); } }
|
5. 锁的性能优化与最佳实践
5.1 减少锁的竞争
- 缩小同步范围:只同步必要的代码块
- 减小锁的粒度:使用多个锁保护不同的资源
- 使用读写锁:在读多写少的场景中使用读写锁
- 使用无锁数据结构:如ConcurrentHashMap
5.2 避免死锁
- 按顺序获取锁:确保所有线程以相同的顺序获取锁
- 使用定时锁:tryLock()方法可以指定超时时间
- 死锁检测:定期检查系统是否发生死锁
5.3 锁分离与锁分段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| public class SeparateLockExample { private final Object readLock = new Object(); private final Object writeLock = new Object(); private int readCount = 0; private int writeCount = 0; public void read() { synchronized (readLock) { readCount++; } } public void write() { synchronized (writeLock) { writeCount++; } } }
public class StripedMap { private static final int N_LOCKS = 16; private final Node[] buckets; private final Object[] locks; private static class Node { Object key; Object value; Node next; } public StripedMap(int numBuckets) { buckets = new Node[numBuckets]; locks = new Object[N_LOCKS]; for (int i = 0; i < N_LOCKS; i++) { locks[i] = new Object(); } } private final int hash(Object key) { return Math.abs(key.hashCode() % buckets.length); } public Object get(Object key) { int hash = hash(key); synchronized (locks[hash % N_LOCKS]) { for (Node m = buckets[hash]; m != null; m = m.next) { if (m.key.equals(key)) { return m.value; } } } return null; } public void put(Object key, Object value) { int hash = hash(key); synchronized (locks[hash % N_LOCKS]) { } } }
|
6. 其他锁机制
6.1 StampedLock
Java 8引入的StampedLock提供了三种模式的锁控制:写锁、悲观读锁和乐观读。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import java.util.concurrent.locks.StampedLock;
public class StampedLockExample { private final StampedLock lock = new StampedLock(); private double x, y; public void move(double deltaX, double deltaY) { long stamp = lock.writeLock(); try { x += deltaX; y += deltaY; } finally { lock.unlockWrite(stamp); } } public double distanceFromOrigin() { long stamp = lock.tryOptimisticRead(); double currentX = x, currentY = y; if (!lock.validate(stamp)) { stamp = lock.readLock(); try { currentX = x; currentY = y; } finally { lock.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); } public void moveIfAtOrigin(double newX, double newY) { long stamp = lock.readLock(); try { while (x == 0.0 && y == 0.0) { long ws = lock.tryConvertToWriteLock(stamp); if (ws != 0L) { stamp = ws; x = newX; y = newY; break; } else { lock.unlockRead(stamp); stamp = lock.writeLock(); } } } finally { lock.unlock(stamp); } } }
|
6.2 条件变量(Condition)
Condition接口提供了类似Object.wait()和notify()的功能,但更灵活。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample { private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); private final Object[] items = new Object[100]; private int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) { notFull.await(); } items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) { notEmpty.await(); } Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }
|
7. 锁的监控与诊断
7.1 检测死锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class DeadlockDetector { public static void detectDeadlock() { ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); long[] threadIds = threadMXBean.findDeadlockedThreads(); if (threadIds != null) { ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(threadIds); for (ThreadInfo threadInfo : threadInfos) { System.out.println("检测到死锁线程: " + threadInfo.getThreadName()); for (LockInfo lockInfo : threadInfo.getLockedSynchronizers()) { System.out.println("锁信息: " + lockInfo); } } } } }
|
7.2 监控锁竞争
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class LockContentionMonitor { public static void monitorLockContention() { ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); long[] threadIds = threadMXBean.getAllThreadIds(); for (long threadId : threadIds) { ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId); if (threadInfo.getLockInfo() != null) { System.out.println("线程 " + threadInfo.getThreadName() + " 正在等待锁: " + threadInfo.getLockInfo()); System.out.println("阻塞的线程数: " + threadInfo.getBlockedCount()); System.out.println("阻塞的时间: " + threadInfo.getBlockedTime() + "ms"); } } } }
|
8. 总结
Java的锁机制从简单的synchronized关键字到复杂的AQS实现,提供了多种同步解决方案。理解这些锁的工作原理和适用场景对于编写高效、安全的多线程程序至关重要。
- synchronized:简单易用,JVM自动优化,适合大多数同步场景
- ReentrantLock:功能丰富,提供更多控制选项,适合复杂同步需求
- 读写锁:读多写少场景的最佳选择,大幅提高并发性能
- StampedLock:Java 8新增,提供乐观读模式,进一步提高性能
- AQS:并发框架的基石,理解AQS有助于深入理解Java并发机制
在实际开发中,应根据具体需求选择合适的锁机制,并遵循最佳实践以避免死锁和性能问题。
9. Java内存模型(JMM)深度解析
1. JMM 概述
1.1 什么是JMM
Java内存模型(Java Memory Model,JMM)是Java虚拟机规范中定义的一种抽象概念,它定义了Java程序中各种变量(线程共享变量)的访问规则,以及在虚拟机中将变量存储到内存和从内存中读取变量这样的底层细节。
1.2 为什么需要JMM
在多核处理器时代,每个处理器都有自己的缓存,这就导致了内存可见性问题。JMM的主要目的是:
- 屏蔽各种硬件和操作系统的内存访问差异
- 保证Java程序在各种平台下都能达到一致的内存访问效果
- 规定线程如何以及何时可以看到其他线程修改过的共享变量
- 规范如何同步访问共享变量
2. 硬件内存架构与JMM
2.1 现代计算机内存架构
现代计算机系统通常采用多级缓存结构:
- CPU寄存器:速度最快,容量最小
- CPU缓存(L1、L2、L3):速度较快,容量较小
- 主内存(RAM):速度较慢,容量较大
graph TB
subgraph "CPU Core 1"
C1[Core 1]
R1[Registers]
L1_1[L1 Cache]
end
subgraph "CPU Core 2"
C2[Core 2]
R2[Registers]
L1_2[L1 Cache]
end
subgraph "Shared Cache"
L2[L2 Cache]
L3[L3 Cache]
end
subgraph "Main Memory"
RAM[RAM]
end
C1 --> R1
C1 --> L1_1
C2 --> R2
C2 --> L1_2
L1_1 --> L2
L1_2 --> L2
L2 --> L3
L3 --> RAM
2.2 内存访问问题
- 可见性问题:一个线程对共享变量的修改,另一个线程不能立即看到
- 原子性问题:一个操作被线程调度器中断,可能只执行了一部分
- 有序性问题:编译器和处理器可能会对指令进行重排序
3. JMM的核心概念
3.1 主内存与工作内存
JMM规定了所有变量都存储在主内存中,每个线程还有自己的工作内存:
- 主内存:所有线程共享的内存区域
- 工作内存:每个线程私有的内存区域,存储该线程使用到的变量的主内存副本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class JMMExample { private static int sharedVariable = 0; public static void main(String[] args) { new Thread(() -> { int localCopy = sharedVariable; localCopy++; sharedVariable = localCopy; }).start(); new Thread(() -> { int localCopy = sharedVariable; System.out.println(localCopy); }).start(); } }
|
3.2 内存间交互操作
JMM定义了8种原子操作来完成主内存与工作内存之间的交互:
- lock(锁定):作用于主内存变量,标识变量为线程独占
- unlock(解锁):作用于主内存变量,释放锁定状态
- read(读取):作用于主内存变量,将变量值传输到工作内存
- load(载入):作用于工作内存变量,将read操作得到的值放入变量副本
- use(使用):作用于工作内存变量,将变量值传递给执行引擎
- assign(赋值):作用于工作内存变量,将执行引擎接收的值赋给变量
- store(存储):作用于工作内存变量,将变量值传输到主内存
- write(写入):作用于主内存变量,将store操作得到的值放入变量
4. 并发编程的三大问题
4.1 原子性问题
原子性是指一个操作是不可中断的,要么全部执行成功,要么全部不执行。
1 2 3 4 5 6 7 8 9 10 11 12
| public class AtomicityProblem { private int count = 0; public void increment() { count++; } }
|
4.2 可见性问题
一个线程对共享变量的修改,另一个线程能够立即看到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class VisibilityProblem { private static boolean flag = true; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (flag) { } System.out.println("Thread stopped"); }).start(); Thread.sleep(1000); flag = false; System.out.println("Flag set to false"); } }
|
4.3 有序性问题
程序执行的顺序可能会被编译器和处理器重排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class OrderingProblem { private static int x = 0, y = 0; private static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000000; i++) { x = 0; y = 0; a = 0; b = 0; Thread one = new Thread(() -> { a = 1; x = b; }); Thread two = new Thread(() -> { b = 1; y = a; }); one.start(); two.start(); one.join(); two.join(); if (x == 0 && y == 0) { System.out.println("Reordering occurred at iteration " + i); break; } } } }
|
5. happens-before 原则
happens-before是JMM的核心概念,它定义了操作之间的内存可见性关系。
5.1 基本原则
- 程序次序规则:在一个线程内,按照程序代码顺序,前面的操作happens-before于后面的操作
- 监视器锁规则:对一个锁的解锁happens-before于随后对这个锁的加锁
- volatile变量规则:对一个volatile域的写happens-before于任意后续对这个volatile域的读
- 线程启动规则:Thread对象的start()方法happens-before于此线程的每一个动作
- 线程终止规则:线程中的所有操作都happens-before于对此线程的终止检测
- 线程中断规则:对线程interrupt()方法的调用happens-before于被中断线程的代码检测到中断事件的发生
- 对象终结规则:一个对象的初始化完成happens-before于它的finalize()方法的开始
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
5.2 happens-before示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class HappensBeforeExample { private int x = 0; private volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v) { System.out.println(x); } } }
|
6. volatile 关键字
6.1 volatile的内存语义
- 可见性:保证对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入
- 禁止指令重排序:通过内存屏障实现
6.2 volatile的实现原理
JVM会在volatile变量的读写操作前后插入内存屏障:
写操作:
- StoreStore屏障:禁止上面的普通写与下面的volatile写重排序
- StoreLoad屏障:禁止上面的volatile写与下面可能有的volatile读/写重排序
读操作:
- LoadLoad屏障:禁止下面的普通读和上面的volatile读重排序
- LoadStore屏障:禁止下面的普通写和上面的volatile读重排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class VolatileExample { private volatile boolean flag = false; private int value = 0; public void write() { value = 42; flag = true; } public void read() { if (flag) { System.out.println(value); } } }
|
6.3 volatile的适用场景
- 状态标志位
- 一次性安全发布(double-checked locking)
- 独立观察(定期更新”发布”观察结果供程序使用)
- volatile bean模式
7. synchronized 的内存语义
7.1 锁的释放和获取的内存语义
- 锁释放:当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
- 锁获取:当线程获取锁时,JMM会把该线程对应的工作内存置为无效,从而临界区的代码必须从主内存中读取共享变量
7.2 实现原理
synchronized基于Monitor实现,编译后会在同步块前后生成monitorenter和monitorexit字节码指令。
1 2 3 4 5 6 7 8 9 10
| public class SynchronizedMemorySemantics { private int x = 0; private final Object lock = new Object(); public void increment() { synchronized (lock) { x++; } } }
|
8. final 域的内存语义
8.1 final域的重排序规则
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序
8.2 final域的内存语义实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class FinalFieldExample { final int x; int y; static FinalFieldExample f; public FinalFieldExample() { x = 3; y = 4; } public static void writer() { f = new FinalFieldExample(); } public static void reader() { if (f != null) { int i = f.x; int j = f.y; } } }
|
9. 内存屏障
9.1 内存屏障类型
- LoadLoad屏障:Load1; LoadLoad; Load2
- StoreStore屏障:Store1; StoreStore; Store2
- LoadStore屏障:Load1; LoadStore; Store2
- StoreLoad屏障:Store1; StoreLoad; Load2
9.2 JVM中的内存屏障
不同的处理器架构有不同的内存屏障指令,JVM会针对不同的平台生成相应的内存屏障指令。
10. JMM的设计理念
10.1 顺序一致性模型
顺序一致性内存模型是一个理论参考模型,特点:
- 所有操作按程序顺序串行执行
- 所有线程只能看到单一的操作执行顺序
- 每个操作都必须原子执行且立刻对所有线程可见
10.2 JMM与顺序一致性模型的差异
JMM在正确同步的程序中具有顺序一致性,但在未正确同步的程序中,为了性能优化,允许出现重排序等现象。
11. 实践建议
11.1 正确使用同步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class CorrectSynchronization { private static int unsafeCounter = 0; private static volatile boolean flag = false; private static int safeCounter = 0; private static final Object lock = new Object(); public static void increment() { synchronized (lock) { safeCounter++; } } private static AtomicInteger atomicCounter = new AtomicInteger(0); public static void atomicIncrement() { atomicCounter.incrementAndGet(); } }
|
11.2 避免常见的并发陷阱
- 不要依赖线程优先级
- 避免过度同步
- 使用线程安全集合
- 注意死锁、活锁和饥饿问题
- 使用合适的并发工具类
总结
Java内存模型是Java并发编程的基石,它通过定义一系列规则和happens-before关系,为开发者提供了编写正确并发程序的理论基础。理解JMM有助于:
- 理解多线程程序的行为
- 编写正确的并发代码
- 诊断和解决并发问题
- 选择合适的同步机制
在实际开发中,应该优先使用java.util.concurrent包提供的高级并发工具,这些工具已经正确实现了JMM的要求,能够帮助开发者编写出高效且正确的并发程序。