【JUC】Lock 和 synchronzied 锁

在 Java 中,synchronized 和 Lock 都是用来实现线程同步、保证共享资源安全访问的机制。它们解决的是同一个问题------多个线程同时访问临界区时的互斥与可见性,但在使用方式、灵活性和底层实现上有明显区别。

下面从几个维度来对比和讲解。


一、synchronized

synchronized 是 Java 语言内置的关键字,属于隐式锁,由 JVM 底层实现。

1. 三种用法

java 复制代码
// 1. 修饰实例方法:锁的是当前对象 this
public synchronized void method() { ... }

// 2. 修饰静态方法:锁的是当前类的 Class 对象
public static synchronized void staticMethod() { ... }

// 3. 修饰代码块:锁的是括号里指定的对象
public void block() {
    synchronized (lockObj) {
        // 临界区
    }
}

2. 特点

  • 自动加锁/释放锁:进入同步块自动加锁,退出(正常或异常)自动释放,不会忘记解锁。
  • 可重入:同一线程可以重复获取同一把锁。
  • 不可中断:等待锁的线程无法被中断,只能一直等。
  • 非公平:默认是非公平锁(不保证等待时间长的线程优先获得锁)。
  • 无法尝试获取:不能"试一下拿不到就走",只能阻塞等待。
  • 底层实现:早期是重量级锁(依赖操作系统 mutex),JDK 1.6 后引入了偏向锁、轻量级锁、自旋优化等,性能大幅提升。

二、Lock 接口

Lock 是 java.util.concurrent.locks 包下的接口,属于显式锁,需要手动加锁和释放。

1. 核心方法

java 复制代码
public interface Lock {
    void lock();                       // 获取锁,拿不到就阻塞
    void lockInterruptibly() throws InterruptedException; // 可中断地获取锁
    boolean tryLock();                 // 尝试获取,立即返回成功/失败
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 限时尝试
    void unlock();                     // 释放锁
    Condition newCondition();          // 创建条件变量
}

2. 标准使用模板

java 复制代码
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();   // 必须在 finally 中释放
}

3. 常用实现类

  • ReentrantLock:可重入锁,最常用。
  • ReentrantReadWriteLock:读写锁,读读不互斥,读写/写写互斥。
  • StampedLock:JDK 8 引入,支持乐观读,性能更好。

三、核心区别对比

维度 synchronized Lock
层级 JVM 关键字,语言层面 JDK 接口,API 层面
锁的获取/释放 自动 手动,必须 finally 释放
可中断 不支持 lockInterruptibly() 支持
尝试获取 不支持 tryLock() 支持
超时获取 不支持 tryLock(time) 支持
公平性 只能非公平 可选公平/非公平
条件变量 配合 wait/notify,单条件 可创建多个 Condition
读写分离 不支持 ReadWriteLock 支持
可重入 支持 ReentrantLock 支持
性能 JDK 1.6 后优化良好 高竞争下更灵活可控

四、Condition:更精细的等待/通知

synchronized 配合 Object 的 wait/notify 只有一个等待队列。而 Lock 可以创建多个 Condition,实现分组唤醒。

java 复制代码
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

// 生产者
lock.lock();
try {
    while (队列满) notFull.await();
    // 生产...
    notEmpty.signal();
} finally {
    lock.unlock();
}

// 消费者
lock.lock();
try {
    while (队列空) notEmpty.await();
    // 消费...
    notFull.signal();
} finally {
    lock.unlock();
}

这样生产者只唤醒消费者,消费者只唤醒生产者,避免无效唤醒,这也是实现阻塞队列的经典方式。


五、底层原理简述

synchronized

  • 依赖对象头里的 Mark Word 和 Monitor(管程)。
  • 锁状态升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
  • 重量级锁会涉及用户态到内核态的切换,成本较高。

ReentrantLock

  • 基于 AQS(AbstractQueuedSynchronizer) 实现。
  • 内部维护一个 volatile int state 表示锁状态,一个 CLH 双向队列存放等待线程。
  • 加锁就是 CAS 修改 state,失败则入队并 LockSupport.park() 阻塞。
  • 解锁就是 state 减到 0 并唤醒队列中的后继节点。

六、如何选择

优先用 synchronized,因为:

  • 写法简单,不易出错(自动释放)。
  • JVM 持续优化,大多数场景性能足够。
  • 代码可读性好。

以下情况考虑 Lock:

  • 需要可中断、可超时、可尝试获取锁。
  • 需要公平锁。
  • 需要读写分离(读多写少场景用 ReadWriteLock)。
  • 需要多个条件变量精细控制线程唤醒。
  • 需要乐观读(StampedLock)。

七、常见误区

  1. 锁对象选错 :synchronized 修饰方法锁的是 this,如果多个不同实例,锁不住共享的静态资源。
  2. 忘记 unlock :Lock 必须在 finally 中释放,否则死锁。
  3. 锁粒度太大:把无关操作也放进同步块,降低并发度。
  4. 用 String 常量或基本类型包装类当锁:可能因常量池/缓存导致意外的锁共享。
  5. 以为 synchronized 一定慢:现代 JVM 下并非如此,不要盲目替换。

一句话总结 :synchronized 是简单、安全、够用的内置锁;Lock 是灵活、可控、功能更丰富的显式锁。日常优先 synchronized,需要高级特性时再上 Lock。

相关推荐
西柚小萌新1 小时前
【LLM&&AI应用开发 八股文】--4.3.Agent智能体(下)
java·开发语言·数据库
北极有牛1 小时前
cpp学习笔记--常量指针
java·开发语言·算法
java资料站1 小时前
二、Spring AI Alibaba · ChatModel
java·windows·spring
十年Java程序媛2 小时前
Java 接口和抽象类对比|Java8 新特性,抛弃老旧八股,正确选型
java·spring boot·后端
caoerzhong2 小时前
JeeWMS 开源 WMS 部署避坑指南:Java 仓库管理系统的环境基线、四类根因与可复现交付
java·开发语言·开源
用户3721574261352 小时前
Java 合并 PDF 文件:完整合并、指定页面合并与流合并
java
for_ever_love__2 小时前
MySQL 全文索引实战:FULLTEXT、ngram 中文分词与 MATCH AGAINST 到底该怎么用
java·python·mysql·全文检索·分词·索引·ngram
java资料站2 小时前
五、Spring AI Alibaba · Memory · Saver(短期会话)
java·spring·microsoft
夜之眷属3 小时前
Core dump 崩溃排查:JVM 宕机后,那份 core 文件怎么用 gdb 还原现场
java·运维·服务器·jvm