切换语言为:繁体

Java 的 synchronized 和 ReentrantLock 源码层面对比

  • 爱糖宝
  • 2024-08-28
  • 2060
  • 0
  • 0

synchronizedReentrantLock是Java中用于线程同步的两种机制。它们之间有一些相似之处,但也存在许多区别。以下是它们的对比:

共同点

  • 目的:二者都是用于实现线程间的互斥与同步,以确保共享资源的安全访问。

  • 可重入性:两者都是可重入锁,这意味着一个持有锁的线程可以再次获取该锁而不会发生死锁。

区别

  1. 实现方式

    • 是Java.util.concurrent包中的一个类,由Java代码实现。

    • 使用AQS实现线程同步,以双向队列维护等待线程。

    • 通过CAS操作+volatile变量来实现线程安全的锁状态改变。

    • 锁的获取逻辑在lock()tryAcquire(int arg)中实现,释放逻辑在unlock()release(int arg)中实现。

    • 是Java语言层面的关键字,由JVM实现(每个对象在Java中都有一个隐式的监视器锁 (monitor lock),synchronized就是基于这个锁实现的)。

    • 基于对象头中的Mark Word记录锁状态,并且利用CAS操作实现锁的竞争和释放。

    • JVM通过偏向锁、轻量级锁、自旋锁和重量级锁等机制优化性能。

    • 编译时固化成字节码,JDK层面无法直接看到其实现。

    • synchronized

    • ReentrantLock

  2. 使用方式

    • 需要显式调用lock()方法获取锁,unlock()方法释放锁。

    • 使用起来稍微复杂,需要在finally块中确保锁的释放以避免死锁。

    • 简单易用,直接通过在方法或者代码块上加 synchronized关键字实现。

    • 自动释放锁,无需显式调用解锁方法。

    • synchronized

    • ReentrantLock

  3. 功能特性

    • synchronized每个锁只有一个隐式的条件队列,可以通过wait()notify()notifyAll()来操作。

    • ReentrantLock支持多个条件变量,通过newCondition()可以创建多个Condition对象,以便更加灵活地控制线程的等待和唤醒。

    • synchronized不支持超时功能。

    • ReentrantLock支持尝试在指定时间内获取锁,使用tryLock(long timeout, TimeUnit unit)方法。

    • synchronized不支持锁获取的中断。

    • ReentrantLock支持可中断的锁获取,通过lockInterruptibly()实现。

    • 可中断锁获取:

    • 超时获取锁:

    • 多条件变量:

  4. 公平锁

    • synchronized没有内置的公平性控制。

    • ReentrantLock可以选择使用公平锁或非公平锁,通过构造函数ReentrantLock(boolean fair)来指定。公平锁会按照请求的顺序分配锁,而非公平锁则可能使某些线程长时间等待。

  5. 性能

    • 在低竞争的情况下,synchronized经过JVM的一系列优化,性能通常较好。

    • ReentrantLock由于其丰富的功能,在高竞争和复杂场景下可能更适用。

选择建议

  • 如果需求简单,只是需要基本的锁机制,并且不需要中断、超时等高级功能,synchronized是一个不错的选择,因为它更简单且由JVM优化。

  • 如果需要锁的中断、超时机制,以及多个条件变量等高级特性,ReentrantLock提供了更大的灵活性和控制力。

synchronized源码分析

synchronized关键字是Java中的一种内置锁机制,用于实现线程同步。虽然在Java代码中使用synchronized非常简单,但其底层实现涉及JVM和操作系统的交互,因此需要从Java字节码和JVM层面进行分析。

基本概念

  • synchronized可以用来修饰方法或者代码块。

  • 它的主要作用是确保在同一时刻只有一个线程可以执行被保护的代码,从而防止线程间的竞争条件。

同步代码块

当在Java代码中使用synchronized语句块时,编译器会将其转化为字节码指令。这些字节码包括:

  • monitorenter:表示进入同步块,获取锁。

  • monitorexit:表示退出同步块,释放锁。这通常会有两个monitorexit指令,一个在正常退出时,一个在异常退出时,以确保锁的释放。

public void synchronizedBlockExample() {
    synchronized(this) {
        // 代码块
    }
}

编译后生成的字节码类似于:

0: aload_0       // 加载"this"到栈顶
1: dup
2: monitorenter  // 获取锁
3: ...           // 被同步的代码块
4: monitorexit   // 释放锁
5: goto 13
6: astore_1
7: aload_0
8: monitorexit   // 异常时释放锁
9: aload_1
10: athrow
11: ...
12: ...
13: ...

同步方法

对于synchronized方法,JVM通过方法标志位来实现同步,而不需要显式的monitorentermonitorexit指令。方法级的synchronized是由JVM进入和退出方法时隐式处理的。

public synchronized void synchronizedMethodExample() {
    // 方法体
}

在字节码中,没有直接的monitorentermonitorexit指令,而是通过方法访问标志(access flags)来表明这个方法是同步的。

锁的实现细节

synchronized的锁机制依赖于对象头中的Mark Word来存储锁状态。根据锁的不同状态,Mark Word可能包含以下信息:

  • 无锁:用于存储对象的哈希码、分代年龄等。

  • 偏向锁:存储线程ID,表示锁被某个线程持有但没有发生竞争。

  • 轻量级锁:存储指向栈帧的指针,用于快速获取锁。

  • 重量级锁:使用操作系统的Mutex来管理锁。

锁优化

为了提高性能,JVM进行了多种锁优化:

  • 偏向锁:如果多次看到相同的线程请求锁,则认为该线程会再次请求,从而减少不必要的同步开销。

  • 轻量级锁:在无竞争情况下,通过CAS操作避免使用重量级锁。

  • 自旋锁:在短时间锁争用情况下,允许线程自旋等待而不是阻塞,以减少线程切换的开销。

ReentrantLock源码分析

ReentrantLock是Java并发包(java.util.concurrent.locks)中的一个锁实现。它提供了比synchronized关键字更灵活和强大的功能,比如可中断的锁获取、超时获取锁以及多个条件变量等。下面是对ReentrantLock核心源码的一些分析。

核心组件

ReentrantLock的核心是基于AbstractQueuedSynchronizer(AQS)来实现同步控制。AQS是一个用于构建锁和其他同步组件的框架。

AQS概述

  • 状态字段:使用一个int类型的state字段来表示锁的状态。

  • FIFO队列:使用一个FIFO等待队列来管理处于等待状态的线程。

  • 模板方法:通过继承AQS,实现特定资源的获取和释放逻辑。

ReentrantLock的实现细节

基本结构

ReentrantLock有两种模式:公平模式和非公平模式,分别由两个内部静态类FairSyncNonfairSync实现,这两个类都继承自AQS:

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
        // 实现具体的锁机制
    }

    static final class NonfairSync extends Sync {
        // 非公平锁实现
    }

    static final class FairSync extends Sync {
        // 公平锁实现
    }

    public ReentrantLock() {
        sync = new NonfairSync(); // 默认非公平锁
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

锁的获取

  • 非公平锁(NonfairSync)

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

    在非公平锁中,尝试获取锁时,如果没有线程持有锁,则直接通过CAS设置状态为已获取;如果当前线程已经持有锁,则增加重入次数。

  • 公平锁(FairSync)

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

    在公平锁中,除了检查锁状态外,还会检查是否有其他线程在排队等待获取锁,这样可以保证先请求锁的线程优先获得锁。

锁的释放

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

锁的释放逻辑主要是减少持有计数,并在计数为0时释放锁(清空持有线程)。

条件变量

ReentrantLock支持条件变量,通过内部的ConditionObject实现:

public Condition newCondition() {
    return sync.newCondition();
}

final class ConditionObject implements Condition, java.io.Serializable {
    // 实现条件变量的等待和通知机制
}

条件变量允许线程在特定条件下等待并被唤醒,这是synchronized无法直接实现的多条件等待模型。

非公平锁抢占机制

ReentrantLock的非公平锁场景下,当多个线程在等待获取同一把锁时,具体哪个线程最终能够拿到锁并不是严格确定的,因为非公平锁机制允许一定程度的“插队”。下面是详细的流程分析:

基本概念

  • 非公平性:非公平锁不保证按照请求的顺序分配锁,新加入的线程会直接尝试获取锁而不考虑等待队列。

详细流程

  1. 初始状态

    • 假设有若干个线程T1T2, ..., Tn在争夺同一个ReentrantLock

    • 当锁被某个线程持有时,例如T0,其他线程会进入AQS(AbstractQueuedSynchronizer)的等待队列。

  2. 线程尝试获取锁

    • 新加入的线程(例如Tnew)调用lock()方法。

    • 在非公平锁的模式下,线程首先通过CAS操作尝试立即获取锁,不管等待队列中是否有其他线程。

  3. CAS操作

    • 成功:如果compareAndSetState(0, 1)成功,即锁当前是空闲的,那么Tnew获得锁,成为持有锁的线程。

    • 失败:如果CAS操作失败,这意味着另一线程持有锁或在同一时刻成功获取了锁。

  4. 进入等待队列

    • 如果CAS失败,Tnew和其他未能获取锁的线程将进入AQS的等待队列。

    • AQS使用一个FIFO队列来管理这些线程。

  5. 队列中的竞争

    • 即使在等待队列中,非公平锁仍然允许新来的线程尝试插队获取锁。如果锁在这个过程中被释放,新线程可能直接通过CAS操作成功获取锁。

    • 由于CAS操作是原子的,并且多个线程会同时争夺锁,最终成功的线程(无论是否在队列中)将是最先完成CAS设置的那个线程。

  6. 锁的释放与重新竞争

    • 当前持有锁的线程释放锁时,会调用unlock(),这将设置状态为0,并唤醒等待队列中的头节点。

    • 被唤醒的线程将从等待队列中出队,并尝试获取锁。然而,由于非公平特性,任何其他线程也可在此时尝试获取锁。

  7. 结果不确定性

    • 虽然通常唤醒的线程有更高的机会拿到锁,但并不绝对。任何处于运行态的新线程或已经唤醒的线程都有机会在锁释放后立即尝试获取锁。

总结

在非公平锁的环境中,哪个线程最终获得锁是由多个因素决定的,包括线程调度、CPU时间片以及CAS操作的竞争结果。由于新的线程可以在任何时候尝试获取锁,所以存在较大的不确定性。这种策略提高了并发性能,但可能导致某些线程长时间得不到锁,这就是所谓的“锁饥饿”现象。

可重入性实现原理

synchronized的可重入性

synchronized 是Java内置关键字,用于简化锁管理,其可重入性由JVM层面直接支持。

  1. 监视器锁

    • 每个对象在Java中都有一个隐式的监视器锁 (monitor lock),synchronized就是基于这个锁实现的。

    • 当一个线程访问被synchronized修饰的方法或代码块时,它会尝试获取该对象的监视器锁。

  2. 锁计数器与持有线程

    • JVM在内部通过关联一个锁计数器(lock count)和持有线程(owner)来管理可重入性。

    • 当一个线程第一次获得锁时,计数器加一,并记录该线程为持有者。

    • 如果同一个线程再次进入同步代码块/方法,计数器会继续增加,而不会阻塞,因为持有者是自己。

  3. 锁释放

    • 每次退出同步代码块/方法时,计数器减一。

    • 当计数器为零时,监视器锁才真正释放,允许其他线程获取。

ReentrantLock的可重入性

ReentrantLock是Java提供的明文锁,位于java.util.concurrent.locks包中,由AbstractQueuedSynchronizer (AQS) 支持其复杂功能。

  1. AQS框架

    • ReentrantLock利用AQS中的状态(state)字段来实现锁的计数,与持有线程信息一起管理可重入性。

  2. 锁获取与持有线程

    • 当线程第一次获取锁时,AQS的state字段从0变为1,并记录当前线程为持有线程。

    • 如果持有该锁的线程再次请求锁,state字段递增,而不需要重新获取锁。

  3. 锁释放

    • 线程每释放一次锁,state字段递减。

    • state降至0时,持有线程信息清空,锁完全释放,可以被其他线程获取。

  4. 显式控制

    • synchronized不同,ReentrantLock提供了显式的锁控制方法,如lock()unlock()tryLock()等,使得程序员可以更灵活地管理锁。

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.