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

简述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()) {// 执行状态值+1int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");// 设置status的值为nextcsetState(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 GCfailed = false;// 直接return  interruptedreturn 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) {// 将当前状态值-1int c = getState() - releases;// 判断当前线程是否是锁的拥有者,若不是直接抛出异常,非法操作,直接一点的解释就是,你都没有拥有锁,还来释放锁,这不是骗人的嘛if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;//执行释放锁操作 1.若状态值=0   2.将当前的锁的拥有者设为nullif (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) // overflowthrow 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. 获取锁不成功,线程处于暂停等待。
    @Overridepublic 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. 实现锁可重入
    @Overridepublic boolean tryLock() {// 判断是否有现成占用if (status.get()==0) {// 执行状态值加1if (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,并将锁拥有者清空,唤醒等待的线程。
    @Overridepublic void unlock() {// TODO Auto-generated method stub// 判断当前线程是否是锁拥有者if (ownerThread!=Thread.currentThread()) {throw new RuntimeException("非法操作");}// 判断状态值是否为0if (status.decrementAndGet()==0) {// 清空锁拥有着ownerThread = null;// 从等待队列中获取前置线程Thread t = waitersQueue.peek();if (t!=null) {// 并立即唤醒该线程LockSupport.unpark(t);}}}

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

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

相关文章

Elasticsearch一些函数查询

1. 根据价格分组统计数量&#xff0c;每组区间为2000&#xff0c; filter_pathaggregations 设置查询结果只展示函数结果 也有date_histogram函数根据日期分组等等 GET order/_search?filter_pathaggregations {"aggs": {"hist_price": {"histogr…

神经网络 代价函数

神经网络 代价函数 首先引入一些便于稍后讨论的新标记方法&#xff1a; 假设神经网络的训练样本有 m m m个&#xff0c;每个包含一组输入 x x x和一组输出信号 y y y&#xff0c; L L L表示神经网络层数&#xff0c; S I S_I SI​表示每层的neuron个数( S l S_l Sl​表示输出…

使用idea如何快速的搭建ssm的开发环境

文章目录 唠嗑部分言归正传1、打开idea&#xff0c;点击新建项目2、填写信息3、找到pom.xml先添加springboot父依赖4、添加其他依赖5、编写启动类、配置文件6、连接创建数据库、创建案例表7、安装MybatisX插件8、逆向工程9、编写controller10、启动项目、测试 结语 唠嗑部分 小…

MIT线性代数笔记-第21讲-特征值,特征向量

目录 21.特征值&#xff0c;特征向量打赏 21.特征值&#xff0c;特征向量 对于一个方阵 A A A&#xff0c;若 A x ⃗ λ x ⃗ A \vec{x} \lambda \vec{x} Ax λx &#xff0c;即 A x ⃗ A \vec{x} Ax 平行于 x ⃗ \vec{x} x &#xff0c;那么 λ \lambda λ是 A A A的特征值…

怎么防止死锁

目录 一、死锁的概念 1、互斥条件 2、 占有和等待条件 3、非抢占条件&#xff08;No Preemption&#xff09;&#xff1a; 4、环路等待条件&#xff08;Circular Wait&#xff09;&#xff1a; 二、防止死锁 死锁预防&#xff08;Deadlock Prevention&#xff09;&#xf…

内网穿透工具获取一个公网ip

下载地址&#xff1a;点击即可下载很简单 然后将他复制到上面的命令行窗口直接回车

LeetCode1423. Maximum Points You Can Obtain from Cards

文章目录 一、题目二、题解 一、题目 There are several cards arranged in a row, and each card has an associated number of points. The points are given in the integer array cardPoints. In one step, you can take one card from the beginning or from the end of…

Programming Contest 2023(AtCoder Beginner Contest 331)E题 Set Meal --- 题解

目录 E题 Set Meal 题目大意&#xff1a; 思路&#xff1a;&#xff08;在求最大时和最小时&#xff0c;如果要求查询代价较小时&#xff0c;可以使用优先队列&#xff09; 代码&#xff1a; E题 Set Meal E - Set Meal (atcoder.jp) 题目大意&#xff1a; 先给出n个…

知识图谱07——图片中表格开源ocr识别

对比了多种ocr识别算法,最终选择了百度paddle官方的ocr算法 在所在的虚拟环境下运行 pip install paddleocr --userfrom paddleocr import PaddleOCR import os import csv# 创建 PaddleOCR 对象 ocr = PaddleOCR(use_gpu=True) # 无gpu时选择False# 指定图片文件夹和结果保…

编程怎么学才能快速入门,分享一款中文编程工具快速学习编程思路,中文编程工具之边条主控菜单构件简介

编程怎么学才能快速入门&#xff0c;分享一款中文编程工具快速学习编程思路&#xff0c;中文编程工具之边条主控菜单构件简介 一、前言 零基础自学编程&#xff0c;中文编程工具下载&#xff0c;中文编程工具构件之扩展系统菜单构件教程编程系统化教程链接https://jywxz.blog…

SALib敏感性分析入门实践笔记

1. 敏感性分析 敏感性分析是指从定量分析的角度研究有关因素发生某种变化对某一个或一组关键指标影响程度的一种不确定分析技术。 其实质是通过逐一改变相关变量数值的方法来解释关键指标受这些因素变动影响大小的规律。 敏感性因素一般可选择主要参数&#xff08;如销售收入、…

autojs-ui悬浮按钮模板

注释很详细&#xff0c;直接上代码 涵盖很多常用知识点&#xff0c;也可当知识点看 运行效果长这样&#xff1a; 开始按钮相当于开关&#xff0c;按钮内容会随点击变换控制台按钮可让运行框显示或隐藏退出按钮退出程序并在3s后关闭运行框只需在对应函数内添加需要实现的内容即可…

云服务器网站遇到HTTPS CC攻击了,有什么办法吗?

目前网站使用SSL证书&#xff0c;用443端口的情况非常普遍&#xff0c;大多数网站&#xff0c;用户都是会选择用证书&#xff0c;这样不仅可以提供数据传输的安全性&#xff0c;增强用户信任和品牌形象&#xff0c;还可以提高网站在搜索结果中的排名&#xff0c;从而增加流量和…

算法复习,数据结构 ,算法特性,冒泡法动态演示,复杂度,辗转相除法*,寻找最大公因数

算法复习 知识点 1. 程序 数据结构 算法 2. 算法&#xff1a; 求解问题的策略数据结构&#xff1a;问题的数学模型程序&#xff1a;微计算机处理问题编制的一组指令 3. **特性 ** 有穷性&#xff1a;算法在执行有穷步后能结束确定性&#xff1a;每一指令有确切的含义&a…

2023年12月4日支付宝蚂蚁庄园小课堂小鸡宝宝考考你今日正确答案是什么?

问题&#xff1a;你知道电杆上安装的“小风车”有什么用途吗&#xff1f; 答案&#xff1a;防止鸟类筑巢 解析&#xff1a;小风车一般做成橙色&#xff0c;因为橙色是一种可令野鸟产生恐慌感的颜色&#xff1b;小风车在转动时&#xff0c;会发出令野鸟害怕的噪声&#xff1b;…

LLM推理部署(四):一个用于训练、部署和评估基于大型语言模型的聊天机器人的开放平台FastChat

FastChat是用于对话机器人模型训练、部署、评估的开放平台。体验地址为&#xff1a;https://chat.lmsys.org/&#xff0c;该体验平台主要是为了收集人类的真实反馈&#xff0c;目前已经支持30多种大模型&#xff0c;已经收到500万的请求&#xff0c;收集了10万调人类对比大模型…

好题记录:

好题记录&#xff1a; 1:2:3&#xff1a;三级目录 1: 下面代码的结果是&#xff1a;&#xff08; &#xff09; 下面代码的结果是&#xff1a;&#xff08; &#xff09;#include <stdio.h> int main() {int arr[] {1,2,3,4,5};short *p (short*)arr;int i …

138. 随机链表的复制 --力扣 --JAVA

题目 给你一个长度为 n 的链表&#xff0c;每个节点包含一个额外增加的随机指针 random &#xff0c;该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成&#xff0c;其中每个新节点的值都设为其对应的原节点的值。新节点…

4个解决特定的任务的Pandas高效代码

在本文中&#xff0c;我将分享4个在一行代码中完成的Pandas操作。这些操作可以有效地解决特定的任务&#xff0c;并以一种好的方式给出结果。 从列表中创建字典 我有一份商品清单&#xff0c;我想看看它们的分布情况。更具体地说&#xff1a;希望得到唯一值以及它们在列表中出…

力扣labuladong——一刷day64

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、力扣515. 在每个树行中找最大值二、力扣637. 二叉树的层平均值三、力扣958. 二叉树的完全性检验 前言 二叉树大部分题目都可以用递归的算法解决&#xff0c…