在现代Java开发中并发编程是绕不开的核心话题。无论是高并发服务器、大数据处理还是普通的Web应用多线程的使用都能极大提升系统性能。然而线程间的资源竞争也带来了数据不一致、死锁、活锁等问题。为了解决这些隐患Java提供了多种锁机制来保证共享数据的安全访问。本文将深入剖析Java中最常用的两类锁——synchronized关键字和Lock接口以ReentrantLock为代表从用法、原理到性能对比辅以完整代码示例帮助你在实际项目中做出正确的技术选型。一、为什么需要锁假设我们有一个银行账户类包含余额和取款方法。如果不加任何同步控制多个线程同时取款会导致余额计算错误public class BankAccount { private int balance 1000; public void withdraw(int amount) { if (balance amount) { // 模拟其他耗时操作 try { Thread.sleep(10); } catch (InterruptedException e) {} balance - amount; } } public int getBalance() { return balance; } public static void main(String[] args) throws InterruptedException { BankAccount account new BankAccount(); Runnable task () - account.withdraw(500); Thread t1 new Thread(task); Thread t2 new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(最终余额 account.getBalance()); // 可能为500或0 } }上述代码中两个线程各取500元最终余额应为0但由于balance amount判断和实际扣除动作不是原子操作可能出现两个线程都通过判断然后各自扣减导致余额变成负数或错误数值。这就是典型的竞态条件。锁的作用就是将这些非原子操作变为原子操作保证同一时刻只有一个线程能修改共享变量。二、synchronized内置锁synchronized是Java语言内置的同步机制使用简单无需手动释放锁。它可以修饰实例方法、静态方法和代码块。1. 修饰实例方法锁住当前实例对象this同一时刻同一个对象的多个同步方法互斥。public class SynchronizedCounter { private int count 0; public synchronized void increment() { count; } public synchronized int getCount() { return count; } }2. 修饰静态方法锁住当前类的Class对象同一类的所有静态同步方法互斥。public class SynchronizedStaticCounter { private static int count 0; public static synchronized void increment() { count; } public static synchronized int getCount() { return count; } }3. 同步代码块可以更精细地控制锁的范围减少锁持有的时间提升并发性。需要显式指定锁对象。public class BankAccountSync { private int balance 1000; private final Object lock new Object(); // 专用锁对象 public void withdraw(int amount) { synchronized (lock) { if (balance amount) { balance - amount; } } } }4. synchronized底层原理synchronized依赖于JVM的对象监视器Monitor。每个对象都与一个监视器关联当线程进入同步代码块前需要成功获得监视器退出时正常或异常释放监视器。在字节码层面同步代码块通过monitorenter和monitorexit指令实现同步方法则通过方法上的ACC_SYNCHRONIZED标志来标识JVM会隐式执行监视器的进入和退出。从JDK 1.6开始JVM对synchronized进行了大量优化引入了偏向锁、轻量级锁和重量级锁以及自旋自适应等技术。初始时锁处于偏向锁状态只有一个线程竞争当有第二个线程竞争时升级为轻量级锁CAS实现竞争激烈时升级为重量级锁操作系统互斥量。因此现代Java中的synchronized性能已经不亚于ReentrantLock且使用更简洁。三、Lock显式锁JDK 1.5引入了java.util.concurrent.locks.Lock接口提供了比synchronized更灵活的锁操作。最常用的实现是ReentrantLock。1. Lock接口核心方法void lock()获取锁如果锁被占用则阻塞直到获得。boolean tryLock()尝试获取锁立即返回成功/失败。boolean tryLock(long time, TimeUnit unit)带超时时间的尝试。void unlock()释放锁必须在finally块中执行。Condition newCondition()获取条件对象实现等待/通知。2. ReentrantLock基本使用import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockCounter { private int count 0; private final Lock lock new ReentrantLock(); public void increment() { lock.lock(); try { count; } finally { lock.unlock(); // 确保释放锁 } } public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } } }3. 可中断锁synchronized无法响应中断线程阻塞在同步锁上时调用interrupt()无效。而ReentrantLock.lockInterruptibly()支持中断响应public void doSomething() throws InterruptedException { lock.lockInterruptibly(); try { // 临界区代码 } finally { lock.unlock(); } }当另一个线程调用当前线程的interrupt()时lockInterruptibly()会抛出InterruptedException使线程有机会处理中断。4. 尝试非阻塞获取锁使用tryLock()可以实现非阻塞逻辑避免死锁public boolean tryTransfer(ReentrantLockCounter from, ReentrantLockCounter to, int amount) { if (from.lock.tryLock()) { try { if (to.lock.tryLock()) { try { if (from.getCount() amount) { from.count - amount; to.count amount; return true; } } finally { to.lock.unlock(); } } } finally { from.lock.unlock(); } } return false; }5. 公平锁与非公平锁ReentrantLock构造函数可指定公平性。公平锁线程按请求顺序获取锁避免饥饿非公平锁允许插队提高吞吐量。默认非公平。// 公平锁 Lock fairLock new ReentrantLock(true); // 非公平锁默认 Lock unfairLock new ReentrantLock();6. 可重入性synchronized和ReentrantLock通过锁计数器实现可重入。线程首次获取锁时计数器置1后续每次重入计数器递增释放时递减至0才真正释放锁资源。public class ReentrantExample { private final Lock lock new ReentrantLock(); public void outer() { lock.lock(); try { inner(); // 嵌套调用仍可获取锁 } finally { lock.unlock(); } } private void inner() { lock.lock(); try { System.out.println(Critical section); } finally { lock.unlock(); } } }四、读写锁ReentrantReadWriteLock当读操作远多于写操作时使用独占锁会严重限制并发性。读写锁允许多个线程同时读但写操作互斥且与读互斥。ReadWriteLock接口及实现ReentrantReadWriteLock提供了这一能力。public class ReadWriteMapK,V { private final MapK,V map new HashMap(); private final ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); private final Lock readLock rwLock.readLock(); private final Lock writeLock rwLock.writeLock(); public V get(K key) { readLock.lock(); try { return map.get(key); } finally { readLock.unlock(); } } public void put(K key, V value) { writeLock.lock(); try { map.put(key, value); } finally { writeLock.unlock(); } } }五、性能对比与适用场景1. 性能JDK 1.6以前synchronized性能较差ReentrantLock优势明显。现代JVM尤其是1.8synchronized经过偏向锁、轻量级锁优化在低/中度竞争下性能接近甚至超过ReentrantLock。但在极高竞争且需要公平锁时ReentrantLock更可控。2. 功能差异特性synchronizedReentrantLock锁获取方式JVM自动管理手动lock/unlock可中断性不支持lockInterruptibly()公平锁非公平可配置公平/非公平条件变量单一wait/notify多Condition支持3. 选择建议优先使用synchronized代码简洁JVM会持续优化且不易忘记释放锁。如果一个锁仅用于保护少量代码且没有高级需求如超时、中断用synchronized。使用ReentrantLock的场景需要可中断的锁获取。需要非阻塞的tryLock()或带超时的尝试。需要公平锁来避免线程饥饿。需要多个Condition来精细控制等待/唤醒。读写锁场景ReentrantReadWriteLock或StampedLock。六、高级锁StampedLockJava 8StampedLock提供三种模式写锁、读锁悲观读和乐观读。乐观读不加锁通过版本号stamp验证数据一致性适合读多写少的极致优化。public class Point { private double x, y; private final StampedLock sl new StampedLock(); public void move(double dx, double dy) { long stamp sl.writeLock(); try { x dx; y dy; } finally { sl.unlockWrite(stamp); } } public double distance() { long stamp sl.tryOptimisticRead(); double currentX x, currentY y; if (!sl.validate(stamp)) { stamp sl.readLock(); try { currentX x; currentY y; } finally { sl.unlockRead(stamp); } } return Math.sqrt(currentX*currentX currentY*currentY); } }StampedLock不支持可重入且写锁与悲观读锁都不支持条件变量。它的性能极高适用于短小的临界区但使用复杂度较高。七、死锁与避免策略锁虽然能保证线程安全但不当使用会导致死锁——两个线程互相等待对方释放锁永远阻塞。经典例子public class DeadlockDemo { private static final Object lockA new Object(); private static final Object lockB new Object(); public static void main(String[] args) { Thread t1 new Thread(() - { synchronized (lockA) { sleep(100); synchronized (lockB) { System.out.println(t1 done); } } }); Thread t2 new Thread(() - { synchronized (lockB) { sleep(100); synchronized (lockA) { System.out.println(t2 done); } } }); t1.start(); t2.start(); } static void sleep(int ms) { try { Thread.sleep(ms); } catch(InterruptedException e) {} } }避免死锁的策略避免嵌套锁尽量不持有一个锁时再去获取另一个锁。统一锁顺序所有线程按相同的顺序获取锁。使用tryLock超时获取失败时释放已有锁回退重试。使用open-close调用减少锁的持有范围避免长时间占锁。八、最佳实践总结最小化锁的作用范围只在必要的代码段加锁避免Synchronized方法包含耗时IO操作。使用finally释放锁对显式锁必须在finally中unlock()防止异常导致锁泄漏。避免锁上调用外部方法外部方法可能又去拿其他锁或者执行耗时操作增加死锁风险。优先使用并发容器如ConcurrentHashMap、CopyOnWriteArrayList等它们内部已经实现了高效的同步策略减少手动锁的需求。考虑使用原子类对于简单的计数器AtomicInteger、AtomicLong基于CAS无锁操作性能更好。要理解锁的可见性语义不仅保证互斥还保证释放锁前写的内容对后续获取锁的线程可见。九、完整示例模拟售票系统下面是一个综合示例使用ReentrantLock模拟100张票的售票系统有10个窗口线程同时售票带尝试超时和重试机制。import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.TimeUnit; public class TicketSystem { private int tickets 100; private final ReentrantLock lock new ReentrantLock(true); // 公平锁 public boolean sellTicket(String windowName) { boolean acquired false; try { // 尝试获取锁最多等待200毫秒 acquired lock.tryLock(200, TimeUnit.MILLISECONDS); if (!acquired) { System.out.println(windowName 获取锁超时放弃购票); return false; } if (tickets 0) { tickets--; System.out.println(windowName 售出1张票剩余 tickets); return true; } else { System.out.println(windowName 票已售罄); return false; } } catch (InterruptedException e) { System.out.println(windowName 被中断); return false; } finally { if (acquired) { lock.unlock(); } } } public static void main(String[] args) throws InterruptedException { TicketSystem system new TicketSystem(); Runnable task () - { String name Thread.currentThread().getName(); for (int i 0; i 15; i) { boolean success system.sellTicket(name); if (!success i 5) break; // 多次失败后退出 try { Thread.sleep(10); } catch (InterruptedException e) {} } }; for (int i 1; i 10; i) { new Thread(task, 窗口- i).start(); } } }该示例展示了tryLock超时、锁释放的规范性以及公平锁的排队效果适合实际项目参考。十、结语Java的锁机制从内置synchronized到功能丰富的Lock接口再到高性能的StampedLock为并发编程提供了层层递进的工具。掌握它们的关键不在于记住多少API而在于理解锁的本质——保证临界区互斥与内存可见性。在实际开发中先问自己是否真的需要手动锁能否用并发容器或原子类代替如果必须使用锁优先选择synchronized只有在需要高级特性时才切换到ReentrantLock或读写锁。同时务必注意锁的粒度、顺序和超时处理避免死锁和性能瓶颈。希望本文的详实讲解和代码示例能够帮助你深入理解Java锁机制并在项目中写出高效、安全的并发代码。如果你有任何疑问或建议欢迎在评论区留言讨论