详述重入锁-ReentrantLock

简介: 详述重入锁-ReentrantLock

有经典,有干货,微信搜索【李子捌】关注这个每日更新的程序员。



简介

重入锁ReentrantLock指的是支持同一个线程对资源的重复加锁。ReentrantLock中有公平锁和非公平锁的两种实现。


synchronized

synchronized关键字支持隐式的重入;当一个线程获取到锁时,是支持这个线程多次获取这个锁的,不会出现自己阻塞自己的情况,并且我们开发过程中对于synchronized关键字也不需要关心锁的释放。举个递归的例子我们来看synchronized关键字对锁的重入。


代码示例

image.pngReentrantLock

ReentrantLock基于AQS和Lock来实现的,那如果是我们ReentrantLock要实现可重入,需要解决和实现如下两个问题:


同一个线程多次获取锁,则需要判断当前来获取锁的线程和占有锁的线程是否为同一个线程

多次获取锁,则需要多次释放这个锁,可以通过一个计数器累加和自减来记录锁的重复获取与释放

ReentrantLock中有两种重入锁的实现


NonfairSync-非公平锁

FairSync-公平锁

公平锁和非公平锁的本质区别就在于,获取锁的顺序是否符合FIFO,对于公平锁来说先加入同步队列等待的线程,必将会先获取到同步状态(锁),对于非公平锁来说,获取到锁的顺序不确定。


简单使用

在进行源码分析之前,先来看看ReentrantLock是如何使用的,ReentrantLock的使用非常简单,示例代码如下:

image.pngimage.pngSync—ReentrantLock组合的自定义同步器抽象

/**
  * ReentrantLock内部类Sync,也是其内部组合实现的自定义同步器的抽象
  */
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;
    /**
      * 定义获取锁的抽象方法,由NonfairSync和FairSync去实现各自获取锁的方式
      */
    abstract void lock();
    /**
      * NonfairSync中tryAcquire调用的方法
      */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        // 如果当前共享状态未被其他线程占用
        if (c == 0) {
            // 尝试通过CAS占有当前共享状态
            if (compareAndSetState(0, acquires)) {
                // 设置共享状态持有线程为当前线程
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 如果共享状态已被占用,则判断当前占用共享状态的线程是否就是当前线程
        else if (current == getExclusiveOwnerThread()) {
            // 如果是则自增获取次数,设值state
            int nextc = c + acquires;
            if (nextc < 0) 
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    /**
     *  释放共享状态
     */
    protected final boolean tryRelease(int releases) {
        // 计算减少后的值state
        int c = getState() - releases;
        // 判断当前线程和持有共享状态的线程是否是同一个线程,不是则抛出异常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        // 如果state递减后的值为0了,表示线程释放完共享状态,需要情况持有共享状态的线程变量
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        // 设置state
        setState(c);
        return free;
    }
    /**
     *  判断占用共享状态的线程是否是当前线程
     */
    protected final boolean isHeldExclusively() {
        return getExclusiveOwnerThread() == Thread.currentThread();
    }
    /**
     *  如果state不为0,则获取共享状态的持有线程,否则返回null
     */
    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }
    /**
     * 如果当前线程持有共享状态,则返回state,否则返回0
     */
    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0;
    }
    /**
     *  判断当前共享状态是否被持有
     */
    final boolean isLocked() {
        return getState() != 0;
    }
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
    final ConditionObject newCondition() {
        return new ConditionObject();
    }
}

image.png

FairSync源码分析
/**
  * 公平锁的代码实现
  */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    /**
     *  调用AQS中的acquire(int arg)尝试获取同步状态,失败则加入等待队列,自旋获取共享状态
     */
    final void lock() {
        acquire(1);
    }
    /**
      * 公平锁和非公平锁的主要区别在于此方法
      */
    protected final boolean tryAcquire(int acquires) {
        // 获取到当前线程
        final Thread current = Thread.currentThread();
        // 获取当前同步状态
        int c = getState();
        // 如果同步状态为0,则说明当前同步状态已完全释放
        if (c == 0) {
            // 1、hasQueuedPredecessors判断当前节点是否存在前驱节点
            // 2、如果不存在则CAS设置state的值
            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;
    }
}

FairSync能够顺序的获取共享状态,也就是保证加入同步队列的顺序和获取到同步状态的顺序一致,依靠的是hasQueuedPredecessors()这个判断当前节点是否存在前驱节点的判断。

NonfairSync和FairSync区别示例代码
package com.lizba.p6;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
/**
 * <p>
 *      公平和非公平锁测试
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/21 23:33
 */
public class FairAndUnfairTest {
    /** 定义公平锁 */
    private static Lock fairLock = new ReentrantLockCustomize(true);
    /** 定义非公平锁 */
    private static Lock unfairLock = new ReentrantLockCustomize(false);
    /**
     * 测试公平锁和非公平锁
     * @param lock
     */
    private static void testFairAndUnfairLock(Lock lock) {
        for (int i = 1; i <= 5; i++) {
            new Job(lock, ""+i).start();
        }
    }
    /**
     * 定义线程实现,打印当前线程和等待队列中的线程
     */
    private static class Job extends Thread {
        private Lock lock;
        public Job(Lock lock,String name) {
            this.lock = lock;
            setName(name);
        }
        @Override
        public void run() {
      // 通过两次输出,来判断是否与队列中一致
            for (int i = 0; i < 2; i++) {
                lock.lock();
                try {
                    System.out.println("获取锁的线程:" + Thread.currentThread().getName());
                    System.out.println("同步队列中的线程:" + ((ReentrantLockCustomize)lock).getQueuedThreads().stream().map(t -> t.getName()).collect(Collectors.joining(",")));
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    /**
     * 自定义可重入锁,主要新增getQueuedThreads()方法,用于获取等待队列中的线程
     */
    private static class ReentrantLockCustomize extends ReentrantLock {
        public ReentrantLockCustomize(boolean fair) {
            super(fair);
        }
        /**
         * 返回正在等待获取锁的线程列表,获取的实现列表逆序输出,反转后则为FIFO队列的原本顺序
         *
         * @return 等待队列中的线程顺序集合
         */
        public Collection<Thread> getQueuedThreads() {
            List<Thread> ts = new ArrayList<>(super.getQueuedThreads());
            Collections.reverse(ts);
            return ts;
        }
    }
}

image.pngimage.png存在问题:


非公平锁很明显存在线程“饥饿”问题,也就是一个线程获取到锁后会继续的再次获取到锁的可能性比较大,导致其他线程等待时间较长,那么为何ReentrantLock还有继续设置其为默认实现呢?这个主要原因是,公平锁会带来大量的线程切换的开销,而非公平锁虽然可能会导致线程“饥饿”问题,但是其吞吐量是远远大于公平锁的,相比之下非公平锁优势更大。


目录
相关文章
|
6月前
|
存储 SQL 关系型数据库
【MySQL】索引核心:联合索引最左前缀匹配原则、索引失效场景、索引设计原则
本文系统梳理MySQL索引核心知识:深入解析B+树原理、最左前缀匹配规则(含联合索引使用与失效边界)、11类高频索引失效场景(函数、类型转换、LIKE、OR等),并给出索引设计四大原则——高选择性、覆盖优化、顺序合理、避免冗余,助力高效查询与面试通关。
|
数据库 数据库管理 索引
索引在提高查询性能方面的优势体现在哪些方面?
索引在提高查询性能方面具有多方面的显著优势
592 65
|
Java 编译器 C++
【Java基础面试一】、为什么Java代码可以实现一次编写、到处运行?
这篇文章解释了Java能够实现“一次编写,到处运行”的原因,主要归功于Java虚拟机(JVM),它能够在不同平台上将Java源代码编译成的字节码转换成对应平台的机器码,实现跨平台运行。
【Java基础面试一】、为什么Java代码可以实现一次编写、到处运行?
|
Java
在Java中,接口之间可以继承吗?
接口继承是一种重要的机制,它允许一个接口从另一个或多个接口继承方法和常量。
1526 60
|
SQL 关系型数据库 MySQL
SQL中,可以使用 `ORDER BY` 子句来实现排序功能
【10月更文挑战第26天】SQL中,可以使用 `ORDER BY` 子句来实现排序功能
2027 6
|
JavaScript 前端开发 安全
跨域解决方案有哪些?
本文介绍了多种跨域解决方案,包括JSONP、CORS、postMessage、WebSocket、document.domain+iframe、window.name、location.hash、Node.js代理、Nginx代理和CORS Anywhere。每种方法都有其适用场景和优缺点,如JSONP简单但只支持GET请求,CORS安全但兼容性稍差,WebSocket适用于实时通信但需服务器支持。开发者可根据具体需求选择合适的跨域方案。
835 3
跨域解决方案有哪些?
|
负载均衡 监控 Java
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
34209 8
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
|
负载均衡 安全 Java
微服务 Gateway 使用详解
网关(Gateway)是连接不同网络并进行数据转发的关键组件。在互联网中,路由器常作为默认网关;在现代操作系统中,网关指本地网络上转发数据包的设备。Spring Cloud Gateway是一款基于Spring Framework的API网关,具备反向代理、高性能、负载均衡、安全控制、限流熔断、日志监控等功能。通过简单配置即可实现请求路由和转发,适用于微服务架构中的集中控制、解耦客户端与服务、自动服务发现等场景,提升系统安全性与可扩展性。
2475 4
|
缓存 安全 Java
Java中 final、finally、finalize 有什么区别?
本文详细阐述了Java中`final`、`finally`和`finalize`的区别:`final`用于修饰类、方法和变量以表示不可变性;`finally`是用于确保在`try-catch`结构中无论是否发生异常都能执行的代码块;而`finalize`是`Object`类的方法,用于在对象被垃圾回收前执行清理工作,但在JDK 9中已被标记为弃用。
864 0
Java中 final、finally、finalize 有什么区别?
|
网络协议 算法 网络性能优化
TCP和UDP有什么区别?
TCP(传输控制协议)和 UDP(用户数据报协议)是互联网协议族(TCP/IP)中的两种核心传输层协议,它们在数据传输的方式和特性上有显著的区别。
1706 9