一、简介

在之前的线程系列文章中,我们介绍到了使用synchronized关键字可以实现线程同步安全的效果,以及采用wait()notify()notifyAll()方法,可以实现多个线程之间的通信协调,基本可以满足并发编程的需求。

但是采用synchronized进行加锁,这种锁一般都比较重,里面的实现机制也非常复杂,同时获取锁时必须一直等待,没有额外的尝试机制,如果编程不当,可能就容易发生死锁现象。

从 JDK 1.5 开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大的简化多线程程序的编写。

比如我们今天要介绍的java.util.concurrent.locks包提供的ReentrantLock类,一个可重入的互斥锁,它具有与使用synchronized加锁一样的特性,并且功能更加强大。

下面我们一起来学习一下ReentrantLock类的基本使用。

二、ReentrantLock 基本用法

在介绍ReentrantLock之前,我们先来看一下传统的使用synchronized对方法进行加锁的示例。

public class Counter {

    private int count;

    public void add() {
synchronized(this) {
count ++;
System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
}
} public int getCount() {
return count;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter(); // 创建5个线程,同时对count进行加一操作
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
counter.add();
}
}).start();
} // 假设休眠1秒,5个线程执行完毕
Thread.sleep(1000);
System.out.println("count:" + counter.getCount());
}

输出结果如下:

ThreadName:Thread-0, count:1
ThreadName:Thread-1, count:2
ThreadName:Thread-2, count:3
ThreadName:Thread-3, count:4
ThreadName:Thread-4, count:5
count:5

如果用ReentrantLock替代,只需要将Counter中的代码改造为如下:

public class Counter {

    private final Lock lock = new ReentrantLock();

    private int count;

    public void add() {
// 加锁
lock.lock();
try {
count ++;
System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
} finally {
// 释放锁
lock.unlock();
}
} public int getCount() {
return count;
}
}

运行程序,结果与上面一致,可以证明:ReentrantLock具备与synchronized一样的加锁功能。

同时,ReentrantLock还具备在指定的时间内尝试获取锁的机制,比如下面这行代码:

if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}

尝试在 3 秒内获取锁,如果获取不到就返回false,程序不需要无限等待下去,这个功能在实际开发中使用非常的广泛。

从上面的示例代码,我们可以总结出synchronizedReentrantLock有以下几点不一样。

  • ReentrantLock需要手动调用加锁方法;而synchronized不需要,它采用了隐藏的加锁方式,借助 jvm 来实现
  • synchronized不需要考虑异常;而ReentrantLock获取锁之后,要在finally中正确的释放锁,否则会影响其它线程
  • ReentrantLock拥有尝试获取锁的超时机制,利用它可以避免无限等待;而synchronized不具备
  • synchronized是 Java 语言层面提供的语法;而ReentrantLock是 Java 代码实现的可重入锁

因此,在并发编程中,使用ReentrantLock比直接使用synchronized更灵活、更安全,采用tryLock(long time, TimeUnit unit)方法,即使未获取到锁也不会导致死锁。

三、ReentrantLock 和 synchronized 持有的对象监视器是同一个吗?

可能有的同学会发出这样的一个问题,使用ReentrantLock进行加锁和使用synchronized加锁,两者持有的对象监视器是同一个吗?

下面我们一起来看一个例子。

public class Counter {

    private final Lock lock = new ReentrantLock();

    private int count;

    public synchronized void methodA() {
System.out.println("ThreadName:" + Thread.currentThread().getName() + ",begin methodA, count:" + getCount());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count ++;
System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount()); } public void methodB() {
// 加锁
lock.lock();
try {
System.out.println("ThreadName:" + Thread.currentThread().getName() + ",begin methodB, count:" + getCount());
Thread.sleep(3000);
count ++;
System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
} catch (Exception e){
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
} public int getCount() {
return count;
}
}
public class MyThreadA extends Thread {

    private Counter counter;

    public MyThreadA(Counter counter) {
this.counter = counter;
} @Override
public void run() {
counter.methodA();
}
}
public class MyThreadB extends Thread {

    private Counter counter;

    public MyThreadB(Counter counter) {
this.counter = counter;
} @Override
public void run() {
counter.methodB();
}
}
public class MyThreadTest {

    public static void main(String[] args) {
Counter counter = new Counter(); MyThreadA threadA = new MyThreadA(counter);
threadA.start(); MyThreadB threadB = new MyThreadB(counter);
threadB.start();
}
}

看一下运行结果:

ThreadName:Thread-0,begin methodA, count:0
ThreadName:Thread-1,begin methodB, count:0
ThreadName:Thread-0, count:2
ThreadName:Thread-1, count:2

从日志上可以看出,采用两个线程分别采用synchronizedReentrantLock两种加锁方式对count进行操作,两个线程交替执行,可以得出一个结论:synchronizedReentrantLock持有的对象监视器不同。

四、Condition 基本用法

在之前的文章中,我们介绍了在synchronized同步方法/代码块中,使用wait()notify()notifyAll()可以实现线程之间的等待/通知模型。

ReentrantLock同样也可以,只需要借助Condition类即可实现,Condition提供的await()signal()signalAll()原理和synchronized锁对象的wait()notify()notifyAll()是一致的,并且其行为也是一样的。

我们还是先来看一个简单的示例。

public class Counter {

    private final Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    private int count;

    public void await(){
// 加锁
lock.lock();
try {
condition.await();
System.out.println("await等待结束,count:" + getCount());
} catch (Exception e){
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
} public void signal(){
// 加锁
lock.lock();
try {
count++;
// 唤醒某个等待线程
condition.signal();
// 唤醒所有等待线程
// condition.signalAll();
System.out.println("signal 唤醒通知完毕");
} catch (Exception e){
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
} public int getCount() {
return count;
}
}
public class MyThreadA extends Thread {

    private Counter counter;

    public MyThreadA(Counter counter) {
this.counter = counter;
} @Override
public void run() {
counter.await();
}
}
public class MyThreadB extends Thread {

    private Counter counter;

    public MyThreadB(Counter counter) {
this.counter = counter;
} @Override
public void run() {
counter.signal();
}
}
public class MyThreadTest {

    public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter(); // 先启动执行等待的线程
MyThreadA threadA = new MyThreadA(counter);
threadA.start(); Thread.sleep(3000); // 过3秒,再启动执行通知的线程
MyThreadB threadB = new MyThreadB(counter);
threadB.start();
}
}

看一下运行结果:

signal 通知完毕
await等待结束,count:1

从结果上看很明显的看出,等待线程MyThreadA先启动,过了 3 秒之后再启动了MyThreadB,但是signal()方法先执行完毕,再通知await()方法执行,符合代码预期。

这个例子也证明了一点:condition.await()方法是释放了锁,不然signal()方法体不会被执行。

相比wait/notify/notifyAll的等待/通知模型,Condition更加灵活,理由有以下几点:

  • notify()方法唤醒线程时,被通知的线程由 Java 虚拟机随机选择;而采用ReentrantLock结合Condition可以实现有选择性地通知,这一特性在实际编程中非常实用
  • 一个Lock里面可以创建多个Condition实例,实现多路通知,使用多个Condition的应用场景很常见,比如ArrayBlockingQueue

五、小结

本文主要围绕ReentrantLock的基本使用做了一次简单的知识总结,如果有不正之处,请多多谅解,并欢迎批评指出。

六、参考

1、博客园 -五月的仓颉 - ReentrantLock的使用和Condition

2、廖雪峰 - 使用ReentrantLock

七、写到最后

最近无意间获得一份阿里大佬写的技术笔记,内容涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL 等技术知识。需要的小伙伴可以点击如下链接获取,资源地址:技术资料笔记

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!

多线程系列(八) -ReentrantLock基本用法介绍的更多相关文章

  1. java多线程系列(八)---CountDownLatch和CyclicBarrie

    CountDownLatch 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java多线程系列(一)之java多线程技能 java多线程系列(二)之对象变量的并发访问 java多线 ...

  2. java多线程系列(四)---ReentrantLock的使用

    Lock的使用 前言:本系列将从零开始讲解java多线程相关的技术,内容参考于<java多线程核心技术>与<java并发编程实战>等相关资料,希望站在巨人的肩膀上,再通过我的理 ...

  3. 【多线程系列】AQS CAS简单介绍

    一.什么是CAS CAS(Compare And Swap),即比较并交换.是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V).预期原值(A)和新值(B). ...

  4. 多线程系列八:线程安全、Java内存模型(JMM)、底层实现原理

    一.线程安全 1.  怎样让多线程下的类安全起来 无状态.加锁.让类不可变.栈封闭.安全的发布对象 2. 死锁 2.1 死锁概念及解决死锁的原则 一定发生在多个线程争夺多个资源里的情况下,发生的原因是 ...

  5. Java多线程系列八——volatile和ThreadLocal

    参考资料: http://ifeve.com/java-memory-model-4/ http://www.infoq.com/cn/articles/java-memory-model-1 htt ...

  6. 【Java多线程系列八】volatile和ThreadLocal

    1. volatile final class Singleton { private static Singleton instance = null; private Singleton() { ...

  7. Java多线程系列2 线程常见方法介绍

    守护线程 执行一些非业务方法,比如gc.当全部都是守护线程的时候,jvm退出 线程优先级  设置线程优先级:setPriority(int priorityLevel).参数priorityLevel ...

  8. java多线程系列(九)---ArrayBlockingQueue源码分析

    java多线程系列(九)---ArrayBlockingQueue源码分析 目录 认识cpu.核心与线程 java多线程系列(一)之java多线程技能 java多线程系列(二)之对象变量的并发访问 j ...

  9. java多线程系列 目录

    Java多线程系列1 线程创建以及状态切换    Java多线程系列2 线程常见方法介绍    Java多线程系列3 synchronized 关键词    Java多线程系列4 线程交互(wait和 ...

  10. Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock

    本章对ReentrantLock包进行基本介绍,这一章主要对ReentrantLock进行概括性的介绍,内容包括:ReentrantLock介绍ReentrantLock函数列表ReentrantLo ...

随机推荐

  1. [转帖]GPT4All 一个开源 ChatGPT

    https://zhuanlan.zhihu.com/p/618947904 通用预训练语言模型. ChatGPT 正在迅速发展与传播,新的大型语言模型 (LLM) 正在以越来越快的速度开发.就在过去 ...

  2. [转帖]Megacli 错误码

    MegaCLI Error Messages 0x00 Command completed successfully 0x01 Invalid command 0x02 DCMD opcode is ...

  3. 使用buildx在x86机器上面编译arm64架构的Docker镜像

    buildx 多架构编译 安装docker 下载docker 下载buildx 安装架构支持 docker run --privileged --rm tonistiigi/binfmt --inst ...

  4. Linux下面rsync 实现 完全一致的同步方法

    1. 在某些特殊的linux机器上面, 比如龙芯后者是飞腾服务器,部分工具不太好用, 需要使用x86弄好之后进行同步过去, 这个时候scp 最简单但是网络流量非常大, 不如使用rsync, rsync ...

  5. Linux查看登录用户记录信息

    Linux查看登录用户记录信息 登录成功的信息 last 可以简单统计一下: last |awk '{print $3}' |sort |uniq -c |sort -k1nr 登录失败的 就是 la ...

  6. 使用Grafana 监控 SQLSERVER数据库

    使用Grafana 监控 SQLSERVER数据库 1.获取镜像信息以及启动镜像 docker pull awaragi/prometheus-mssql-exporter docker run -e ...

  7. git中git cherry-pick的使用

    git中git cherry-pick的使用 A分支是从远端的开发分支dev拉取的 B分支是从远端的测试分支rel拉取的 现在我们遇见一个问题. 我们在A分支修改了代码.并且推送到了远端的dev分支. ...

  8. 【小分享】vm-storage中,计算metric的平均长度

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 表达式如下: sum by (type) (vm_cach ...

  9. Django笔记七之ManyToMany和OneToOne介绍

    本文首发于本人微信公众号:Hunter后端 原文链接:Django笔记七之ManyToMany和OneToOne介绍 ManyToMany 是一种多对多的关系,在用途和使用方法上和外键 Foreign ...

  10. 私密离线聊天新体验!llama-gpt聊天机器人:极速、安全、搭载Llama 2

    "私密离线聊天新体验!llama-gpt聊天机器人:极速.安全.搭载Llama 2,尽享Code Llama支持!" 一个自托管的.离线的.类似chatgpt的聊天机器人.由美洲驼 ...