Java并发基石ReentrantLock:深入解读其原理与实现

简介: Java并发基石ReentrantLock:深入解读其原理与实现

一、ReentrantLock概述

ReentrantLock,也被称为“可重入锁”,是一个同步工具类,在java.util.concurrent.locks包下。这种锁的一个重要特点是,它允许一个线程多次获取同一个锁而不会产生死锁。这与synchronized关键字提供的锁定机制非常相似,但ReentrantLock提供了更高的扩展性。

二、ReentrantLock的核心特性

可重入性:ReentrantLock的一个主要特点是它的名字所表示的含义——“可重入”。简单来说,如果一个线程已经持有了某个锁,那么它可以再次调用lock()方法而不会被阻塞。这在某些需要递归锁定的场景中非常有用。锁的持有计数会在每次成功调用lock()方法时递增,并在每次unlock()方法被调用时递减。

公平性:与内置的synchronized关键字不同,ReentrantLock提供了一个公平锁的选项。公平锁会按照线程请求锁的顺序来分配锁,而不是像非公平锁那样允许线程抢占已经等待的线程的锁。公平锁可以减少“饥饿”的情况,但也可能降低一些性能。

可中断性:ReentrantLock的获取锁操作(lockInterruptibly()方法)可以被中断。这提供了另一个相对于synchronized关键字的优势,因为synchronized不支持响应中断。

条件变量:ReentrantLock类中还包含一个Condition接口的实现,该接口允许线程在某些条件下等待或唤醒。这提供了一种比使用wait()和notify()更灵活和更安全的线程通信方式。

三、ReentrantLock与synchronized

ReentrantLock与synchronized都是Java中用于多线程同步的机制,但它们在使用方式、功能和灵活性上有一些不同。

3.1 相同点

  1. 互斥性:ReentrantLock和synchronized都保证了一个时间点只有一个线程可以执行某个代码块,即它们都是互斥锁。
  2. 可重入性:两者都支持可重入性,意味着同一个线程可以多次获取同一个锁,不会产生死锁。

3.2 不同点

  1. 来源:synchronized是Java语言内建的关键字,而ReentrantLock是Java并发库java.util.concurrent.locks包中的一个类。
  2. 等待可中断性:ReentrantLock提供了一种能够响应中断的获取锁的方式(lockInterruptibly()),而synchronized是不可中断的,一旦线程没有获取到锁,就会进入阻塞状态,直到获取锁。
  3. 锁释放:ReentrantLock必须由手动释放锁(unlock()),所以使用起来需要特别小心,避免忘记释放锁导致死锁;而synchronized则是由JVM自动释放锁,当线程执行完同步代码块或方法后,JVM会自动释放线程持有的锁。
  4. 锁的申请:ReentrantLock提供了tryLock()方法,可以尝试获取锁,如果获取不到就返回false,不会一直等待;而synchronized没有这种机制,一旦获取不到锁,就会一直等待。
  5. 公平锁与非公平锁:ReentrantLock可以在构造函数中指定是公平锁还是非公平锁,而synchronized是非公平的,不保证等待时间最长的线程先获得锁。
  6. 绑定条件Condition:ReentrantLock可以与多个Condition对象绑定,以实现更细粒度的锁控制和线程间的协作;而synchronized没有这个功能,只能与整个对象绑定。

总的来说,ReentrantLock提供了比synchronized更灵活、更强大的锁机制,但使用起来也更复杂,需要更谨慎地处理锁的获取和释放。synchronized虽然功能相对简单,但在很多情况下已经足够使用,并且由于是内建关键字,使用起来也更方便。

四、ReentrantLock的使

下面代码模拟了一个账户转账的场景,展示了ReentrantLock如何保证多线程下的数据安全性。

import java.util.concurrent.locks.ReentrantLock;

public class Account {
    // 账户余额
    private int balance;
    // 锁对象
    private final ReentrantLock lock = new ReentrantLock();

    public Account(int balance) {
        this.balance = balance;
    }

    // 存钱
    public void deposit(int amount) {
        lock.lock();  // 获取锁
        try {
            balance += amount;
            System.out.println("存入金额: " + amount + ",当前余额: " + balance);
        } finally {
            lock.unlock();  // 释放锁
        }
    }

    // 取钱
    public void withdraw(int amount) {
        lock.lock();  // 获取锁
        try {
            if (balance >= amount) {
                balance -= amount;
                System.out.println("取出金额: " + amount + ",当前余额: " + balance);
            } else {
                System.out.println("余额不足,取款失败!");
            }
        } finally {
            lock.unlock();  // 释放锁
        }
    }

    // 获取账户余额
    public int getBalance() {
        return balance;
    }

    // 主函数,模拟多线程下的转账操作
    public static void main(String[] args) {
        final Account account = new Account(1000);

        // 启动一个线程进行存钱操作
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                account.deposit(100);
                try {
                    Thread.sleep(100);  // 模拟延时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 启动一个线程进行取钱操作
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                account.withdraw(50);
                try {
                    Thread.sleep(100);  // 模拟延时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

Account类表示一个账户,包含了存钱(deposit)和取钱(withdraw)两个操作。为了保证账户余额在多线程环境下的数据安全性,我们在这两个方法中使用了ReentrantLock来确保同时只有一个线程能够修改账户余额。


deposit方法中,先获取锁,然后进行余额的增加操作,最后释放锁。同样地,在withdraw方法中也是先获取锁,然后判断余额是否足够,足够则进行扣款操作,否则输出提示信息,最后释放锁。


在main方法中,创建了一个账户对象,并启动了两个线程,一个进行多次存钱操作,另一个进行多次取钱操作。由于我们使用了ReentrantLock来保证同步,因此即使在多线程环境下,账户的余额也不会出现不一致的情况。

五、ReentrantLock的实现原理

5.1 实现原理概述

ReentrantLock的实现依赖于内部的Sync类,这个类是AbstractQueuedSynchronizer(AQS)的一个实现。AQS是Java并发库中许多同步工具(包括Semaphore、CountDownLatch和CyclicBarrier等)的核心。

AQS使用一个int类型的变量来表示同步状态,ReentrantLock用它来表示锁的持有计数和持有线程的信息。当计数为0时,表示锁未被任何线程持有。当一个线程首次成功获取锁时,JVM会记录这个锁的持有线程,并将计数器设置为1。如果同一个线程再次请求这个锁,它将能够再次获得这个锁,并且计数器会递增。当线程释放锁时(通过调用unlock()方法),计数器会递减。如果计数器递减为0,则表示锁已经完全释放,其他等待的线程有机会获取它。

此外,AQS还维护了一个队列,用于管理那些等待锁的线程。这个队列遵循FIFO原则,但也可以通过设置为公平锁来严格按照线程请求锁的顺序来排队。

5.2 源码原理分析

首先,ReentrantLock的核心实现是基于AbstractQueuedSynchronizer(AQS),它是一个用于构建锁和同步器的框架。ReentrantLock内部有一个静态内部类Sync,它继承了AQS并实现了所需的同步状态管理。

public class ReentrantLock implements Lock, java.io.Serializable {
    // 默认使用非公平锁
    private final Sync nonfairSync;
    // 公平锁
    private final Sync fairSync;
    // 抽象队列同步器,实际是nonfairSync或fairSync
    private final Sync sync;

    // 构造函数,默认非公平锁
    public ReentrantLock() {
        sync = nonfairSync = new NonfairSync();
    }

    // 构造函数,可指定公平性
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

    // 实现Lock接口的lock方法
    public void lock() {
        sync.lock();
    }

    // ... 其他方法,如tryLock, unlock等

    // 抽象队列同步器的实现
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // ...

        // 是否处于占用状态
        final boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 尝试获取锁
        final boolean tryAcquire(int acquires) {
            // ... 省略具体实现
        }

        // 释放锁
        protected final boolean tryRelease(int releases) {
            // ... 省略具体实现
        }

        // ... 其他方法
    }

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

        // 锁获取
        final void lock() {
            // ... 省略具体实现
        }

        // ... 其他方法
    }

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

        // 锁获取,考虑公平性
        final void lock() {
            // ... 省略具体实现
        }

        // ... 其他方法
    }
}

上面的代码只是ReentrantLock的一个简版框架,通过这个框架,我们可以理解ReentrantLock的基本结构和关键组成部分:


同步状态管理:ReentrantLock使用AQS的同步状态来管理锁的持有情况。当状态为0时,表示锁未被任何线程持有;当状态为1时,表示锁被某个线程持有。对于可重入锁,每次重入都会增加状态值,每次释放都会减少状态值。但这里简化的表示只用状态值1来表示锁被持有,实际实现中会有更复杂的状态管理。


锁的获取:在NonfairSync和FairSync中,lock()方法实现了锁的获取逻辑。非公平锁在尝试获取锁时不会考虑队列中的等待线程,而公平锁则会严格按照FIFO原则来处理等待线程。这些方法最终会调用AQS的acquire()方法,该方法会处理同步状态的变更、线程的阻塞和唤醒等。


锁的释放:锁的释放通过unlock()方法实现,最终会调用AQS的release()方法。这个方法会负责减少同步状态、唤醒等待线程等。在释放锁之前,必须确保当前线程是锁的持有者。


条件变量:ReentrantLock还提供了newCondition()方法,用于创建条件变量。这些条件变量可以用于实现更复杂的线程同步和通信逻辑。条件变量的实现也是基于AQS的。


ReentrantLock的实现主要依赖于AQS框架,通过扩展AQS并实现特定的同步状态管理逻辑来实现可重入锁的功能。它提供了比synchronized关键字更灵活和可定制的同步机制,包括公平性选择、可中断的锁获取操作以及条件变量等。在使用ReentrantLock时,需要注意正确管理锁的获取和释放,以避免死锁和性能问题。

六、使用ReentrantLock的注意事项

  1. 始终在finally块中释放锁:为了确保锁能够在所有情况下都被正确释放(包括在可能抛出异常的代码中),你应该总是在finally块中调用unlock()方法。
  2. 避免锁泄露:锁泄露是指由于某些原因(如忘记释放锁或持有锁的线程意外死亡),导致锁无法被其他线程获取。这可能导致应用程序挂起或无法正常工作。使用try-finally语句可以帮助避免这种情况。
  3. 小心使用条件变量:虽然Condition接口提供了一种灵活的线程通信方式,但如果不当使用,也可能导致死锁或活锁等问题。你应该确保在使用条件变量时始终遵循正确的模式(如在调用await()方法之前检查条件,并在修改条件之后调用signal()signalAll()方法)。
  4. 公平性考虑:根据你的应用场景选择合适的锁公平性策略。虽然公平锁可以减少“饥饿”现象并提高可预测性,但它们也可能降低性能。另一方面,非公平锁可能会提供更好的性能,但在高竞争场景下可能导致线程“饥饿”。
  5. 性能考虑:与synchronized关键字相比,ReentrantLock在某些情况下可能提供更好的性能。但是,这也意味着你需要更小心地管理锁的获取和释放,以及处理可能出现的竞争和死锁问题。此外,过度使用锁(无论是synchronized还是ReentrantLock)都可能导致性能下降和可伸缩性问题。因此,在设计并发程序时,应该尽量使用无锁或低锁竞争的数据结构和算法。

结语

ReentrantLock 是 Java 提供的一种可重入的互斥锁,它具有与 synchronized 关键字类似的同步和锁定能力,但比 synchronized 更灵活。ReentrantLock支持中断获取锁、尝试获取锁(限时/非限时)和可轮询的获取锁等特性,适用于需要更高级锁定控制的场景。

相关文章
|
13天前
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
26 3
|
13天前
|
安全 算法 Java
Java CAS原理和应用场景大揭秘:你掌握了吗?
CAS(Compare and Swap)是一种乐观锁机制,通过硬件指令实现原子操作,确保多线程环境下对共享变量的安全访问。它避免了传统互斥锁的性能开销和线程阻塞问题。CAS操作包含三个步骤:获取期望值、比较当前值与期望值是否相等、若相等则更新为新值。CAS广泛应用于高并发场景,如数据库事务、分布式锁、无锁数据结构等,但需注意ABA问题。Java中常用`java.util.concurrent.atomic`包下的类支持CAS操作。
44 2
|
2月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
2月前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
2月前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
2月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
Java C++
详解JAVA中的 i++ 和 ++i ,案例及原理,通俗易懂
i++和++i是日常开发中,经常使用的语句形式,也是面试中经常见到的一个知识点。但是你真的理解其中的原理吗?
951 0
|
10天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
12天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
12天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。