Java主流锁全解析:从分类到实践

在Java并发编程中,锁是解决线程安全问题的核心工具。不同场景下选择合适的锁,直接影响程序的性能与稳定性。

本文将系统梳理Java主流锁分类(包括悲观锁、乐观锁、公平锁、非公平锁、共享锁、排它锁、偏向锁、轻量级锁、自旋锁、自适应自旋锁、重量级锁、无锁),并结合代码示例讲解实现逻辑,最后给出锁选择指南与最佳实践,帮助你在实际开发中精准选型。

一、锁的分类逻辑:按维度梳理更清晰

Java锁的分类并非零散孤立,而是可按「设计思想」「核心特性」「线程行为」「JVM优化」四个维度归类。这种分类方式能帮我们更深刻理解每种锁的设计初衷,先看整体全景图:

下面逐维度解析每种锁的定义、实现与代码示例。

二、思想维度:悲观锁、乐观锁、无锁

思想维度的锁,核心差异是「是否认为线程并发会产生冲突」,决定了锁的底层实现逻辑。

1. 悲观锁:假设冲突必然发生

定义:认为多线程并发时,数据修改冲突是必然的,因此在操作前必须先获取锁,禁止其他线程干扰。是最经典的「先锁后操作」思路。

核心实现

  • synchronized:Java原生关键字,JVM层面实现,自动加锁/释放锁,无需手动管理。
  • ReentrantLock:JUC包下的可重入锁,支持手动加锁/释放(必须在finally中释放),提供中断、超时等高级功能。
  • ReentrantReadWriteLock.WriteLock:读写锁中的写锁,属于悲观排它锁。

代码示例(synchronized与ReentrantLock)

java 复制代码
// 1. synchronized实现:方法级 + 代码块级
public class SynchronizedDemo {
    private int count = 0;

    // 方法级锁:锁对象为当前实例(this)
    public synchronized void increment() {
        count++;
        System.out.println("synchronized方法:count = " + count);
    }

    // 代码块级锁:显式指定锁对象(可自定义,如this、Class对象)
    public void decrement() {
        synchronized (this) { // 锁粒度更细,只锁定核心逻辑
            count--;
            System.out.println("synchronized代码块:count = " + count);
        }
    }
}

// 2. ReentrantLock实现:手动加锁/释放,支持公平性配置
public class ReentrantLockDemo {
    // 非公平锁(默认):new ReentrantLock(true) 为公平锁
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock(); // 手动加锁(若锁被占用,线程阻塞)
        try {
            // 核心业务逻辑:只有持有锁的线程能执行
            count++;
            System.out.println("ReentrantLock:count = " + count);
        } finally {
            // 必须在finally中释放锁,避免异常导致锁泄漏
            lock.unlock();
        }
    }
}

2. 乐观锁:假设冲突概率极低

定义:认为多线程并发时,数据修改冲突概率很低,因此操作前不获取锁,而是在「提交修改」时检查数据是否被其他线程篡改,若未篡改则成功,否则重试/失败。核心是「先操作后校验」。

核心实现

  • CAS操作:Compare And Swap(比较并交换),CPU原子指令,是乐观锁的底层核心。
  • 原子类AtomicIntegerAtomicLong等,基于CAS实现无锁并发安全。
  • 版本号机制 :如数据库乐观锁(通过version字段校验)、JPA的@Version注解。
  • StampedLock乐观读:JUC包下的高级锁,支持乐观读模式,性能优于传统读写锁。

代码示例(原子类与StampedLock乐观读)

java 复制代码
// 1. AtomicInteger原子类:基于CAS实现无锁累加
public class AtomicDemo {
    // AtomicInteger的incrementAndGet()是CAS原子操作
    private final AtomicInteger count = new AtomicInteger(0);

    public void safeIncrement() {
        // 无需加锁,底层通过CAS保证并发安全
        int newCount = count.incrementAndGet();
        System.out.println("AtomicInteger:count = " + newCount);
    }

    // 手动CAS校验:compareAndSet(预期值, 新值)
    public boolean updateIfMatch(int expect, int update) {
        return count.compareAndSet(expect, update);
    }
}

// 2. StampedLock乐观读:读多写少场景性能最优
public class StampedLockOptimisticDemo {
    private final StampedLock lock = new StampedLock();
    private double x = 0.0, y = 0.0; // 模拟共享数据

    // 乐观读:无锁读取,提交前校验
    public double distanceFromOrigin() {
        // 1. 尝试乐观读,返回一个「邮戳」(stamp)
        long stamp = lock.tryOptimisticRead();
        // 2. 无锁读取数据(此时可能被其他线程修改)
        double currentX = x;
        double currentY = y;

        // 3. 校验邮戳:若stamp有效,说明数据未被修改;否则升级为悲观读锁
        if (!lock.validate(stamp)) {
            // 乐观读失败,升级为悲观读锁(避免数据不一致)
            stamp = lock.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                lock.unlockRead(stamp); // 释放读锁
            }
        }

        // 4. 计算结果(无锁/已升级为读锁,数据安全)
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 写操作:悲观排它锁
    public void move(double deltaX, double deltaY) {
        long stamp = lock.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            lock.unlockWrite(stamp);
        }
    }
}

3. 无锁:完全不依赖锁的并发控制

定义:比乐观锁更极致的并发策略------完全不使用锁机制,直接通过CPU原子指令(如CAS)或线程本地存储(ThreadLocal)保证线程安全,不存在「锁竞争」问题,性能最优。

核心实现

  • Atomic系列原子类(如AtomicIntegerAtomicReference)。
  • Unsafe.CASsun.misc.Unsafe类的CAS方法,是原子类的底层依赖(不建议直接使用,需通过反射获取)。
  • ThreadLocal:通过线程隔离实现无锁(每个线程持有独立副本,无共享则无竞争)。

注意:无锁并非「不需要保证安全」,而是通过非锁机制实现安全,适用于「线程间无共享数据修改」或「修改冲突极少」的场景。

三、特性维度:公平锁、非公平锁、共享锁、排它锁

特性维度的锁,核心差异是「锁的分配规则」与「锁的持有方式」,决定了锁在多线程竞争下的行为表现。

1. 公平锁 vs 非公平锁:锁的分配规则

  • 公平锁:严格按照线程「申请锁的顺序」分配锁,先申请的线程先获得锁,避免线程饥饿(长期得不到锁)。
  • 非公平锁:不保证申请顺序,允许「插队」------刚释放锁的线程可能直接重新获取锁(无需排队),减少线程切换开销,性能更高(默认选择)。

核心实现

锁类型 公平锁实现 非公平锁实现(默认)
ReentrantLock new ReentrantLock(true) new ReentrantLock()
读写锁 new ReentrantReadWriteLock(true) new ReentrantReadWriteLock()
synchronized 不支持(始终是非公平锁) 支持(默认)

代码示例(公平锁与非公平锁对比)

java 复制代码
public class FairVsUnfairDemo {
    // 公平锁:按申请顺序分配
    private final ReentrantLock fairLock = new ReentrantLock(true);
    // 非公平锁:允许插队(默认)
    private final ReentrantLock unfairLock = new ReentrantLock();

    // 公平锁测试:先启动的线程先获得锁
    public void testFairLock() {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                fairLock.lock();
                try {
                    System.out.println("公平锁 - 获得锁的线程:" + Thread.currentThread().getName());
                    Thread.sleep(100); // 模拟业务耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    fairLock.unlock();
                }
            }, "Fair-Thread-" + i).start();
        }
        // 输出结果:Fair-Thread-0 → Fair-Thread-1 → Fair-Thread-2(严格顺序)
    }

    // 非公平锁测试:可能插队
    public void testUnfairLock() {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                unfairLock.lock();
                try {
                    System.out.println("非公平锁 - 获得锁的线程:" + Thread.currentThread().getName());
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    unfairLock.unlock();
                }
            }, "Unfair-Thread-" + i).start();
        }
        // 输出结果:可能出现 Unfair-Thread-0 → Unfair-Thread-0(释放后直接重获)
    }
}

2. 共享锁 vs 排它锁:锁的持有方式

  • 共享锁(读锁):允许多个线程同时持有锁,仅限制「写操作」,不限制「读操作」。适用于「读多写少」场景(如查询数据)。
  • 排它锁(写锁):仅允许一个线程持有锁,完全禁止其他线程(读/写)获取锁。适用于「写操作」(如修改数据)。

核心实现

  • ReentrantReadWriteLock:JUC包下的读写锁,包含ReadLock(共享锁)和WriteLock(排它锁)。
  • StampedLock:支持共享读锁与排它写锁,且支持乐观读升级。
  • synchronized:仅支持排它锁,不支持共享。

代码示例(ReentrantReadWriteLock读写分离)

java 复制代码
public class ReadWriteLockDemo {
    // 读写锁:维护一对锁(读锁+写锁)
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock(); // 共享锁(读锁)
    private final Lock writeLock = rwLock.writeLock(); // 排它锁(写锁)

    private Map<String, String> cache = new HashMap<>(); // 模拟缓存(共享数据)

    // 读操作:共享锁,多线程可同时读
    public String getFromCache(String key) {
        readLock.lock();
        try {
            System.out.println("读锁 - 线程:" + Thread.currentThread().getName() + " 读取key:" + key);
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }

    // 写操作:排它锁,仅单线程可写
    public void putToCache(String key, String value) {
        writeLock.lock();
        try {
            System.out.println("写锁 - 线程:" + Thread.currentThread().getName() + " 写入key:" + key);
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockDemo demo = new ReadWriteLockDemo();

        // 3个读线程同时读:无冲突
        for (int i = 0; i < 3; i++) {
            new Thread(() -> demo.getFromCache("user1"), "Read-Thread-" + i).start();
        }

        // 1个写线程写:会阻塞读线程,直到写锁释放
        new Thread(() -> demo.putToCache("user1", "Alice"), "Write-Thread").start();
    }
}

四、行为维度:自旋锁、自适应自旋锁

行为维度的锁,核心差异是「线程等待锁时的策略」------是「忙等」还是「阻塞」,决定了锁的上下文切换开销。

1. 自旋锁:忙等不阻塞

定义:当线程请求锁被占用时,不立即阻塞(阻塞会导致CPU上下文切换,开销大),而是通过「循环重试」的方式等待锁释放(即「忙等」)。适用于「锁持有时间短、线程数少」的场景。

核心实现

  • AtomicBoolean自旋 :通过AtomicBoolean的CAS操作实现简单自旋锁。
  • JVM轻量级锁自旋 :JVM对synchronized的优化,轻量级锁阶段会自旋重试。

代码示例(简单自旋锁实现)

java 复制代码
// 基于AtomicBoolean实现的自旋锁
public class SpinLock {
    private final AtomicBoolean locked = new AtomicBoolean(false);

    // 加锁:自旋重试,直到CAS成功(获取锁)
    public void lock() {
        // CAS尝试将locked从false改为true,失败则循环重试(自旋)
        while (!locked.compareAndSet(false, true)) {
            // 自旋过程:可加Thread.yield()减少CPU占用(可选)
            // Thread.yield();
        }
    }

    // 解锁:将locked设为false
    public void unlock() {
        locked.set(false);
    }

    // 测试:自旋锁的使用
    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        int count = 0;

        Runnable task = () -> {
            spinLock.lock();
            try {
                for (int i = 0; i < 1000; i++) {
                    count++;
                }
            } finally {
                spinLock.unlock();
            }
        };

        new Thread(task).start();
        new Thread(task).start();

        // 最终count=2000(并发安全)
        System.out.println("最终count:" + count);
    }
}

2. 自适应自旋锁:动态调整忙等时长

定义:自旋锁的优化版------自旋次数不再是固定值,而是JVM根据「历史自旋结果」动态调整:

  • 若某线程自旋成功获取锁,下次会增加自旋次数(认为后续仍可能成功)。
  • 若某线程自旋多次失败,下次会减少自旋次数甚至直接阻塞(避免无效忙等)。

核心实现

  • JVM内置实现:synchronized的轻量级锁阶段默认使用自适应自旋,无需手动编码。
  • 优势:兼顾「短锁持有时间」的自旋收益与「长锁持有时间」的阻塞开销,性能更优。

五、JVM锁优化:偏向锁、轻量级锁、重量级锁

这三类锁并非独立锁类型,而是 synchronized的「锁升级路径」------JVM根据「锁竞争激烈程度」自动切换锁状态,实现性能最优。

1. 锁升级逻辑

synchronized的锁状态存储在对象头(Mark Word)中,升级路径为: 无锁 → 偏向锁 → 轻量级锁 → 重量级锁(只能升级,不能降级)。

锁状态 适用场景 实现逻辑
偏向锁 无竞争(单线程重复加锁) 给对象头标记「线程ID」,后续该线程加锁无需CAS,直接复用锁。
轻量级锁 轻微竞争(多线程交替加锁) 线程将对象头Mark Word复制到栈帧,通过CAS竞争锁,竞争失败则自旋重试。
重量级锁 激烈竞争(多线程同时抢锁) 依赖OS互斥量(Mutex)实现,竞争失败的线程直接阻塞,避免无效自旋。

2. 代码示例(synchronized锁升级演示)

java 复制代码
public class LockUpgradeDemo {
    private final Object lock = new Object(); // 锁对象
    private int count = 0;

    public void demonstrateUpgrade() {
        // 1. 无竞争 → 偏向锁:单线程重复加锁
        synchronized (lock) {
            count++;
            System.out.println("阶段1:偏向锁(单线程加锁),count = " + count);
        }

        // 2. 轻微竞争 → 轻量级锁:多线程交替加锁(无同时抢锁)
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                count++;
                System.out.println("阶段2:轻量级锁(t1加锁),count = " + count);
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                count--;
                System.out.println("阶段2:轻量级锁(t2加锁),count = " + count);
            }
        });

        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 3. 激烈竞争 → 重量级锁:多线程同时抢锁
        System.out.println("阶段3:开始激烈竞争,升级为重量级锁");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                synchronized (lock) {
                    count++;
                    System.out.println("重量级锁 - 线程:" + Thread.currentThread().getName() + ",count = " + count);
                }
            }, "Heavy-Thread-" + i).start();
        }
    }

    public static void main(String[] args) {
        new LockUpgradeDemo().demonstrateUpgrade();
    }
}

六、锁选择指南:场景化选型建议

掌握锁分类后,核心是「根据场景选对锁」。以下是常见场景的推荐方案:

业务场景 推荐锁类型 选型理由
简单同步逻辑(如单例、计数器) synchronized JVM自动优化(偏向/轻量/重量级),代码简洁,无锁泄漏风险。
需要中断/超时/公平锁 ReentrantLock 支持lockInterruptibly()(可中断)、tryLock(long)(超时)、公平配置。
读多写少(如缓存、报表) ReentrantReadWriteLock / StampedLock 读操作并发执行,StampedLock乐观读性能优于传统读写锁。
高并发计数器(如接口调用量) AtomicLong / LongAdder CAS无锁操作,性能远超synchronizedLongAdder在高并发下更优。
高并发集合(如并发缓存) ConcurrentHashMap 底层用「分段锁+CAS」,支持高并发读写,避免全表锁。
资源池控制(如连接池) Semaphore 精确控制并发线程数(如限制10个线程同时获取连接)。

七、最佳实践:避免锁坑,提升性能

  1. 优先用synchronized :除非需要ReentrantLock的高级功能(如中断、超时),否则优先选synchronized------JVM持续优化(如JDK 1.6后引入偏向锁/轻量级锁),且无需手动释放。
  2. 缩小锁粒度:只锁定「核心业务逻辑」,避免锁范围过大(如不要在锁内执行IO操作)。
  3. 避免死锁 :多锁竞争时,按「固定顺序」获取锁(如按锁对象的hashCode排序),或使用tryLock()超时机制。
  4. 读写分离优先 :读多写少场景必用读写锁(ReentrantReadWriteLock/StampedLock),避免读操作阻塞读操作。
  5. 监控锁竞争 :用JMX(如java.lang.management.LockInfo)或APM工具(如SkyWalking)监控锁等待时间,及时优化高竞争锁。
  6. 慎用乐观锁重试:乐观锁重试次数不宜过多(建议3-5次),失败后可降级为悲观锁或返回友好提示(避免CPU空转)。

八、总结

Java锁的分类并非孤立,而是从「思想」「特性」「行为」「JVM优化」四个维度层层递进。核心记住三点:

  1. 无锁/乐观锁 性能最优,但适用场景有限;悲观锁通用性强,但需注意锁竞争。
  2. synchronized 是基础,JVM自动优化;JUC锁 (如ReentrantLockStampedLock)功能更丰富,需手动管理。
  3. 选型核心是场景 :读多写少用读写锁,简单同步用synchronized,高并发计数用原子类。

希望本文能帮你理清Java锁的脉络,在实际开发中避开锁坑,写出高效、安全的并发代码!如果有疑问或补充,欢迎在评论区交流~

相关推荐
拖拉斯旋风4 小时前
Gitee 新手入门指南:从零开始掌握代码版本管理
面试·程序员
小高0074 小时前
instanceof 和 typeof 的区别:什么时候该用哪个?
前端·javascript·面试
空空kkk4 小时前
Java——接口
java·开发语言·python
MaxHua4 小时前
JAVA开发处理金额的数据类型你知道多少?
java·后端
知其然亦知其所以然4 小时前
我被问懵了:Tomcat 到底有几种部署方式?
后端·面试·tomcat
oak隔壁找我4 小时前
公司级 Maven Parent POM 设计指南
java·后端
uhakadotcom4 小时前
如何从阿里云的sls日志中清洗出有价值的信息?
后端·面试·github
zl9798994 小时前
SpringBoot-Web开发之内容协商
java·spring boot
bb456b4 小时前
Snipaste (截图贴图工具) 精准截图 中文免费版
java·工具·贴图