Java 锁的介绍

阿里云教程14小时前发布
2 0 0

一、为什么需要锁?——并发问题的根源

在多线程环境下,多个线程同时访问和修改共享变量,会导致竞态条件(Race Condition)。

先来看一个例子:

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // 创建两个线程,同时对计数器进行递增操作
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });

        // 启动两个线程
        thread1.start();
        thread2.start();

        // 等待两个线程执行完毕
        thread1.join();
        thread2.join();

        // 输出最终的计数结果
        System.out.println("Final count: " + counter.getCount());
    }
}

返回的结果:

Java 锁的介绍

在这个例子中,我们定义了一个 Counter 类,它包含一个 count 变量和一个 increment 方法,用于对 count 进行递增操作。然后,我们创建了两个线程,分别对计数器进行 1000 次递增操作。

由于 increment 方法中的 count++ 操作不是原子性的,它实际上包含三个步骤:读取 count 的值,将 count 的值加 1,然后将新的值写回 count。当多个线程同时执行 increment 方法时,可能会出现以下情况:

  1. 线程 A 读取 count 的值,列如是 10。
  2. 线程 B 读取 count 的值,也是 10。
  3. 线程 A 将 count 的值加 1,得到 11,并写回 count。
  4. 线程 B 也将 count 的值加 1,得到 11,并写回 count。

结果是,两个线程都对 count 进行了一次递增操作,但 count 的值只增加了 1,而不是预期的 2。这就是线程不安全的一个典型例子。

二、悲观锁 vs 乐观锁:核心思想对比

特性

悲观锁(Pessimistic Locking)

乐观锁(Optimistic Locking)

核心思想

“总会有人抢,我先锁住再说”

“大家很文明,我先改,最后检查是否被改过”

加锁时机

操作前就加锁

不加锁,更新时检查

性能

低(阻塞线程)

高(无锁,减少等待)

适用场景

写操作频繁,冲突多

读多写少,冲突少

实现方式

synchronized、ReentrantLock

CAS、版本号、时间戳


三、实验环境设定

  • 目标:1000 个线程,每个线程对 count 自增 100 次
  • 期望结果:1000 × 100 = 100,000
  • 测试方式:分别测试 不加锁、synchronized、ReentrantLock、AtomicInteger

四、第一阶段:不加锁 vs 悲观锁(synchronized)对比

public class UnsafeCounter {
    private int count = 0;
    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

2. 加锁版本(synchronized)

public class SynchronizedCounter {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

对比实验结果

【不加锁】最终结果: 98765 (耗时: 52 ms)
【synchronized】最终结果: 100000 (耗时: 120 ms)

✅ 结论:不加锁快但错,synchronized 慢但正确。


五、第二阶段:不加锁 vs 悲观锁(ReentrantLock)对比

1. 不加锁版本(同上)

2. 加锁版本(ReentrantLock)

import java.util.concurrent.locks.ReentrantLock;
public class ReentrantCounter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

对比实验结果

【不加锁】最终结果: 98765 (耗时: 52 ms)
【ReentrantLock】最终结果: 100000 (耗时: 110 ms)

✅ 结论:ReentrantLock 比 synchronized 略快,且功能更强劲。


六、第三阶段:不加锁 vs 乐观锁(AtomicInteger)对比

1. 不加锁版本(int 类型)

public class IntCounter {
    private int count = 0;
    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

2. 乐观锁版本(AtomicInteger)

import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);
    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

对比实验结果

【int 不加锁】最终结果: 98765 (耗时: 52 ms)
【AtomicInteger】最终结果: 100000 (耗时: 60 ms)

✅ 结论:AtomicInteger 性能高且正确,适合高并发场景。


七、核心工具详解

7.1synchronized:概念、用法、注意点、问题

概念

  • Java 内置关键字,由 JVM 实现
  • 可重入、非公平、互斥锁
  • 自动加锁/释放,无需手动干预
  • 支持三种形式:
  • 修饰实例方法(锁 this) 实例锁 :跟实例走有多少个实例就会有多少把锁,锁的数据 量和实例对象的数据相等
  • 修饰静态方法(锁 类.class)类锁:只有一把,不管有多少实例,它只有一把
  • 修饰代码块(锁指定对象)

✅ 用法示例

// 1. 修饰方法(锁 this)
public synchronized void method() { ... }
// 2. 修饰代码块(锁指定对象)
private final Object lock = new Object();
synchronized (lock) { ... }
// 3. 静态方法(锁类)
public static synchronized void staticMethod() { ... }

✅ 注意事项

  • 可重入性:同一线程可多次进入同一把锁(JVM 维护计数器)
  • 自动释放:方法结束或异常时自动释放,不会因忘记 unlock 而死锁
  • 锁升级:JVM 会自动优化(偏向锁 → 轻量级锁 → 重量级锁)
  • 性能:JDK 1.6+ 优化后性能已大幅提升

⚠️ 常见问题与陷阱

  1. 锁对象选择错误
  2. ❌ synchronized(“lock”):字符串常量池可能导致锁冲突
  3. ✅ 使用 private final Object lock = new Object();
  4. 锁范围过大
  5. ❌ 将 I/O 操作也包含在 synchronized 块中
  6. ✅ 只锁临界区,缩小锁粒度
  7. 死锁风险
  8. 多个 synchronized 嵌套时,锁顺序不一致可能导致死锁
  9. 示例:线程A先锁A再锁B,线程B先锁B再锁A → 死锁!

7.2ReentrantLock:概念、用法、注意点、问题

✅ 概念

  • java.util.concurrent.locks.ReentrantLock
  • 显式锁,需手动控制加锁/释放,并且需要成对使用
  • 支持:
  • 公平锁 / 非公平锁(构造函数参数)
  • 可中断等待(lockInterruptibly())
  • 超时获取(tryLock(timeout))
  • 多个 Condition 条件变量

✅ 用法示例

private final ReentrantLock lock = new ReentrantLock(); // 默认非公平
public void method() {
    lock.lock(); // 手动加锁
    try {
        // 临界区
        count++;
    } finally {
        lock.unlock(); // 必须在 finally 中释放
    }
}

✅ 注意事项

  • 必须手动释放:unlock() 必须在 finally 块中调用
  • 可重入性:同 synchronized,支持同一线程重复获取
  • 公平性:可设置 true 启用公平锁(先来先得)
  • 灵活性:适合复杂场景(如超时、中断、条件等待)

⚠️ 常见问题与陷阱

  1. 忘记 unlock() → 死锁!
  2. ❌ 在 try 块外 unlock,异常时无法释放
  3. ✅ 必须在 finally 中调用 unlock()
  4. 重复 unlock()
  5. ❌ 同一线程多次 unlock 会抛 IllegalMonitorStateException
  6. ✅ 确保 lock 和 unlock 成对出现
  7. 未处理中断
  8. lockInterruptibly() 可能抛 InterruptedException
  9. 必须捕获并处理,否则线程中断状态被破坏
  10. 锁未释放的异常情况
  11. 如果在 lock() 后抛出异常,且未进入 try 块,unlock() 不会执行
  12. ✅ 始终确保 unlock() 在 finally 中

7.3AtomicInteger:概念、用法、注意点、问题

✅ 概念

  • 基于 CAS(Compare-And-Swap) 原子指令实现 old value 2 origin value new value 1 qidaizhi 1
  • 乐观锁的典型代表 自旋
  • 无锁(lock-free),通过循环重试保证线程安全
  • 属于 java.util.concurrent.atomic 包

用法示例

private AtomicInteger count = new AtomicInteger(0);
public void increment() {
    count.incrementAndGet(); // 原子自增
}

public boolean compareAndSet(int expect, int update) {
    return count.compareAndSet(expect, update);
}

注意事项

  • 高性能:无阻塞,适合高并发读写
  • 简单易用:API 直观,无需手动加锁
  • 适用于简单操作:如自增、赋值、比较交换

⚠️ 常见问题与陷阱

  1. ABA 问题
  2. 线程1读取 A
  3. 线程2 将 A → B → A
  4. 线程1 CAS 成功,但中间状态被改变
  5. ✅ 解决方案:使用 AtomicStampedReference(带版本号)
  6. 复杂逻辑不适合
  7. ❌ 无法原子地执行“读 → 计算 → 写”复合逻辑
  8. ✅ 对于复杂业务,仍需使用悲观锁
  9. 内存占用略高
  10. AtomicInteger 内部使用 volatile int,有必定内存开销
  11. 但在大多数场景下可忽略

八、总结与选型提议

场景

推荐锁类型

缘由

计数器、状态标记

✅ 乐观锁(AtomicXXX)

高并发,冲突少,性能高

金融交易、库存扣减

✅ 悲观锁(ReentrantLock)

安全第一,防止超卖

用户信息更新(数据库)

✅ 乐观锁(@Version)

冲突少,用户体验好

复杂业务逻辑临界区

✅ synchronized

简单安全,自动释放

需要超时或中断控制

✅ ReentrantLock

支持 tryLock / lockInterruptibly

最终结论:

不加锁 = 快但错 → ❌ 绝对禁止

悲观锁 = 慢但稳 → ✅ 安全场景首选

乐观锁 = 快且稳 → ✅ 高并发首选


© 版权声明

相关文章

暂无评论

none
暂无评论...