【面试专栏|Java并发编程】Java并发锁对比:synchronized与Lock,底层原理+适用场景详解


🍃 予枫个人主页
📚 个人专栏 : 《Java 从入门到起飞》《读研码农的干货日常》《Java 面试刷题指南

💻 Debug 这个世界,Return 更好的自己!


引言

面试Java并发,synchronized与Lock接口的对比绝对是高频考点!很多程序员只会用,却分不清两者的底层差异、适用场景,被面试官追问"什么时候用synchronized?什么时候用Lock?"时直接翻车。今天就从底层原理、核心区别、适用场景三个维度,结合案例和面试追问,帮你彻底分清两者,面试不慌、实战不踩坑!

文章目录

一、前言:为什么要对比synchronized与Lock?

在Java并发编程中,synchronized和Lock都是实现线程同步、保证线程安全的核心方式,但两者的设计理念、底层实现、使用场景截然不同。

  • synchronized是Java内置关键字,简单易用、自动释放锁,适合大多数基础同步场景;
  • Lock是Java.util.concurrent.locks包下的接口,需手动释放锁、功能更灵活,适合复杂并发场景。

💡 小提示:面试时,面试官不仅会问"两者有什么区别",还会追问"为什么这么设计""实际项目中怎么选",建议点赞收藏,吃透这篇,轻松应对所有相关追问!

二、synchronized 与 Lock 核心区别(底层+用法,面试必答)

两者的区别主要集中在底层实现、锁特性、用法三个维度,用表格对比更清晰,面试时直接按这个框架回答,逻辑更清晰!

对比维度 synchronized Lock接口(以ReentrantLock为例)
底层实现 依赖JVM的Monitor监视器锁,基于对象头 依赖AQS(AbstractQueuedSynchronizer)框架,基于CAS操作
锁释放 自动释放(代码执行完/抛出异常时自动释放) 手动释放(必须在finally块中调用unlock(),否则会造成死锁)
锁类型 非公平锁(默认),不可手动指定 可指定公平锁/非公平锁(构造方法传入boolean值)
可中断性 不可中断,线程获取锁时会一直阻塞,除非获取到锁或被中断 可中断(调用lockInterruptibly()方法,可响应中断)
尝试获取锁 不可尝试,一旦调用,必须等待锁释放 可尝试获取锁(tryLock()方法,超时可放弃,避免死锁)
条件变量 无专门条件变量,需通过wait()/notify()/notifyAll()配合 有Condition接口,可实现多条件唤醒(精准唤醒特定线程)
性能 JDK1.6优化后(偏向锁、轻量级锁),性能接近Lock;高并发下略逊于Lock 高并发场景下性能更优,灵活度更高

2.1 底层实现差异(面试官重点追问)

synchronized 底层实现

synchronized的底层依赖JVM的Monitor(监视器锁),关联对象头的Mark Word,之前在synchronized核心原理中详细讲过,这里重点对比Lock:

  • 当线程获取synchronized锁时,本质是获取对象对应的Monitor所有权;
  • 释放锁时,自动释放Monitor,无需手动操作,JVM会处理异常场景下的锁释放。

Lock 底层实现

Lock接口的核心实现类是ReentrantLock,底层依赖AQS框架(抽象队列同步器):

  • AQS内部维护一个volatile修饰的状态变量(state),用于表示锁的持有状态;
  • 线程通过CAS操作修改state的值,获取锁(state从0变为1)和释放锁(state从1变为0);
  • AQS维护一个等待队列,未获取到锁的线程会进入队列等待,避免忙等,提升性能。

2.2 用法差异(结合代码案例,一看就会)

synchronized 用法(3种场景,简单易用)

java 复制代码
// 场景1:修饰普通方法(锁this)
public synchronized void syncMethod() {
    // 同步代码
    System.out.println("synchronized修饰普通方法");
}

// 场景2:修饰静态方法(锁Class对象)
public static synchronized void syncStaticMethod() {
    // 同步代码
    System.out.println("synchronized修饰静态方法");
}

// 场景3:修饰代码块(显式指定锁对象)
public void syncBlock() {
    Object lock = new Object();
    synchronized (lock) {
        // 同步代码
        System.out.println("synchronized修饰代码块");
    }
}

✅ 优势:无需手动释放锁,代码简洁,不易出错,适合简单同步场景。

Lock 用法(需手动释放,灵活度高)

以ReentrantLock(最常用的Lock实现类)为例:

java 复制代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    // 1. 创建Lock对象,可指定公平锁(true)/非公平锁(false,默认)
    private final Lock lock = new ReentrantLock(true);

    public void lockMethod() {
        // 2. 手动获取锁
        lock.lock();
        try {
            // 3. 同步代码
            System.out.println("Lock接口实现同步");
        } finally {
            // 4. 手动释放锁(必须在finally中,避免异常导致死锁)
            lock.unlock();
        }
    }

    // 尝试获取锁(超时放弃,避免死锁)
    public void tryLockMethod() throws InterruptedException {
        // 尝试获取锁,超时时间3秒,获取失败则放弃
        if (lock.tryLock(3, TimeUnit.SECONDS)) {
            try {
                System.out.println("尝试获取锁成功");
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("尝试获取锁失败,超时放弃");
        }
    }
}

⚠️ 注意:Lock必须手动释放锁,若忘记在finally中调用unlock(),会导致锁无法释放,造成死锁,这是新手最容易踩的坑!

2.3 核心特性差异(实战重点)

  1. 锁的公平性:synchronized只能是非公平锁,Lock可自由选择(公平锁适合对顺序要求高的场景,非公平锁性能更优);
  2. 可中断性:Lock的lockInterruptibly()方法可中断线程的锁等待,避免线程一直阻塞(比如需要停止某个线程时,synchronized无法做到);
  3. 条件唤醒:Lock的Condition接口可实现精准唤醒,比如生产者-消费者模型中,可分别唤醒生产者和消费者,而synchronized的notify()只能随机唤醒一个线程,notifyAll()唤醒所有线程,效率较低。

三、适用场景对比(实战选型指南,避免踩坑)

很多人分不清两者的使用场景,其实核心原则是:简单场景用synchronized,复杂场景用Lock,具体拆解如下:

3.1 优先用 synchronized 的场景

  1. 简单同步场景:比如单线程修改共享变量、简单的方法同步,代码简洁,无需复杂的锁操作;
  2. 不需要灵活控制锁的场景:无需中断锁等待、无需尝试获取锁、无需多条件唤醒,synchronized自动释放锁,不易出错;
  3. 低并发场景:低并发下,synchronized的性能和Lock差距不大,且使用更简单,开发效率更高。

示例:简单的计数器同步(适合用synchronized)

java 复制代码
public class Counter {
    private int count = 0;

    // 简单同步,用synchronized更简洁
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

3.2 优先用 Lock 的场景

  1. 高并发场景:高并发下,Lock的性能更优,AQS框架的等待队列机制比synchronized的Monitor更高效;
  2. 需要灵活控制锁的场景:
    • 需指定公平锁/非公平锁(比如秒杀场景,需要公平分配锁,避免线程饥饿);
    • 需中断锁等待(比如用户取消操作时,中断线程的锁等待,避免资源浪费);
    • 需尝试获取锁(比如超时获取锁,避免线程一直阻塞,造成死锁);
  3. 多条件唤醒场景:比如生产者-消费者模型,需要分别唤醒生产者和消费者,用Lock的Condition接口更高效。

示例:生产者-消费者模型(适合用Lock)

java 复制代码
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ProducerConsumer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int capacity = 5;
    private final Lock lock = new ReentrantLock();
    // 两个条件变量:队列满(生产者等待)、队列空(消费者等待)
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    // 生产者
    public void produce(int value) throws InterruptedException {
        lock.lock();
        try {
            // 队列满,生产者等待
            while (queue.size() == capacity) {
                notFull.await();
            }
            queue.offer(value);
            System.out.println("生产者生产:" + value);
            // 唤醒消费者(队列非空)
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    // 消费者
    public int consume() throws InterruptedException {
        lock.lock();
        try {
            // 队列空,消费者等待
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            int value = queue.poll();
            System.out.println("消费者消费:" + value);
            // 唤醒生产者(队列非满)
            notFull.signal();
            return value;
        } finally {
            lock.unlock();
        }
    }
}

3.3 选型总结(图文可视化)

简单场景(无灵活锁需求、低并发)
复杂场景(高并发、灵活锁需求)
优点:简洁、自动释放锁、不易出错
优点:高性能、可控制、多条件唤醒
并发同步需求
场景复杂度
优先用synchronized
优先用Lock
适合基础同步场景
适合复杂并发场景

💡 互动提示:评论区说说你项目中用synchronized还是Lock?遇到过哪些选型坑?收藏这张流程图,实战选型时直接对照,不踩坑!

四、面试官追问环节(实战必备,拉开差距)

这部分是核心亮点,比纯八股文更有实战价值,提前准备好,面试时直接应对!

追问1:synchronized 和 Lock 的性能对比?JDK1.6后synchronized为什么性能提升这么多?

  1. 性能对比:JDK1.6之前,synchronized是重量级锁,性能远低于Lock;JDK1.6对synchronized进行优化(引入偏向锁、轻量级锁、自旋锁等),性能接近Lock;高并发场景下,Lock的性能略优于synchronized(因为AQS的等待队列机制更高效)。
  2. synchronized性能提升原因:
    • 引入偏向锁、轻量级锁,减少重量级锁的使用(避免内核态切换);
    • 引入自旋锁、自适应自旋锁,减少线程阻塞;
    • 实现锁消除、锁粗化,减少不必要的锁操作;
    • 优化Monitor的实现,提升锁的获取和释放效率。

追问2:Lock的tryLock()和lock()方法有什么区别?tryLock()的优势是什么?

  • lock():获取锁时,若锁已被持有,线程会一直阻塞,直到获取到锁,不可中断;
  • tryLock():尝试获取锁,若获取成功返回true,失败返回false,不会阻塞;可指定超时时间(tryLock(long timeout, TimeUnit unit)),超时后放弃获取锁。
  • 优势:避免线程一直阻塞,可有效防止死锁(比如超时后放弃获取锁,释放资源),适合高并发场景下的锁竞争。

追问3:ReentrantLock为什么叫可重入锁?和synchronized的可重入性有什么区别?

两者都是可重入锁(同一线程可多次获取同一把锁),区别在于实现方式:

  1. synchronized的可重入性:底层通过Monitor的计数器实现,线程第一次获取锁时计数器为1,再次获取计数器加1,释放时计数器减1,直到为0释放锁;
  2. ReentrantLock的可重入性:底层通过AQS的state变量实现,state初始为0,线程获取锁时state加1,再次获取时state继续加1,释放时state减1,直到为0释放锁。
  • 相同点:都支持可重入,避免同一线程多次获取锁导致死锁。

追问4:公平锁和非公平锁的区别?synchronized和Lock默认是什么锁?

  1. 区别:
    • 公平锁:严格按照线程等待顺序获取锁,不会出现线程饥饿,但性能较低(需要维护等待队列的顺序);
    • 非公平锁:线程释放锁后,新到来的线程可优先获取锁,无需排队,性能更高,但可能导致某些线程长期无法获取锁(线程饥饿)。
  2. 默认锁类型:
    • synchronized:默认是非公平锁,不可手动指定;
    • Lock(ReentrantLock):默认是非公平锁,可通过构造方法传入true指定为公平锁。

追问5:使用Lock时,为什么必须在finally块中释放锁?

因为Lock需要手动释放锁,若同步代码块中抛出异常,线程会直接退出,若未在finally中释放锁,锁会一直被持有,导致其他线程无法获取锁,造成死锁。

  • 示例:若未在finally中释放锁,抛出异常后锁无法释放
java 复制代码
// 错误示例(易造成死锁)
public void wrongLockMethod() {
    lock.lock();
    // 若此处抛出异常,unlock()不会执行,锁无法释放
    System.out.println("同步代码");
    lock.unlock();
}

// 正确示例(必须在finally中释放)
public void rightLockMethod() {
    lock.lock();
    try {
        System.out.println("同步代码");
    } finally {
        lock.unlock(); // 无论是否抛出异常,都会释放锁
    }
}

五、总结

synchronized和Lock都是Java并发编程的核心同步方式,核心区别和选型要点总结如下:

  1. 核心区别:底层实现(Monitor vs AQS)、锁释放(自动 vs 手动)、锁特性(灵活度不同)、性能(高并发下Lock更优);
  2. 选型原则:简单场景用synchronized(简洁、不易出错),复杂场景用Lock(灵活、高性能);
  3. 面试重点:底层实现差异、性能对比、适用场景、可重入性、锁的公平性。

掌握这些内容,无论是面试中的对比题,还是实战中的选型,都能从容应对。建议结合代码练习,加深理解,避免死记硬背,真正做到学以致用。

📌 最后提示:如果觉得这篇文章对你有帮助,点赞+收藏,关注我(予枫),后续持续更新Java并发面试干货,带你避开面试坑、吃透底层原理!评论区留下你的面试经历或项目选型经验,一起交流学习~

相关推荐
醇氧2 小时前
PowerPoint 批量转换为 PDF
java·spring boot·spring·pdf·powerpoint
java1234_小锋2 小时前
Java高频面试题:RabbitMQ如何实现消息的持久化?
java·开发语言
爱打代码的小林2 小时前
用 LangChain 解析大模型输出
java·python·langchain·大模型
_日拱一卒2 小时前
LeetCode(力扣):只出现一次的数字
java·数据结构·算法
小箌2 小时前
JavaWeb_02
java·数据库·maven·mybatis
gxy1990262 小时前
【springboot】Spring 官方抛弃了 Java 8!新idea如何创建java8项目
java·spring boot·spring
阿杰真不会敲代码2 小时前
Elasticsearch 入门到实战:安装 + CRUD + 查询
java·大数据·elasticsearch·搜索引擎
老邋遢2 小时前
干货篇|02. 纯AI Coding商业应用
java·人工智能
阴暗扭曲实习生2 小时前
135编辑器开放平台架构解析:企业级富文本接入方案的技术实现
java·开发语言·中间件