Java中ReentrantLock4种常见的坑

目录
  • 前言
  • Lock 简介
  • ReentrantLock 使用
  • ReentrantLock 中的坑
    • 1.ReentrantLock 默认为非公平锁
    • 2.在 finally 中释放锁
    • 3.锁不能被释放多次
    • 4.lock 不要放在 try 代码内
  • 总结

前言

JDK 1.5 之前 synchronized 的性能是比较低的,但在 JDK 1.5 中,官方推出一个重量级功能 Lock,一举改变了 Java 中锁的格局。JDK 1.5 之前当我们谈到锁时,只能使用内置锁 synchronized,但如今我们锁的实现又多了一种显式锁 Lock。

前面的文章我们已经介绍了 synchronized,详见以下列表:

《浅谈synchronized加锁this和class的区别》

《Java中的synchronized 优化方法之锁膨胀机制》

《Java中synchronized 的4个优化技巧》

所以本文咱们重点来看 Lock。

Lock 简介

Lock 是一个顶级接口,它的所有方法如下图所示: 

 它的子类列表如下: 

我们通常会使用 ReentrantLock 来定义其实例,它们之间的关联如下图所示:

PS:Sync 是同步锁的意思,FairSync 是公平锁,NonfairSync 是非公平锁。

ReentrantLock 使用

学习任何一项技能都是先从使用开始的,所以我们也不例外,咱们先来看下 ReentrantLock 的基础使用:

public class LockExample {
    // 创建锁对象
    private final ReentrantLock lock = new ReentrantLock();
    public void method() {
        // 加锁操作
        lock.lock();
        try {
            // 业务代码......
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

ReentrantLock 在创建之后,有两个关键性的操作:

  • 加锁操作:lock()
  • 释放锁操作:unlock()

ReentrantLock 中的坑

1.ReentrantLock 默认为非公平锁

很多人会认为(尤其是新手朋友),ReentrantLock 默认的实现是公平锁,其实并非如此,ReentrantLock 默认情况下为非公平锁(这主要是出于性能方面的考虑),

比如下面这段代码:

import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
    // 创建锁对象
    private static final ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        // 定义线程任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // 加锁
                lock.lock();
                try {
                    // 打印执行线程的名字
                    System.out.println("线程:" + Thread.currentThread().getName());
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            }
        };
        // 创建多个线程
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
    }
}

以上程序的执行结果如下: 

从上述执行的结果可以看出,ReentrantLock 默认情况下为非公平锁。因为线程的名称是根据创建的先后顺序递增的,所以如果是公平锁,那么线程的执行应该是有序递增的,但从上述的结果可以看出,线程的执行和打印是无序的,这说明 ReentrantLock 默认情况下为非公平锁。

想要将 ReentrantLock 设置为公平锁也很简单,只需要在创建 ReentrantLock 时,设置一个 true 的构造参数就可以了,如下代码所示:

import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
    // 创建锁对象(公平锁)
    private static final ReentrantLock lock = new ReentrantLock(true);
    public static void main(String[] args) {
        // 定义线程任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // 加锁
                lock.lock();
                try {
                    // 打印执行线程的名字
                    System.out.println("线程:" + Thread.currentThread().getName());
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            }
        };
        // 创建多个线程
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
    }
}

以上程序的执行结果如下: 

从上述结果可以看出,当我们显式的给 ReentrantLock 设置了 true 的构造参数之后,ReentrantLock 就变成了公平锁,线程获取锁的顺序也变成有序的了。

其实从 ReentrantLock 的源码我们也可以看出它究竟是公平锁还是非公平锁,ReentrantLock 部分源码实现如下:

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

从上述源码中可以看出,默认情况下 ReentrantLock 会创建一个非公平锁,如果在创建时显式的设置构造参数的值为 true 时,它就会创建一个公平锁。

2.在 finally 中释放锁

使用 ReentrantLock 时一定要记得释放锁,否则就会导致该锁一直被占用,其他使用该锁的线程则会永久的等待下去,所以我们在使用 ReentrantLock 时,一定要在 finally 中释放锁,这样就可以保证锁一定会被释放。

反例

import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
    // 创建锁对象
    private static final ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        // 加锁操作
        lock.lock();
        System.out.println("Hello,ReentrantLock.");
        // 此处会报异常,导致锁不能正常释放
        int number = 1 / 0;
        // 释放锁
        lock.unlock();
        System.out.println("锁释放成功!");
    }
}

以上程序的执行结果如下: 

从上述结果可以看出,当出现异常时锁未被正常释放,这样就会导致其他使用该锁的线程永久的处于等待状态。

正例

import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
    // 创建锁对象
    private static final ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        // 加锁操作
        lock.lock();
        try {
            System.out.println("Hello,ReentrantLock.");
            // 此处会报异常
            int number = 1 / 0;
        } finally {
            // 释放锁
            lock.unlock();
            System.out.println("锁释放成功!");
        }
    }
}

以上程序的执行结果如下: 

从上述结果可以看出,虽然方法中出现了异常情况,但并不影响 ReentrantLock 锁的释放操作,这样其他使用此锁的线程就可以正常获取并运行了。

3.锁不能被释放多次

lock 操作的次数和 unlock 操作的次数必须一一对应,且不能出现一个锁被释放多次的情况,因为这样就会导致程序报错。

反例

一次 lock 对应了两次 unlock 操作,导致程序报错并终止执行,示例代码如下:

import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
    // 创建锁对象
    private static final ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        // 加锁操作
        lock.lock();

        // 第一次释放锁
        try {
            System.out.println("执行业务 1~");
            // 业务代码 1......
        } finally {
            // 释放锁
            lock.unlock();
            System.out.println("锁释锁");
        }

        // 第二次释放锁
        try {
            System.out.println("执行业务 2~");
            // 业务代码 2......
        } finally {
            // 释放锁
            lock.unlock();
            System.out.println("锁释锁");
        }
        // 最后的打印操作
        System.out.println("程序执行完成.");
    }
}

以上程序的执行结果如下: 

从上述结果可以看出,执行第 2 个 unlock 时,程序报错并终止执行了,导致异常之后的代码都未正常执行。

4.lock 不要放在 try 代码内

在使用 ReentrantLock 时,需要注意不要将加锁操作放在 try 代码中,这样会导致未加锁成功就执行了释放锁的操作,从而导致程序执行异常。

反例

import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
    // 创建锁对象
    private static final ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        try {
            // 此处异常
            int num = 1 / 0;
            // 加锁操作
            lock.lock();
        } finally {
            // 释放锁
            lock.unlock();
            System.out.println("锁释锁");
        }
        System.out.println("程序执行完成.");
    }
}

以上程序的执行结果如下: 

 从上述结果可以看出,如果将加锁操作放在 try 代码中,可能会导致两个问题:

  • 未加锁成功就执行了释放锁的操作,从而导致了新的异常;
  • 释放锁的异常会覆盖程序原有的异常,从而增加了排查问题的难度。

总结

本文介绍了 Java 中的显式锁 Lock 及其子类 ReentrantLock 的使用和注意事项,Lock 在 Java 中占据了锁的半壁江山,但在使用时却要注意 4 个问题:

  • 默认情况下 ReentrantLock 为非公平锁而非公平锁;
  • 加锁次数和释放锁次数一定要保持一致,否则会导致线程阻塞或程序异常;
  • 加锁操作一定要放在 try 代码之前,这样可以避免未加锁成功又释放锁的异常;
  • 释放锁一定要放在 finally 中,否则会导致线程阻塞。

到此这篇关于Java中ReentrantLock 4种常见的 坑的文章就介绍到这了,更多相关ReentrantLock 坑内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java常用锁synchronized和ReentrantLock的区别

    目录 区别1:用法不同 synchronized 基础使用 ReentrantLock 基础使用 区别2:获取锁和释放锁方式不同 区别3:锁类型不同 区别4:响应中断不同 区别5:底层实现不同 小结 前言: 在 Java 中,常用的锁有两种:synchronized(内置锁)和 ReentrantLock(可重入锁),二者的功效都是相同得,但又有很多不同点,所以我们今天就来聊聊. 区别1:用法不同 synchronized 可用来修饰普通方法.静态方法和代码块,而 ReentrantLock 只

  • java并发编程专题(五)----详解(JUC)ReentrantLock

    上一节我们了解了Lock接口的一些简单的说明,知道Lock锁的常用形式,那么这节我们正式开始进入JUC锁(java.util.concurrent包下的锁,简称JUC锁).下面我们来看一下Lock最常用的实现类ReentrantLock. 1.ReentrantLock简介 由单词意思我们可以知道这是可重入的意思.那么可重入对于锁而言到底意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放.这模仿了 sy

  • Java并发编程之浅谈ReentrantLock

    一.首先看图 二.lock()跟踪源码 这里对公平锁和非公平锁做了不同实现,由构造方法参数决定是否公平. public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } 2.1 非公平锁实现 static final class NonfairSync extends Sync { private static final long serialVersionUID = 731615

  • java并发编程中ReentrantLock可重入读写锁

    目录 一.ReentrantLock可重入锁 二.ReentrantReadWriteLock读写锁 三.读锁之间不互斥 一.ReentrantLock可重入锁 可重入锁ReentrantLock 是一个互斥锁,即同一时间只有一个线程能够获取锁定资源,执行锁定范围内的代码.这一点与synchronized 关键字十分相似.其基本用法代码如下: Lock lock = new ReentrantLock(); //实例化锁 //lock.lock(); //上锁 boolean locked =

  • java ReentrantLock详解

    介绍 ReentrantLock称为重入锁,比内部锁synchonized拥有更强大的功能,它可中断.可定时.设置公平锁 [注]使用ReentrantLock时,一定要释放锁,一般释放放到finnal里写. 提供以下重要的方法 lock():获得锁,如果锁已被占用,则等待 lockInterruptibly():获得锁,但有限响应中断 unlock():释放锁 tryLock():尝试获取锁.如果获得,返回true:否则返回false tryLock(long time, TimeUnit un

  • Java线程安全解决方案(synchronized,ReentrantLock,Atomic)

    线程安全解决方案 synchronized,ReentrantLock,Atomic 使用场景描述 在实际开发过程中如果服务量,请求频繁,就会经常碰见并发,这时候不做处理就会出现很多非法数据.这时候就需要解决线程安全的问题,这时候就可以使用java当中的锁机制.常用有java关键synchronized.可重入锁ReentrantLock,还有并发包下的Atomic 或者Concurrent的安全类型. synchronized使用场景: 在资源竞争不是很激烈的情况下,偶尔出现并发,需要同步的情

  • java底层组合AQS实现类kReentrantLock源码解析

    目录 不啰嗦,我们直接开始! 引导语 上两小节我们学习了 AQS,本章我们就要来学习一下第一个 AQS 的实现类:ReentrantLock,看看其底层是如何组合 AQS ,实现了自己的那些功能. 本章的描述思路是先描述清楚 ReentrantLock 的构成组件,然后使用加锁和释放锁的方法把这些组件串起来. 1.类注释 ReentrantLock 中文我们习惯叫做可重入互斥锁,可重入的意思是同一个线程可以对同一个共享资源重复的加锁或释放锁,互斥就是 AQS 中的排它锁的意思,只允许一个线程获得

  • Java中ReentrantLock4种常见的坑

    目录 前言 Lock 简介 ReentrantLock 使用 ReentrantLock 中的坑 1.ReentrantLock 默认为非公平锁 2.在 finally 中释放锁 3.锁不能被释放多次 4.lock 不要放在 try 代码内 总结 前言 JDK 1.5 之前 synchronized 的性能是比较低的,但在 JDK 1.5 中,官方推出一个重量级功能 Lock,一举改变了 Java 中锁的格局.JDK 1.5 之前当我们谈到锁时,只能使用内置锁 synchronized,但如今我

  • 浅谈Java中几种常见的比较器的实现方法

    在Java中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题. 通常对象之间的比较可以从两个方面去看: 第一个方面:对象的地址是否一样,也就是是否引用自同一个对象.这种方式可以直接使用"=="来完成. 第二个方面:以对象的某一个属性的角度去比较. 从最新的JDK8而言,有三种实现对象比较的方法: 一.覆写Object类的equals()方法: 二.继承Comparable接口,并实现compareTo()方法: 三.定义一个单独的对象比较器,继承自Comparator接口

  • java中几种常见的排序算法总结

    目录 本节目标: [插入排序] [优化版] [希尔排序] [选择排序] [堆排序]  [冒泡排序] 介绍一个冒泡排序的优化方法:  [快速排序] [归并排序] [正文] [代码简介:]  [排序总结] 本节目标: :分析常见的比较排序算法基本原理及实现 :分析排序算法的性能分析 :分析Java中常用排序方法 1 排序 排序,就是使一串记录,按照其中某个或某些关键字的大小,递增或递减排列的操作. 平时的上下文中,提到排序 通常指排升序. 2 稳定性 两个相同的数据,如果经过排序后,排序算法能保证其

  • Java中七种排序算法总结分析

    目录 前言:对文章出现的一些名词进行解释 一.插入排序 1.基本思想 2.直接插入排序 3.希尔排序(缩小增量排序) 二.选择排序 1.基本思想 2.直接选择排序 3.堆排序 三.交换排序 1.基本思想 2.冒泡排序 3.快速排序(递归与非递归) 1.Hoare版 2.挖坑法 3.前后标记法(难理解) 4.快速排序优化 5.快速排序非递归 6.相关特性总结 四.归并排序(递归与非递归) 前言:对文章出现的一些名词进行解释 排序: 使一串记录,按照其中的某个或某些关键字的大小,递增或者递减排列起来

  • 详解Java中两种分页遍历的使用姿势

    在日常开发中,分页遍历迭代的场景可以说非常普遍了,比如扫表,每次捞100条数据,然后遍历这100条数据,依次执行某个业务逻辑:这100条执行完毕之后,再加载下一百条数据,直到扫描完毕 那么要实现上面这种分页迭代遍历的场景,我们可以怎么做呢 本文将介绍两种使用姿势 常规的使用方法 借助Iterator的使用姿势 1. 数据查询模拟 首先mock一个分页获取数据的逻辑,直接随机生成数据,并且控制最多返回三页 public static int cnt = 0; private static List

  • Java中几种常用数据库连接池的使用

    一.应用程序直接获取数据库连接的缺点 用户每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长.假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出.拓机.如下图所示: 二.使用数据库连接池优化程序性能 2.1.数据库连接池的基本概念 数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现的尤为突出.对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性

  • Java中4种经典限流算法讲解

    目录 限流是什么? 常见的限流算法 固定窗口限流算法 滑动窗口限流算法 漏桶算法 令牌桶算法 最近,我们的业务系统引入了Guava的RateLimiter限流组件,它是基于令牌桶算法实现的,而令牌桶是非常经典的限流算法.本文将跟大家一起学习几种经典的限流算法. 限流是什么? 维基百科的概念如下: In computer networks, rate limiting is used to control the rate of requests sent or received by a net

  • 一起聊聊Java中13种锁的实现方式

    目录 1.悲观锁 2.乐观锁 3.分布式锁 加锁 4.可重入锁 5.自旋锁 6.独享锁 7.共享锁 8.读锁/写锁 9.公平锁/非公平锁 10.可中断锁/不可中断锁 11.分段锁 12.锁升级(无锁|偏向锁|轻量级锁|重量级锁) 无锁 偏向锁 轻量级锁 重量级锁 13.锁优化技术(锁粗化.锁消除) 最近有很多小伙伴给我留言,分布式系统时代,线程并发,资源抢占,"锁" 慢慢变得很重要.那么常见的锁都有哪些? 今天Tom哥就和大家简单聊聊这个话题. 1.悲观锁 正如其名,它是指对数据修改时

  • Java中四种遍历List的方法总结(推荐)

    实例如下: package com.ietree.basic.collection.loop; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * List遍历 * * @author Dylan */ public class ListLoop { public static void main(String[] args) { // 初始化一个长度为10的ArrayList L

  • Java中八种基本数据类型的默认值

    通过一段代码来测试一下 8种基本数据类型的默认值 package dierge; public class Ceshi { int a; double b; boolean c; char d; float f; byte e; long h; short j; public static void main(String args[]){ Ceshi a=new Ceshi(); System.out.println("整型的默认值是:"+a.a); System.out.print

随机推荐