Java并发编程之ReentrantLock和ReentrantReadWriteLock

2023-11-21 02:50

本文主要是介绍Java并发编程之ReentrantLock和ReentrantReadWriteLock,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在Java多线程编程中,除了可以使用synchronized关键字实现线程同步外,从JDK1.5开始,新增了ReentrantLock、ReentrantReadWriteLock等类,同样能实现同步效果,而且在使用上更加方便。

ReentrantLock

ReentrantLock是可重入互斥锁,调用它的lock()方法获取锁,unlock()方法释放锁。

lock()和unlock()的逻辑

当一个线程调用ReentrantLock的lock()方法时,如果这个锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为1;如果当前线程已经保持了该锁,则把保持计数加1,并且该方法立即返回;如果这个锁被另一个线程保持,则在获得锁之前,该线程将一直处于休眠状态,此时锁的保持计数被设置为1。

当一个线程调用ReentrantLock的unlock()方法时,如果当前线程是此锁的拥有者,则将保持计数减1,如果保持计数变成了0,则释放该锁。如果当前线程不是该锁的持有者,则抛出IlletalMonitorStateException。

Condition

Condition概要

ReentrantLock实现了Lock接口,与Lock类相关的一个类是Condition,借助Condition可以实现线程间的等待/通知。Condition也是Java5中出现的技术,它具有比object.wait()和object.notify()更好的灵活性,比如实现多路通知功能,即一个Lock可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而有选择性地进行线程通知,在调度线程上更加灵活。

使用Condition实现等待/通知的样例代码如下:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class MyService {private Lock lock = new ReentrantLock();public Condition condition = lock.newCondition();public void await() {try {lock.lock();System.out.println("await时间为" + System.currentTimeMillis());condition.await();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public void signal() {try {lock.lock();System.out.println("signal时间为" + System.currentTimeMillis());condition.signal();} finally {lock.unlock();}}
}class ThreadA extends Thread {private MyService service;ThreadA(MyService service) {super();this.service = service;}@Overridepublic void run() {service.await();}
}public class Main {public static void main(String[] args) throws InterruptedException {MyService service = new MyService();ThreadA a = new ThreadA(service);a.start();Thread.sleep(3000);service.signal();}}
await时间为1538418609554
signal时间为1538418612513

使用多个Condition实现通知部分线程:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class MyService {private Lock lock = new ReentrantLock();private Condition conditionA = lock.newCondition();private Condition conditionB = lock.newCondition();void awaitA() {try {lock.lock();System.out.println("begin awaitA时间为" + System.currentTimeMillis()+ " ThreadName=" + Thread.currentThread().getName());conditionA.await();System.out.println("end awaitA时间为" + System.currentTimeMillis()+ " ThreadName=" + Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}void awaitB() {try {lock.lock();System.out.println("begin awaitB时间为" + System.currentTimeMillis()+ " ThreadName=" + Thread.currentThread().getName());conditionB.await();System.out.println("end awaitB时间为" + System.currentTimeMillis()+ " ThreadName=" + Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}void signalAll_A() {try {lock.lock();System.out.println("signalAll_A时间为" + System.currentTimeMillis()+ " ThreadName=" + Thread.currentThread().getName());conditionA.signalAll();} finally {lock.unlock();}}public void signalAll_B() {try {lock.lock();System.out.println("signalAll_B时间为" + System.currentTimeMillis()+ " ThreadName=" + Thread.currentThread().getName());conditionB.signalAll();} finally {lock.unlock();}}
}class ThreadA extends Thread {private MyService service;ThreadA(MyService service) {super();this.service = service;}@Overridepublic void run() {service.awaitA();}
}class ThreadB extends Thread {private MyService service;ThreadB(MyService service) {super();this.service = service;}@Overridepublic void run() {service.awaitB();}
}public class Main {public static void main(String[] args) throws InterruptedException {MyService service = new MyService();ThreadA a = new ThreadA(service);a.setName("A");a.start();ThreadB b = new ThreadB(service);b.setName("B");b.start();Thread.sleep(3000);service.signalAll_A();}}
begin awaitA时间为1538418670871 ThreadName=A
begin awaitB时间为1538418670871 ThreadName=B
signalAll_A时间为1538418673871 ThreadName=main
end awaitA时间为1538418673871 ThreadName=A

condition.await()的典型调用方式

通常一个线程是在某个条件c下需要被阻塞,这时我们让该线程在条件c对应的条件对象condition下等待。另一个线程使条件c被解除,同时它调用条件对象condition的signalAll()方法,唤醒所有等待线程,告诉它们可以继续执行了。

这时所有在此条件对象上等待的线程从等待集中移出,它们再次成为可运行的,调度器将再次激活它们。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。由于其执行,条件c可能又被满足,导致其他的原来被阻塞的线程在执行的时候仍然需要被阻塞,这便是“虚假唤醒”。因此,条件c需要循环进行判断。

condition.await()的典型调用方式如下:

while(!okToProceed){condition.await();
}

关于此可以看下我的https://blog.csdn.net/nlznlz/article/details/89042809这篇文章中对阻塞队列ArrayBlockingQueue的源码分析,其中便体现了这个道理。

公平锁和非公平锁

锁Lock分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即FIFO的顺序。非公平锁则是一种获取锁的抢占机制,是随机获得锁的,先来的不一定先得到锁,这个方式可能造成某些线程一直得不到锁。

下面的图形象化展示了公平锁和非公平锁的区别:

公平锁与非公平锁的一个重要区别就在于上图中的2、6、10那个步骤,对应源码如下:

//非公平锁
final boolean nonfairTryAcquire(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) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}//公平锁
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;}

分析以上代码,我们可以看到公平锁就是在获取锁之前会先判断等待队列是否为空或者自己是否位于队列头部,该条件通过才能继续获取锁。

再结合兔子喝水的图分析,非公平锁获取所得顺序基本决定在9、10、11这三个事件发生的先后顺序:

  1. 若在释放锁的时候总是没有新的兔子来打扰,则非公平锁等于公平锁;
  2. 若释放锁的时候,正好一个兔子来喝水,而此时位于队列头的兔子还没有被唤醒(因为线程上下文切换是需要不少开销的),此时后来的兔子则优先获得锁,成功打破公平,成为非公平锁;

其实对于非公平锁,只要线程进入了等待队列,队列里面依然是FIFO的原则,跟公平锁的顺序是一样的。因为公平锁与非公平锁的release()部分代码是共用AQS的代码:

private void unparkSuccessor(Node node) {int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);Node s = node.next;if (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)LockSupport.unpark(s.thread);}

由于非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起,因此非公平锁的效率要高于公平锁。

ReentrantLock的其他方法

ReentrantLock的其他方法如下:

  • getHoldCount(),查询当前线程保持此锁的个数,如果此锁未被当前线程保持过,则返回0;
  • getQueueLength(),返回正等待获取此锁的线程估计数;
  • getWaitQueueLength(Condition),返回等待此Condition的线程估计数;
  • hasQueuedThread(Thread),查询指定的线程是否正在等待获取此锁定;
  • hasQueuedThreads(),查询是否有线程正在等待获取此锁定;
  • hasWaiters(Condition),查询是否有线程正在等待此Condition;
  • isFair(),判断此锁是不是公平锁;
  • isHeldByCurrentThread(),查询当前线程是否保持此锁;
  • isLocked(),查询此锁是否由任意线程保持;
  • tryLock(),如果调用时锁未被另一个线程保持,则获取该锁,并返回true,否则返回false。

ReentrantReadWriteLock

ReentrantLock是完全互斥排他的锁,这样虽然保证了线程安全,但效率却是非常低下的,对此,JDK提供了一种读写锁ReentrantReadWriteLock,使用它可以加快运行效率。

读写锁表示有两个锁,一个是读锁,也叫共享锁;一个是写锁,也叫排他锁。多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。

读读共享

这段代码展示了读锁的共享性质:

import java.util.concurrent.locks.ReentrantReadWriteLock;public class Main {public static void main(String[] args) {Service s = new Service();Thread ta = new MyThread("threadA", s);ta.start();Thread tb = new MyThread("threadB", s);tb.start();}public static class MyThread extends Thread {Service s;public MyThread(String name, Service s) {super(name);this.s = s;}public void run() {s.service();}}public static class Service {ReentrantReadWriteLock lock = new ReentrantReadWriteLock();public void service() {lock.readLock().lock();System.out.println(Thread.currentThread().getName() + "获得了读锁,当前时间: " + System.currentTimeMillis());lock.readLock().unlock();}}
}
threadA获得了读锁,当前时间: 1538414809755
threadB获得了读锁,当前时间: 1538414809756

可以看到,两个线程都能顺利走到lock()方法后面的代码中,即读锁是共享的,不同的线程可以同时持有。

写写互斥

这段代码展示了写锁的排他性质:

import java.util.concurrent.locks.ReentrantReadWriteLock;public class Main {public static void main(String[] args) {Service s = new Service();Thread ta = new MyThread("threadA", s);ta.start();Thread tb = new MyThread("threadB", s);tb.start();}public static class MyThread extends Thread {Service s;public MyThread(String name, Service s) {super(name);this.s = s;}public void run() {s.service();}}public static class Service {ReentrantReadWriteLock lock = new ReentrantReadWriteLock();public void service() {lock.writeLock().lock();System.out.println(Thread.currentThread().getName() + "获得了写锁,当前时间: " + System.currentTimeMillis());try {Thread.sleep(10000);} catch (Exception e) {e.printStackTrace();}lock.writeLock().unlock();}}
}
threadA获得了写锁,当前时间: 1538415074339
threadB获得了写锁,当前时间: 1538415084340

可以看到,在大约10秒钟后另一个线程才进入到lock()方法后的代码,即写锁是排他的。

读写互斥

import java.util.concurrent.locks.ReentrantReadWriteLock;public class Main {public static void main(String[] args) {Service s = new Service();Thread ta = new MyThread("threadA", s);ta.start();Thread tb = new MyThread("threadB", s);tb.start();}public static class MyThread extends Thread {Service s;public MyThread(String name, Service s) {super(name);this.s = s;}public void run() {if ("threadA".equals(getName())) {s.read();} else if ("threadB".equals(getName())) {s.write();}}}public static class Service {ReentrantReadWriteLock lock = new ReentrantReadWriteLock();public void read() {lock.readLock().lock();System.out.println(Thread.currentThread().getName() + "获得了读锁,当前时间: " + System.currentTimeMillis());try {Thread.sleep(10000);} catch (Exception e) {e.printStackTrace();}lock.readLock().unlock();}public void write() {lock.writeLock().lock();System.out.println(Thread.currentThread().getName() + "获得了写锁,当前时间: " + System.currentTimeMillis());try {Thread.sleep(10000);} catch (Exception e) {e.printStackTrace();}lock.writeLock().unlock();}}
}
threadA获得了读锁,当前时间: 1538415518663
threadB获得了写锁,当前时间: 1538415528663

 

这篇关于Java并发编程之ReentrantLock和ReentrantReadWriteLock的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/399308

相关文章

javax.net.ssl.SSLHandshakeException:异常原因及解决方案

《javax.net.ssl.SSLHandshakeException:异常原因及解决方案》javax.net.ssl.SSLHandshakeException是一个SSL握手异常,通常在建立SS... 目录报错原因在程序中绕过服务器的安全验证注意点最后多说一句报错原因一般出现这种问题是因为目标服务器

Java实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja

springboot项目中整合高德地图的实践

《springboot项目中整合高德地图的实践》:本文主要介绍springboot项目中整合高德地图的实践,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一:高德开放平台的使用二:创建数据库(我是用的是mysql)三:Springboot所需的依赖(根据你的需求再

spring中的ImportSelector接口示例详解

《spring中的ImportSelector接口示例详解》Spring的ImportSelector接口用于动态选择配置类,实现条件化和模块化配置,关键方法selectImports根据注解信息返回... 目录一、核心作用二、关键方法三、扩展功能四、使用示例五、工作原理六、应用场景七、自定义实现Impor

SpringBoot3应用中集成和使用Spring Retry的实践记录

《SpringBoot3应用中集成和使用SpringRetry的实践记录》SpringRetry为SpringBoot3提供重试机制,支持注解和编程式两种方式,可配置重试策略与监听器,适用于临时性故... 目录1. 简介2. 环境准备3. 使用方式3.1 注解方式 基础使用自定义重试策略失败恢复机制注意事项

SpringBoot整合Flowable实现工作流的详细流程

《SpringBoot整合Flowable实现工作流的详细流程》Flowable是一个使用Java编写的轻量级业务流程引擎,Flowable流程引擎可用于部署BPMN2.0流程定义,创建这些流程定义的... 目录1、流程引擎介绍2、创建项目3、画流程图4、开发接口4.1 Java 类梳理4.2 查看流程图4

一文详解如何在idea中快速搭建一个Spring Boot项目

《一文详解如何在idea中快速搭建一个SpringBoot项目》IntelliJIDEA作为Java开发者的‌首选IDE‌,深度集成SpringBoot支持,可一键生成项目骨架、智能配置依赖,这篇文... 目录前言1、创建项目名称2、勾选需要的依赖3、在setting中检查maven4、编写数据源5、开启热

Java对异常的认识与异常的处理小结

《Java对异常的认识与异常的处理小结》Java程序在运行时可能出现的错误或非正常情况称为异常,下面给大家介绍Java对异常的认识与异常的处理,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参... 目录一、认识异常与异常类型。二、异常的处理三、总结 一、认识异常与异常类型。(1)简单定义-什么是

SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志

《SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志》在SpringBoot项目中,使用logback-spring.xml配置屏蔽特定路径的日志有两种常用方式,文中的... 目录方案一:基础配置(直接关闭目标路径日志)方案二:结合 Spring Profile 按环境屏蔽关

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.