JDK中lock锁的机制,其底层是一种无锁的架构实现的,公平锁和非公平锁

news2025/4/4 11:13:57

简述JDK中lock锁的机制,其底层是一种无锁的架构实现的,是否知道其是如何实现的

synchronized与lock

lock是一个接口,而synchronized是在JVM层面实现的。synchronized释放锁有两种方式:

  1. 获取锁的线程执行完同步代码,释放锁 。
  2. 线程执行发生异常,jvm会让线程释放锁。

lock锁的释放,出现异常时必须在finally中释放锁,不然容易造成线程死锁。lock显式获取锁和释放锁,提供超时获取锁、可中断地获取锁。

synchronized是以隐式地获取和释放锁,synchronized无法中断一个正在等待获取锁的线程。

synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作。

具体的悲观锁和乐观锁的详细介绍请参考这篇文章[浅谈数据库乐观锁、悲观锁]

JDK5中增加了一个Lock接口实现类ReentrantLock.它不仅拥有和synchronized相同的并发性和内存语义,还多了锁投票,定时锁,等候和中断锁等.它们的性能在不同的情况下会有不同。

在资源竞争不是很激烈的情况下,synchronized的性能要由于ReentrantLock,但是在资源竞争很激烈的情况下,synchronized的性能会下降得非常快,而ReentrantLock的性能基本保持不变.

接下来我们会进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState

lock源码

在阅读源码的成长的过程中,有很多人会遇到很多困难,一个是源码太多,另一方面是源码看不懂。在阅读源码方面,我提供一些个人的建议:

  1. 第一个是抓主舍次,看源码的时候,很多人会发现源码太长太多,看不下去,这就要求我们抓住哪些是核心的方法,哪些是次要的方法。当舍去次要方法,就会发现代码精简和很多,会大大提高我们阅读源码的信心。
  2. 第二个是不要死扣,有人看源码会一行一行的死扣,当看到某一行看不懂,就一直停在那里死扣,知道看懂为止,其实很多时候,虽然看不懂代码,但是可以从变量名和方法名知道该代码的作用,java中都是见名知意的。

接下来进入阅读lock的源码部分,在lock的接口中,主要的方法如下:

public interface Lock {
    // 加锁
    void lock()// 尝试获取锁
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 解锁
    void unlock();
}

在lock接口的实现类中,最主要的就是ReentrantLock,来看看ReentrantLocklock()方法的源码:

    // 默认构造方法,非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }
 
    // 构造方法,公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
 
    // 加锁
    public void lock() {
        sync.lock();
    }

在初始化lock实例对象的时候,可以提供一个boolean的参数,也可以不提供该参数。提供该参数就是公平锁,不提供该参数就是非公平锁。

什么是非公平锁和公平锁呢?

非公平锁就是不按照线程先来后到的时间顺序进行竞争锁,后到的线程也能够获取到锁,公平锁就是按照线程先来后到的顺序进行获取锁,后到的线程只能等前面的线程都获取锁完毕才执行获取锁的操作,执行有序。

我们来看看lock()这个方法,这个有区分公平锁和非公平锁,这个两者的实现不同,先来看看公平锁,源码如下:

// 直接调用 acquire(1)
final void lock() {
     acquire(1);
 }

我们来看看acquire(1)的源码如下:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

这里的判断条件主要做两件事:

  1. 通关过该方法tryAcquire(arg)尝试的获取锁
  2. 若是没有获取到锁,通过该方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg)就将当前的线程加入到存储等待线程的队列中。

其中tryAcquire(arg)是尝试获取锁,这个方法是公平锁的核心之一,它的源码如下:

protected final boolean tryAcquire(int acquires) {
             // 获取当前线程 
            final Thread current = Thread.currentThread();
            // 获取当前线程拥有着的状态
            int c = getState();
            // 若为0,说明当前线程拥有着已经释放锁
            if (c == 0) {
                 // 判断线程队列中是否有,排在前面的线程等待着锁,若是没有设置线程的状态为1。
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    // 设置线程的拥有着为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
                // 若是当前的线程的锁的拥有者就是当前线程,可重入锁
            } else if (current == getExclusiveOwnerThread()) {
                // 执行状态值+1
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 设置status的值为nextc
                setState(nextc);
                return true;
            }
            return false;
        }

tryAcquire()方法中,主要是做了以下几件事:

  1. 判断当前线程的锁的拥有者的状态值是否为0,若为0,通过该方法hasQueuedPredecessors()再判断等待线程队列中,是否存在排在前面的线程。
  2. 若是没有通过该方法 compareAndSetState(0, acquires)设置当前的线程状态为1。
  3. 将线程拥有者设为当前线程setExclusiveOwnerThread(current)
  4. 若是当前线程的锁的拥有者的状态值不为0,说明当前的锁已经被占用,通过current == getExclusiveOwnerThread()判断锁的拥有者的线程,是否为当前线程,实现锁的可重入。
  5. 若是当前线程将线程的状态值+1,并更新状态值。

公平锁的tryAcquire(),实现的原理图如下:

在这里插入图片描述

我们来看看acquireQueued()方法,该方法是将线程加入等待的线程队列中,源码如下:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 死循环处理
            for (;;) {
                // 获取前置线程节点
                final Node p = node.predecessor();
                // 这里又尝试的去获取锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    // 直接return  interrupted
                    return interrupted;
                }
                // 在获取锁失败后,应该将线程Park(暂停)
                if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

acquireQueued()方法主要执行以下几件事:

  1. 死循环处理等待线程中的前置节点,并尝试获取锁,若是p == head && tryAcquire(arg),则跳出循环,即获取锁成功。
  2. 若是获取锁不成功shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()就会将线程暂停。

acquire(int arg)方法中,最后若是条件成立,执行下面的源码:

selfInterrupt();
// 实际执行的代码为
Thread.currentThread().interrupt();

即尝试获取锁失败,就会将锁加入等待的线程队列中,并让线程处于中断等待。公平锁lock()方法执行的原理图如下:

在这里插入图片描述

之所以画这些原理的的原因,是为后面写一个自己的锁做铺垫,因为你要实现和前人差不多的东西,你必须了解该东西执行的步骤,最后得出的结果,执行的过程是怎么样的。

有了流程图,在后面的实现自己的东西才能一步一步的进行。这也是阅读源码的必要之一。

lock()方法,其实在lock()方法中,已经包含了两方面:

  1. 锁方法lock()
  2. 尝试获取锁方法tryAquire()

接下来,我们来看一下unlock()方法的源码。

  public void unlock() {
        sync.release(1);
    }

直接调用release(1)方法,来看release方法源码如下:

    public final boolean release(int arg) {
       // 尝试释放当前节点
        if (tryRelease(arg)) {
            // 取出头节点
            Node h = head;
            if (h != null && h.waitStatus != 0)
                // 释放锁后要即使唤醒等待的线程来获取锁
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

通过调用tryRelease(arg),尝试释放当前节点,若是释放锁成功,就会获取的等待队列中的头节点,就会即使唤醒等待队列中的等待线程来获取锁。接下来看看tryRelease(arg)的源码如下:

// 尝试释放锁
 protected final boolean tryRelease(int releases) {
            // 将当前状态值-1
            int c = getState() - releases;
            // 判断当前线程是否是锁的拥有者,若不是直接抛出异常,非法操作,直接一点的解释就是,你都没有拥有锁,还来释放锁,这不是骗人的嘛
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //执行释放锁操作 1.若状态值=0   2.将当前的锁的拥有者设为null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 重新更新status的状态值
            setState(c);
            return free;
        }

总结上面的几个方法,unlock释放锁方法的执行原理图如下:

在这里插入图片描述

对于非公平锁与公平锁的区别,在非公平锁尝试获取锁中不会执行hasQueuedPredecessors()去判断是否队列中还有等待的前置节点线程。

如下面的非公平锁,尝试获取锁nonfairTryAcquire()源码如下:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 直接就将status-1,并不会判断是否还有前置线程在等待
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

以上就是公平锁和非公平锁的主要的核心方法的源码,接下来我们实现自己的一个锁,首先依据前面的分析中,要实现自己的锁,拥有的锁的核心属性如下:

  1. 状态值status,0为未占用锁,1未占用锁,并且是线程安全的。
  2. 等待线程队列,用于存放获取锁的等待线程。
  3. 当前线程的拥有者。

lock锁的核心的Api如下:

  1. lock方法
  2. trylock方法
  3. unlock方法

依据以上的核心思想来实现自己的锁,首先定义状态值status,使用的是AtomicInteger原子变量来存放状态值,实现该状态值的并发安全和可见性。定义如下:

// 线程的状态 0表示当前没有线程占用   1表示有线程占用
    AtomicInteger status =new AtomicInteger();

接下来定义等待线程队列,使用LinkedBlockingQueue队列来装线程,定义如下:

// 等待的线程
LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<Thread>();

最后的属性为当前锁的拥有者,直接就用Thread来封装,定义如下:

// 当前线程拥有者
Thread ownerThread =null;

接下来定义lock()方法,依据上面的源码分析,在lock方法中主要执行的几件事如下:

  1. 死循环的处理等待线程队列中的线程,知道获取锁成功,将该线程从队列中删除,跳出循环。
  2. 获取锁不成功,线程处于暂停等待。
    @Override
    public void lock() {
        // TODO Auto-generated method stub
        // 尝试获取锁
        if (!tryLock()) {
            // 获取锁失败,将锁加入等待的队列中
            waitersQueue.add(Thread.currentThread());
            // 死循环处理队列中的锁,不断的获取锁
            for (;;) {
                if (tryLock()) {
                    // 直到获取锁成功,将该线程从等待队列中删除
                    waitersQueue.poll();
                    // 直接返回
                    return;
                } else {
                    // 获取锁不成功,就直接暂停等待。
                    LockSupport.park();
                }
            }
        }
    }

然后是trylock方法,依据上面的源码分析,在trylock中主要执行的以下几件事:

  1. 判断当前拥有锁的线程的状态是否为0,为0,执行状态值+1,并将当前线程设置为锁拥有者。
  2. 实现锁可重入
    @Override
    public boolean tryLock() {
        // 判断是否有现成占用
        if (status.get()==0) {
            // 执行状态值加1
            if (status.compareAndSet(0, 1)) {
                // 将当前线程设置为锁拥有者
                ownerThread = Thread.currentThread();
                return true;
            } else if(ownerThread==Thread.currentThread())  {
                // 实现锁可重入
                status.set(status.get()+1);
            }
        }
        return false;
    }

最后就是unlock方法,依据上面的源码分析,在unlock中主要执行的事情如下:

  1. 判断当前线程是否是锁拥有者,若不是直接抛出异常。
  2. 判断状态值是否为0,并将锁拥有者清空,唤醒等待的线程。
    @Override
    public void unlock() {
        // TODO Auto-generated method stub
        // 判断当前线程是否是锁拥有者
        if (ownerThread!=Thread.currentThread()) {
            throw new RuntimeException("非法操作");
        }
        // 判断状态值是否为0
        if (status.decrementAndGet()==0) {
            // 清空锁拥有着
            ownerThread = null;
            // 从等待队列中获取前置线程
            Thread t = waitersQueue.peek();
            if (t!=null) {
               // 并立即唤醒该线程
                LockSupport.unpark(t);
            }
        }
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1279743.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

安装selenium+chrome详解

1、创建yaml文件 创建yaml文件,命名为:docker-compose-chrome.yaml,具体内容如下: version: "3.9" services:spiderdriver:image: selenium/standalone-chrome:114.0restart: alwayshostname: spiderdrivercontainer_name: spiderdriverdeploy:resources:limit…

【STM32F103】GPIO通用输入输出口

GPIO 简介 GPIO&#xff08;General Purpose Input Output&#xff09;通用输入输出口是微控制器&#xff08;MCU&#xff09;必备的片上外设&#xff0c;可以实现微控制器与外部设备的数字交换。 STM32F103系列的芯片最多可以提供112个多功能双向IO引脚&#xff0c;但是显然…

8 9 固件库

文章目录 8.1 什么是固件库8.2 什么时候使用8.3 封装注意点8.3.1 fsl 8.4 GPIO库函数定义8.4.1 初始化结构体 gpio_pin_config_t8.4.2 定义引脚模式的枚举类型8.4.3 初始化结构体8.4.4 定义中断配置函数8.4.5 定义GPIO初始化函数8.4.6 使用函数控制GPIO8.4.7 定义 IOMUXC 外设控…

开源软件license介绍与检测

开源License介绍 通俗来讲&#xff0c;开源许可证就是一种允许软件使用者在一定条件内按照需要自由使用和修改软件及其源代码的的法律条款。借此条款&#xff0c;软件作者可以将这些权利许可给使用者&#xff0c;并告知使用限制。这些许可条款可以由个人、商业公司或非赢利组织…

Session 与 JWT 的对决:谁是身份验证的王者? (下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

vue3请求代理proxy中pathRewrite失效

问题引入 在vue3配置请求代理proxy的时候pathRewrite失效。 有这样一个例子&#xff0c;作用是为了把所有以/api开头的请求代理到后端的路径和端口上&#xff0c;在vue.config.js配置文件中 设置了代理跨域和默认端口。但是重新运行之后发现端口是改了&#xff0c;但是路径仍然…

maven篇---第一篇

系列文章目录 文章目录 系列文章目录前言一、什么是maven?二、Maven能为我们解决什么问题?三、说说maven有什么优缺点?前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码…

CyclicBarrier实战应用——批量数据多线程协调异步处理(主线程执行事务回滚)

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; CCyclicBarrier实战应用——批量数据多线程协调异步处理(主线程执行事务…

关于抓取明文密码的探究

基础知识 SSP&#xff08;Security Support Provider&#xff09;是windows操作系统安全机制的提供者。简单的说&#xff0c;SSP就是DLL文件&#xff0c;主要用于windows操作系统的身份认证功能&#xff0c;例如NTLM、Kerberos、Negotiate、Secure Channel&#xff08;Schanne…

倒计时 1 天,2023 IoTDB 用户大会期待与您相见!

终于&#xff01;就在明天&#xff0c;2023 IoTDB 用户大会即将在北京与大家见面&#xff01; 这场筹备已久的盛会&#xff0c;汇集了超 20 位大咖嘉宾带来的精彩议题&#xff0c;届时来自美国国家工程院、清华大学软件学院的产业大拿&#xff0c;与能源电力、钢铁冶炼、城轨运…

数据结构:堆的实现思路

我们之前写过堆的实现代码&#xff1a;数据结构&#xff1a;堆的实现-CSDN博客 这篇文章我们了解一下堆到底是如何实现的 1.堆向下调整算法 现在我们给出一个数组&#xff0c;逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆 向下调…

CyclicBarrier实战应用——批量数据多线程协调异步处理(子线程执行事务回滚)

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; CountDownLatch实战应用——批量数据多线程协调异步处理(子线程执行事务…

时序预测 | Python实现LSTM长短期记忆神经网络时间序列预测(多图,多指标)

时序预测 | Python实现LSTM长短期记忆神经网络时间序列预测(多图,多指标) 目录 时序预测 | Python实现LSTM长短期记忆神经网络时间序列预测(多图,多指标)预测效果基本介绍环境准备程序设计参考资料预测效果 基本介绍 LSTM是一种递归神经网络(RNN)的变体

JVM之基本概念(一)

(1) 基本概念&#xff1a; JVM 是可运行 Java 代码的假想计算机 &#xff0c;包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收&#xff0c;堆 和 一个存储方法域。JVM 是运行在操作系统之上的&#xff0c;它与硬件没有直接的交互。 (2) 运行过程&#xff1a; 我们都…

2 文本分类入门:TextCNN

论文链接&#xff1a;https://arxiv.org/pdf/1408.5882.pdf TextCNN 是一种用于文本分类的卷积神经网络模型。它在卷积神经网络的基础上进行了一些修改&#xff0c;以适应文本数据的特点。 TextCNN 的主要思想是使用一维卷积层来提取文本中的局部特征&#xff0c;并通过池化操…

使用Python免费调用通义千问大模型

Qwen-72b开源模型 模型的主要用途是预测或描述一个系统或现象的行为模式。它可以帮助人们更好地理解这个系统或现象&#xff0c;例如预测股市变化、天气预报、地震预警、交通流量等。模型也常用于设计和优化产品和工艺。在科学研究中&#xff0c;模型也是一种方法&#xff0c;用…

Stm32F401RCT6内部FLASH数据擦除读写方法

Stm32F401RCT6内部FLASH数据的分区和F103的已经不一样了&#xff0c;读写格式化的方法网上内容不多&#xff0c;自己摸索了一下&#xff0c;基本可以&#xff0c;还存在一个问题 读取&#xff1a; uint16_t f[5];uint8_t tx[10];f[0] *(volatile uint16_t*)0x08020000; //ST…

同旺科技 USB TO SPI / I2C --- 调试W5500_读写网关地址

所需设备&#xff1a; 内附链接 1、USB转SPI_I2C适配器(专业版); 首先&#xff0c;连接W5500模块与同旺科技USB TO SPI / I2C适配器&#xff0c;如下图&#xff1a; 这里的网关地址设置为192.168.1.1 先将网关地址写入寄存器&#xff0c;然后再读取出来&#xff1a;

【SpringBoot】讲清楚日志文件lombok

文章目录 前言一、日志是什么&#xff1f;二、⽇志怎么⽤&#xff1f;三.自定义打印日志3.1在程序中得到日志对象3.2使用日志打印对象 四.⽇志级别4.1日志级别有什么用4.2 ⽇志级别的分类与使⽤ 五.日志持久化六.lombok6.1添加lobok依赖注意&#xff1a;使⽤ Slf4j 注解&#x…

Linux命令与shell脚本编程大全【读书笔记 + 思考总结】

Linux命令与shell脚本编程大全 第 1 章 初识Linux shellLinux的组成及关系结构图是什么&#xff1f;Linux系统内核的作用是什么&#xff1f;内核的主要功能是什么&#xff1f;&#xff08;4点&#xff09;物理内存和虚拟内存是什么关系&#xff1f;内核如何实现虚拟内存&#x…