您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

到底什么情况下该用ReentrantLock?

时间:2023-05-06 14:29:02  来源:今日头条  作者:你的老师父



在多线程编程中,锁(Lock)是一种重要的同步机制,它可以保证同一时间只有一个线程可以访问共享资源。JAVA 中提供了两种类型的锁:隐式锁和显式锁。

 

隐式锁通过 synchronized 关键字实现,在使用时比较方便,但其粒度较大,无法满足复杂的同步需求。而显式锁则通过 Lock 接口实现,可以更灵活地控制锁的粒度和行为。本文将介绍 Java 显式锁中的显示锁(ReentrantLock)和显示条件队列(Condition),并讨论它们的使用方法、进阶用法以及可能遇到的问题和解决方案。

一、显示锁

1. 简介

显示锁(ReentrantLock)是 Java 显式锁中最常用的一种,它实现了 Lock 接口的所有特性,并提供了可重入和公平性等额外功能。其中,可重入指同一线程可以多次获取该锁而不会造成死锁,公平性指多个线程按照申请锁的顺序获得锁。

与隐式锁不同的是,显示锁需要手动加锁和释放锁,通常使用 try-finally 语句块保证锁的正确释放,避免异常导致锁未能被及时释放而造成死锁。

2. 基本使用

显示锁(ReentrantLock)的基本用法如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyRunnable implements Runnable {
    private final Lock lock = new ReentrantLock();
    private int count = 0;

    public void run() {
        lock.lock(); // 加锁
        try {
            count++; // 访问共享资源
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

在上述示例中,我们首先创建了一个 ReentrantLock 对象,并将其作为同步对象(Monitor)来访问共享资源。然后,在访问共享资源时使用 lock.lock() 方法加锁,使用 lock.unlock() 方法解锁。由于 lock 和 unlock 方法都可能抛出异常,因此通常需要使用 try-finally 语句块来确保锁的正确释放。

3. 可重入性

在 Java 中,可重入性指同一线程可以多次获得该锁而不会产生死锁或排斥自己的情况。这是由于每个线程在加锁时会记录加锁的次数,只有在解锁和加锁次数相等时才真正释放锁。例如:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyRunnable implements Runnable {
    private final Lock lock = new ReentrantLock();
    private int count = 0;

    public void run() {
        lock.lock(); // 第一次加锁
        try {
            count++; // 访问共享资源
            lock.lock(); // 第二次加锁
            try {
                count++; // 访问共享资源
            } finally {
                lock.unlock(); // 第二次解锁
            }
        } finally {
            lock.unlock(); // 第一次解锁
        }
    }
}

在上述示例中,我们先后两次获取了同一个锁,并在其中访问了共享资源。由于锁是可重入的,因此即使在第二次加锁时仍然持有锁,也不会产生死锁或排斥自己的情况。

4. 公平性

在 Java 中,公平性指多个线程按照申请锁的顺序获得锁的特性。公平性可以避免某些线程长期持有锁,导致其他线程无法获得锁而等待过长时间的情况。

 

在显示锁中,默认情况下是非公平的,即当前线程可以随时获得锁,而不考虑其他线程的申请顺序。这样可能会导致某些线程一直无法获得锁,从而产生线程饥饿(Thread Starvation)的问题。

为了解决这个问题,Java 中提供了公平锁(FAIrLock),它会按照线程申请锁的顺序进行排队,并且保证先来先得的原则。示例代码如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyRunnable implements Runnable {
    private final Lock lock = new ReentrantLock(true); // 公平锁
    private int count = 0;

    public void run() {
        lock.lock(); // 加锁
        try {
            count++; // 访问共享资源
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

在上述示例中,我们创建了一个公平锁(FairLock),并将其传递给 ReentrantLock 的构造函数中。然后,在访问共享资源时使用 lock.lock() 方法加锁,使用 lock.unlock() 方法解锁。由于公平锁会按照线程申请锁的顺序进行排队,因此可以避免线程饥饿的问题。

二、显示条件队列

1. 简介

条件队列(Condition)是 Java 显式锁中实现线程等待/通知机制的一种方式。它允许多个线程在某些条件不满足时暂停执行,并在特定条件满足时恢复执行。与 synchronized 关键字相比,条件队列提供了更灵活和细粒度的同步控制,可以更好地支持复杂的同步需求。

条件队列通常与显示锁一起使用,通过
ReentrantLock.newCondition() 方法创建一个 Condition 对象,并使用 await()、signal() 和 signalAll() 等方法来进行线程等待和唤醒操作。其中,await() 方法用于使当前线程等待某个条件发生变化,signal() 方法用于唤醒一个等待该条件的线程,signalAll() 方法用于唤醒所有等待该条件的线程。

2. 基本使用

显示条件队列(Condition)的基本用法如下:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyRunnable implements Runnable {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean flag = false;

    public void run() {
        lock.lock(); // 加锁
        try {
            while (!flag) {
                condition.await(); // 等待条件变化
            }
            // 访问共享资源
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock(); // 解锁
        }
    }

    public void changeFlag() {
        lock.lock(); // 加锁
        try {
            flag = true; // 修改条件
            condition.signalAll(); // 唤醒等待的线程
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

在上述示例中,我们首先创建了一个 Condition 对象,并将其关联到一个显示锁(ReentrantLock)上。然后,在访问共享资源时使用 while 循环判断条件是否满足,如果不满足则调用 condition.await() 方法使当前线程进入等待状态。在修改条件时调用 changeFlag() 方法,并使用 condition.signalAll() 唤醒所有等待该条件的线程。需要注意的是,await() 方法和 signal()/signalAll() 方法都必须在锁保护下进行调用,否则会抛出
IllegalMonitorStateException 异常。

 

3. 进阶使用

条件队列(Condition)还提供了许多高级操作,用于支持更复杂的同步需求。以下是一些常用的进阶使用方式:

(1)等待超时

有时候我们希望线程在等待一段时间后自动唤醒,而不是一直等待到被唤醒为止。这时候可以使用 condition.await(long time, TimeUnit unit) 方法,它允许我们指定等待的最长时间,如果超过指定时间仍未被唤醒,则自动退出等待状态。示例代码如下:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class MyRunnable implements Runnable {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean flag = false;

    public void run() {
        lock.lock(); // 加锁
        try {
            long timeout = 10L; // 等待 10 秒
            while (!flag) {
                if (!condition.await(timeout, TimeUnit.SECONDS)) {
                    // 在等待一定时间后还未被唤醒,做相应处理
                    break;
                }
            }
            // 访问共享资源
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock(); // 解锁
        }
    }

    public void changeFlag() {
        lock.lock(); // 加锁
        try {
            flag = true; // 修改条件
            condition.signalAll(); // 唤醒等待的线程
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

在上述示例中,我们使用 condition.await(timeout, TimeUnit.SECONDS) 方法等待了 10 秒,如果超过该时间还未被唤醒,则退出等待状态并做相应处理。

(2)等待多个条件

有时候我们需要等待多个条件同时满足后才能继续执行,这时候可以使用多个条件队列(Condition)来实现。示例代码如下:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyRunnable implements Runnable {
    private final Lock lock = new ReentrantLock();
    private final Condition condition1 = lock.newCondition();
    private final Condition condition2 = lock.newCondition();
    private boolean flag1 = false;
    private boolean flag2 = false;

    public void run() {
        lock.lock(); // 加锁
        try {
            while (!flag1 || !flag2) {
                if (!flag1) {
                    condition1.await(); // 等待条件 1
                }
                if (!flag2) {
                    condition2.await(); // 等待条件 2
                }
            }
            // 访问共享资源
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock(); // 解锁
        }
    }

    public void changeFlag1() {
        lock.lock(); // 加锁
        try {
            flag1 = true; // 修改条件 1
            condition1.signalAll(); // 唤醒等待条件 1 的线程
        } finally {
            lock.unlock(); // 解锁
        }
    }

    public void changeFlag2() {
        lock.lock(); // 加锁
        try {
            flag2 = true; // 修改条件 2
            condition2.signalAll(); // 唤醒等待条件 2 的线程
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

在上述示例中,我们创建了两个条件队列(Condition),分别用于等待两个不同的条件。然后,在访问共享资源时使用 while 循环判断两个条件是否都满足,如果不满足则分别调用 condition1.await() 和 condition2.await() 方法使当前线程进入等待状态。在修改条件时分别调用 changeFlag1() 和 changeFlag2() 方法,并使用 condition1.signalAll() 和 condition2.signalAll() 唤醒等待相应条件的线程。

 

(3)实现生产者消费者模型

条件队列(Condition)还可以用于实现生产者消费者模型,其中生产者和消费者共享一个缓冲区,当缓冲区为空时,消费者需要等待生产者生产数据;当缓冲区满时,生产者需要等待消费者消费数据。示例代码如下:

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 MyRunnable implements Runnable {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final Queue<Integer> queue = new LinkedList<>();
    private final int maxSize = 10;

    public void run() {
        while (true) {
            lock.lock(); // 加锁
            try {
                while (queue.isEmpty()) {
                    notEmpty.await(); // 等待不为空
                }
                int data = queue.poll(); // 取出数据
                notFull.signalAll(); // 唤醒生产者
                // 处理数据
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock(); // 解锁
            }
        }
    }

    public void produce(int data) {
        lock.lock(); // 加锁
        try {
            while (queue.size() == maxSize) {
                notFull.await(); // 等待不满
            }
            queue.offer(data); // 添加数据
            notEmpty.signalAll(); // 唤醒消费者
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

在上述示例中,我们创建了一个缓冲区(Queue),并使用两个条件队列(Condition)分别表示缓冲区不为空和不满。在消费者线程中,使用 while 循环判断缓冲区是否为空,如果为空则调用 notEmpty.await() 方法使当前线程进入等待状态。当从缓冲区取出数据后,调用 notFull.signalAll() 方法唤醒所有等待不满的生产者线程。在生产者线程中,使用 while 循环判断缓冲区是否已满,如果已满则调用 notFull.await() 方法使当前线程进入等待状态。当往缓冲区添加数据后,调用 notEmpty.signalAll() 方法唤醒所有等待不为空的消费者线程。

三、读写锁

1. 简介

读写锁是一种特殊的锁,它允许多个线程同时读取共享资源,但只允许一个线程对共享资源进行写操作。读写锁可以有效地提高并发性能,特别是在读取操作远多于写操作的场景下。

Java 中提供了 ReentrantReadWriteLock 类来实现读写锁。它包含一个读锁和一个写锁,读锁可同时被多个线程持有,但写锁一次只能被一个线程持有。示例代码如下:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MyRunnable implements Runnable {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private int count = 0;

    public void run() {
        lock.readLock().lock(); // 获取读锁
        try {
            // 访问共享资源(读取)
        } finally {
            lock.readLock().unlock(); // 释放读锁
        }
    }

    public void write() {
        lock.writeLock().lock(); // 获取写锁
        try {
        		// 访问共享资源(写入)
        } finally {
       		 lock.writeLock().unlock(); // 释放写锁
        }
    }
}

在上述示例中,我们创建了一个读写锁(ReentrantReadWriteLock),并使用 readLock() 方法获取读锁,writeLock() 方法获取写锁。在访问共享资源时,读取操作可以同时被多个线程持有读锁,而写入操作必须先获取写锁,然后其他所有操作都被阻塞,直到写入完成并释放写锁。

2. 使用场景

读写锁适用于以下场景:

  • 读取操作远多于写入操作。
  • 共享资源的状态不会发生太大变化,即读取操作和写入操作之间的时间间隔较长。
  • 写入操作对资源的一致性要求高,需要独占式访问。

使用读写锁可以有效地提高程序的并发性能,特别是在读取操作远多于写入操作的情况下。但需要注意的是,读写锁的实现需要消耗更多的系统资源,因此只有在读取操作远多于写入操作、且读写操作之间的时间间隔较长时才应该使用读写锁。

四、StampedLock

1. 简介

StampedLock 是 Java 8 新增的一种锁机制,它是对读写锁的一种改进,具有更高的并发性能。StampedLock 支持三种模式:读(共享)、写(独占)和乐观读(非独占)。与 ReadWriteLock 不同的是,StampedLock 的读取操作不会被阻塞,但可能会失败,如果读取的数据在读取过程中发生了改变,则读取操作会失败并返回一个标记(stamp),此时可以根据需要重试读取操作或者转换为独占写入操作。

StampedLock 使用一个长整型的 stamp 来表示锁的版本号,每次修改数据后都会更新版本号。读取操作需要传入当前版本号以确保读取的数据没有被修改,写入操作则需要传入上一次读取操作返回的版本号以确保数据的一致性。示例代码如下:

 

import java.util.concurrent.locks.StampedLock;
public class MyRunnable implements Runnable {
    private final StampedLock lock = new StampedLock();
    private int x = 0;
    private int y = 0;

    public void run() {
        long stamp = lock.tryOptimisticRead(); // 尝试乐观读取
        int currentX = x;
        int currentY = y;
        if (!lock.validate(stamp)) { // 校验版本号
            stamp = lock.readLock(); // 获取读锁
            try {
                currentX = x; // 重新读取数据
                currentY = y;
            } finally {
                lock.unlockRead(stamp); // 释放读锁
            }
        }
        // 访问共享资源(读取)
    }

    public void write(int newX, int newY) {
        long stamp = lock.writeLock(); // 获取写锁
        try {
            x = newX; // 修改数据
            y = newY;
        } finally {
            lock.unlockWrite(stamp); // 释放写锁
        }
    }
}

在上述示例中,我们创建了一个 StampedLock,并使用 tryOptimisticRead() 方法尝试进行乐观读取操作。如果校验版本号失败,则说明数据被修改过,此时需要再次获取读锁并重新读取数据。在修改数据时,使用 writeLock() 方法获取写锁,修改完成后释放写锁。

 

2. 使用场景

StampedLock 适用于以下场景:

  • 读取操作频繁,而写入操作较少。
  • 数据的一致性要求不高,即数据会发生周期


性的变化,但读取操作与写入操作之间的时间间隔较短,不需要使用分布式锁或者数据库事务来保证数据一致性。

 

使用 StampedLock 可以提高程序的并发性能,特别是在读取操作频繁、写入操作较少的情况下。但需要注意的是,StampedLock 的实现依赖于硬件的 CAS(Compare and Swap)指令,因此在某些 CPU 架构上可能会存在性能问题。此外,在使用乐观读取模式时需要进行版本号校验,如果校验失败则需要重新获取读锁并重新读取数据,这可能会带来额外的开销和复杂度。

五、总结

Java 提供了多种锁机制来协调多个线程对共享资源的访问。ReentrantLock 是最基本的一种锁,它采用独占式访问方式,可以精确控制多个线程对共享资源的访问顺序。Condition 可以用于在锁的基础上实现更灵活的同步操作,例如线程的等待和唤醒。ReadWriteLock 是一种特殊的锁,它允许多个线程同时读取共享资源,但只允许一个线程对共享资源进行写操作。StampedLock 是对读写锁的一种改进,具有更高的并发性能,但需要注意的是它的实现依赖于硬件的 CAS 指令。

在使用锁时需要注意避免死锁、避免过度竞争和防止资源饥饿等问题。应该根据具体的场景选择不同的锁机制,并合理地设置锁的粒度和范围。同时也可以考虑使用一些高级的并发工具来简化锁的管理,例如 Executor 框架、原子变量、信号量、倒计时门闩等。



Tags:ReentrantLock   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
到底什么情况下该用ReentrantLock?
在多线程编程中,锁(Lock)是一种重要的同步机制,它可以保证同一时间只有一个线程可以访问共享资源。Java 中提供了两种类型的锁:隐式锁和显式锁。 隐式锁通过 synchronized 关键...【详细内容】
2023-05-06  Search: ReentrantLock  点击:(366)  评论:(0)  加入收藏
ReentrantLock原理、应用与优秀实践
一、ReentrantLock简介1.1 什么是ReentrantLockReentrantLock是Java并发包( java.util.concurrent.locks)中的一个重要类,用于实现可重入的互斥锁。它提供了一种替代synchroniz...【详细内容】
2023-04-05  Search: ReentrantLock  点击:(106)  评论:(0)  加入收藏
JAVA并发之ReentrantLock原理解析
Java从版本5开始,在 java.util.concurrent.locks包内给我们提供了除了synchronized关键字以外的几个新的锁功能的实现,ReentrantLock就是其中的一个。但是这并不意味着我们可...【详细内容】
2021-12-17  Search: ReentrantLock  点击:(297)  评论:(0)  加入收藏
还不知道ReentrantLock的实现流程,那你就out了
公平锁和非公平锁的区别锁的公平是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。在上面分析的例子来说,只要CAS设置...【详细内容】
2021-01-20  Search: ReentrantLock  点击:(351)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(14)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(24)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(25)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(56)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(68)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(72)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(89)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(106)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(97)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(75)  评论:(0)  加入收藏
站内最新
站内热门
站内头条