synchronized和ReentrantLock的区别是什么?他们的底层原理是什么?

synchronizedReentrantLock 是 Java 中实现线程同步的两种核心机制,它们在用法、功能和底层实现上有显著差异,以下从 区别底层原理 两方面详细说明:

一、核心区别

对比维度 synchronized ReentrantLock
锁的类型 隐式锁(JVM 自动管理锁的获取和释放) 显式锁(需手动调用 lock()unlock()
可中断性 不可中断(一旦进入阻塞,无法被其他线程中断) 可中断(支持 lockInterruptibly() 响应中断)
公平性 非公平锁(默认,无法设置为公平锁) 可指定公平 / 非公平(构造函数传入 true 为公平锁)
条件变量 不支持(仅通过 wait()/notify() 实现简单等待唤醒) 支持(通过 newCondition() 获取 Condition 对象,可实现多条件等待)
锁绑定多个条件 不支持(一个锁只能对应一个等待队列) 支持(一个锁可创建多个 Condition,对应多个等待队列)
性能 JDK 1.6 后优化(偏向锁、轻量级锁),性能接近 ReentrantLock 早期版本性能优于 synchronized,JDK 1.6 后两者接近
使用场景 简单同步场景(如单条件同步、方法 / 代码块同步) 复杂同步场景(如中断控制、公平锁、多条件等待)

二、底层原理

1. synchronized 的底层原理

synchronized 是 JVM 内置的同步机制,底层通过 对象头(Mark Word)监视器锁(Monitor) 实现,具体依赖 JVM 指令(monitorenter/monitorexit)。

  • 对象头(Mark Word) :Java 对象在内存中的布局包含 对象头 ,其中 Mark Word 存储对象的锁状态(无锁、偏向锁、轻量级锁、重量级锁)。当线程竞争 synchronized 锁时,JVM 会通过修改 Mark Word 的锁状态标识线程持有锁的情况。

  • 锁升级过程(JDK 1.6 优化) :为减少锁竞争的开销,synchronized 采用 锁升级 策略,从低开销到高开销逐步升级:

    1. 无锁状态 :对象刚创建时,Mark Word 记录哈希码等信息,无锁竞争。
    2. 偏向锁 :若只有一个线程获取锁,JVM 会在 Mark Word 中记录该线程 ID,后续该线程可直接获取锁(无需 CAS 操作),减少开销。
    3. 轻量级锁:当有其他线程竞争时,偏向锁升级为轻量级锁,线程通过 CAS 操作尝试获取锁(自旋等待,不阻塞),适合短时间竞争。
    4. 重量级锁 :若竞争激烈(自旋失败),轻量级锁升级为重量级锁,依赖操作系统的 互斥量(Mutex) 实现,线程会进入内核态阻塞(开销大),此时关联一个 Monitor(监视器) 管理锁的获取和释放。
  • Monitor 监视器 :重量级锁的核心是 Monitor,每个对象都关联一个 Monitor(通过 ObjectMonitor 实现),包含 EntryList(等待锁的线程队列)Owner(持有锁的线程)WaitSet(调用 wait() 等待的线程队列) 。线程通过 monitorenter 尝试获取 Monitor 的所有权(成功则成为 Owner,失败则进入 EntryList 阻塞),通过 monitorexit 释放所有权(唤醒 EntryList 中的线程竞争)。

2. ReentrantLock 的底层原理

ReentrantLock 是 JDK 提供的工具类(位于 java.util.concurrent.locks),底层基于 AQS(AbstractQueuedSynchronizer,抽象队列同步器) 实现,是一种 显式锁

  • AQS 核心结构 :AQS 是并发工具的基础框架,内部维护一个 volatile 修饰的 state 变量 (表示锁的状态,0 为未锁定,>0 为已锁定,支持重入时累加)和一个 双向链表(等待队列)(存储阻塞的线程)。

  • 加锁过程

    1. 线程调用 lock() 时,通过 CAS 尝试修改 state 变量:
      • state=0(未锁定),将 state 设为 1,记录当前线程为所有者,加锁成功。
      • state>0 且当前线程是所有者(重入),将 state 加 1,加锁成功。
      • 若加锁失败,当前线程被封装为 Node 节点,加入 AQS 等待队列,进入阻塞状态。
  • 解锁过程

    1. 线程调用 unlock() 时,将 state 减 1:
      • state 减为 0,释放锁,唤醒等待队列中的线程(通过 LockSupport.park/unpark 控制线程阻塞 / 唤醒)。
      • state>0(重入未完全释放),仅更新 state,不释放锁。
  • 公平锁与非公平锁

    • 非公平锁(默认):线程加锁时直接尝试 CAS 获取锁,无视等待队列中的线程,可能导致线程饥饿,但性能更高。
    • 公平锁:线程加锁时需先检查等待队列,若有线程排队则当前线程进入队列尾部,保证按顺序获取锁,但性能略低(因需维护队列顺序)。
  • 条件变量(Condition)ReentrantLock 通过 newCondition() 创建 Condition 对象,每个 Condition 对应一个 等待队列 (与 AQS 主队列分离)。调用 await() 会将线程加入 Condition 等待队列并释放锁,调用 signal() 会将线程从 Condition 队列移到 AQS 主队列参与锁竞争,实现多条件等待。

三、synchronized 示例

synchronized 是隐式锁,通过修饰方法或代码块实现同步,由 JVM 自动管理锁的获取和释放。

java 复制代码
public class SynchronizedDemo {
    // 共享资源
    private int count = 0;

    // 1. 修饰方法(锁为当前对象实例)
    public synchronized void increment() {
        count++;
    }

    // 2. 修饰代码块(锁为指定对象,这里用this)
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDemo demo = new SynchronizedDemo();
        // 多线程操作共享资源
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                demo.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                demo.decrement();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("最终结果:" + demo.getCount()); // 预期为 0(线程安全)
    }
}

特点

  • 无需手动释放锁(方法 / 代码块执行完自动释放)。
  • 不可中断(若线程阻塞在 synchronized 处,无法被其他线程中断)。

四、ReentrantLock 示例

ReentrantLock 是显式锁,需手动调用 lock() 获取锁、unlock() 释放锁(通常在 try-finally 中确保释放),支持更多高级特性。

示例 1:基本用法(非公平锁,默认)
java 复制代码
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    private int count = 0;
    // 创建锁(默认非公平锁,公平锁需传 true:new ReentrantLock(true))
    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        // 手动获取锁
        lock.lock();
        try {
            count++; // 临界区
        } finally {
            // 手动释放锁(必须放在 finally 中,避免异常导致锁未释放)
            lock.unlock();
        }
    }

    public void decrement() {
        lock.lock();
        try {
            count--;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                demo.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                demo.decrement();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("最终结果:" + demo.getCount()); // 预期为 0(线程安全)
    }
}
示例 2:高级特性(可中断、条件变量)

体现 ReentrantLock 相比 synchronized 的独特功能:

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

public class ReentrantLockAdvancedDemo {
    private final ReentrantLock lock = new ReentrantLock();
    // 创建两个条件变量(类似两个等待队列)
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();
    private int[] buffer = new int[10]; // 缓冲区
    private int size = 0; // 缓冲区元素数量

    // 生产者:向缓冲区添加元素
    public void put(int value) throws InterruptedException {
        lock.lockInterruptibly(); // 可中断的加锁(区别于 lock())
        try {
            // 若缓冲区满,等待 notFull 信号
            while (size == buffer.length) {
                notFull.await(); // 释放锁,进入 notFull 等待队列
            }
            buffer[size++] = value;
            System.out.println("生产:" + value + ",当前 size:" + size);
            notEmpty.signal(); // 唤醒等待 notEmpty 的线程(消费者)
        } finally {
            lock.unlock();
        }
    }

    // 消费者:从缓冲区取元素
    public int take() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            // 若缓冲区空,等待 notEmpty 信号
            while (size == 0) {
                notEmpty.await(); // 释放锁,进入 notEmpty 等待队列
            }
            int value = buffer[--size];
            System.out.println("消费:" + value + ",当前 size:" + size);
            notFull.signal(); // 唤醒等待 notFull 的线程(生产者)
            return value;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockAdvancedDemo demo = new ReentrantLockAdvancedDemo();

        // 启动生产者线程
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 15; i++) {
                    demo.put(i); // 生产 15 个元素(缓冲区最大 10,会阻塞等待)
                }
            } catch (InterruptedException e) {
                System.out.println("生产者被中断");
            }
        });

        // 启动消费者线程
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    demo.take(); // 消费 10 个元素
                }
            } catch (InterruptedException e) {
                System.out.println("消费者被中断");
            }
        });

        producer.start();
        consumer.start();

        // 演示可中断性:让生产者运行 1 秒后中断
        Thread.sleep(1000);
        producer.interrupt(); // 触发 producer 的 lockInterruptibly() 中断
    }
}

高级特性体现

  1. 可中断 :通过 lockInterruptibly() 允许线程在等待锁时被中断(如 producer.interrupt() 可终止生产者的等待)。
  2. 多条件变量notEmptynotFull 两个 Condition 分别管理消费者和生产者的等待队列,实现更精细的等待唤醒控制(synchronized 仅能通过 wait()/notify() 操作一个等待队列)。

总结

synchronizedReentrantLock 是 Java 保障线程安全的核心同步机制,核心差异体现在锁管理、功能灵活性与底层实现,选择需贴合场景需求:

  • 用法与功能synchronized 是 JVM 内置隐式锁,无需手动管理锁的获取与释放,用法简洁,仅支持基础同步,适合简单场景;ReentrantLock 是 JDK 提供的显式锁,需通过 lock()/unlock() 手动控制(需配合 finally 释放),支持可中断、公平 / 非公平锁、多条件变量等高级特性,适配复杂并发场景。
  • 底层实现synchronized 依赖对象头(Mark Word)和 Monitor 监视器,通过 "无锁→偏向锁→轻量级锁→重量级锁" 的升级策略优化性能;ReentrantLock 基于 AQS(抽象队列同步器),通过 state 变量记录锁状态,用双向链表管理等待线程,支持灵活的锁机制。
  • 性能与选择 :JDK 1.6 后 synchronized 经优化,性能与 ReentrantLock 接近;无特殊需求时优先选 synchronized(低风险、低成本),需高级功能时选用 ReentrantLock(灵活可控),两者最终均通过控制临界区访问保障线程安全。
相关推荐
gc_229940 分钟前
学习C#调用Microsoft.Office.Interop.Word将Word转换为html
c#·html·word·interop.word
ChineHe42 分钟前
Golang并发编程篇002_Go并发基础
开发语言·后端·golang
默恋~微凉43 分钟前
shell(八)——WEB与Nginx
开发语言·前端·php
lsx2024061 小时前
Go 语言类型转换
开发语言
t***L2662 小时前
JavaScript在机器学习中的库
开发语言·javascript·机器学习
唐青枫2 小时前
C#.NET 集合表达式详解:新时代的集合初始化方式
c#·.net
勇闯逆流河3 小时前
【C++】C++11(下)
开发语言·c++
青衫码上行3 小时前
【Java Web学习 | 第15篇】jQuery(万字长文警告)
java·开发语言·前端·学习·jquery
hez20108 小时前
TypedSql:在 C# 类型系统上实现一个 SQL 查询引擎
c#·.net·.net core·compiler