当前位置: 首页 > news >正文

JavaEE——锁相关

在开发过程中,如果需要开发者自主实现一把锁,就必须了解锁策略和锁的实现原理。

目录

锁策略

乐观锁和悲观锁

互斥锁和读写锁

轻量级锁和重量级锁

自旋锁和挂起等待锁

公平锁和非公平锁

可重入锁和不可重入锁

死锁

发生死锁的必要条件

synchronized锁

synchronized的锁升级

CAS指令

编译器+JVM的其他优化

锁消除

锁粗化

ReentrantLock锁

ReentrantLock的用法

synchronized与ReentrantLock的区别


锁策略

常见的锁策略有:

  1. 乐观锁和悲观锁
  2. 互斥锁和读写锁
  3. 轻量级锁和重量级锁
  4. 自旋锁和挂起等待锁
  5. 公平锁和非公平锁
  6. 可重入锁和不可重入锁

乐观锁和悲观锁

就像名字一样,乐观锁假设一般情况下数据不会发生冲突,只有在修改数据操作时,才会检测数据是否发生冲突,一旦发生冲突,返回错误信息,交给用户做决定;而悲观锁假设每次数据操作时都会发生冲突,每次对数据进行操作时都进行加锁,这时别的线程就无法修改同一个数据了。

乐观锁虽然会造成数据冲突,但是效率高;而悲观锁避免了数据冲突,却降低了效率。因此在选择使用乐观锁还是悲观锁时,需要估计数据冲突的概率,冲突概率小就使用乐观锁,反之使用悲观锁。

互斥锁和读写锁

互斥锁是一种独占锁,同一时间只允许一个线程访问该对象,无论读写;而读写锁则区分读者和写者,同一时间内只允许一个写者,但是允许多个读者同时读对象。synchronized是一种互斥锁,读写不分开。

Java 标准库提供了 ReentrantReadWriteLock 类,实现了读写锁:

//读写锁的使用
public class ThreadDemo {private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();//读写锁public static void read() {lock.readLock().lock();//读加锁try {System.out.println(Thread.currentThread().getName() + "读操作");Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "读取完毕");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.readLock().unlock();//读解锁}}public static void write() {lock.writeLock().lock();//写加锁try {System.out.println(Thread.currentThread().getName() + "写操作");Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "写入完毕");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.writeLock().unlock();//写解锁}}public static void main(String[] args) {new Thread(() -> write()).start();new Thread(() -> read()).start();}
}

轻量级锁和重量级锁

当涉及到大量的内核态用户态切换时,需要用到重量级锁;当涉及到很少的内核态操作而大多是用户态操作时,往往只用到轻量级锁。轻量级锁更加高效,往往在加锁解锁频繁情况下使用;重量级锁的操作成本较高,不轻易使用。轻量级锁往往频繁使用,因此通常也是乐观锁;而重量级锁也往往是悲观锁。

自旋锁和挂起等待锁

当多个对象竞争同一把锁时,就会出现加锁失败的情况,没有抢到锁的对象因为采取的措施不同而分为自旋锁和挂起等待锁。自旋锁指的是加锁失败时,不释放CPU资源,而是进入空循环,直到获取到锁才会退出循环;挂起等待锁则在加锁失败时释放CPU资源,等到锁释放后再去竞争锁。

自旋锁是一种极其耗费CPU资源的手段,但在某些情况下却不得不使用。

自旋锁伪代码:

while (竞争锁 == 失败) {}

公平锁和非公平锁

公平锁指的是遵循“先来后到”原则,竞争锁失败的对象有顺序地进入等待队列,释放锁后再按顺序获取锁;非公平锁则不遵循这个原则,竞争锁失败的对象不管先后都处于同一起跑线,锁释放后同时竞争锁。

比如说线程A、B、C三个线程,A先获取到锁,B首先进入等待,然后是C。锁释放后,如果是公平锁,则B获取到锁,而如果是非公平锁,则B和C同时竞争释放的锁。

可重入锁和不可重入锁

可重入锁指的是同一线程可以重复获取锁,而不会发生死锁的现象;不可重入锁指的是同一线程不能两次获得同一把锁,否则发生死锁现象。

当线程第一次加锁时,可以正常加锁;第二次加锁时,由于线程已被锁,只能进入阻塞等待,而解除阻塞等待只能先解锁,要解锁就要解除阻塞等待,这种情况就称为死锁。就好比家钥匙落在车里,车钥匙落在家里的情况。

可重入锁的实现逻辑是在加锁前先进行判断识别,如果识别到该锁和第一次加锁是同一把锁,则不进行加锁。

死锁

一个线程一把锁,多次加锁,可重入锁不死锁,不可重入锁死锁。但是,多个线程多把锁,可重入锁也会导致死锁现象:

假如t1线程拿到lock1,t2线程拿到lock2,这时t1线程拿不到lock2就只能阻塞等待,但同样t2线程也拿不到lock1,因此也只能阻塞等待,这就发生了死锁。

线程越多,锁越多,就越容易发生死锁现象。

发生死锁的必要条件

发生死锁有四个必要条件,缺一不可,分别是:互斥条件请求和保持条件不剥夺条件循环等待条件

  • 互斥条件:一个线程拿到一把锁后,其他线程不能使用。
  • 请求和保持条件:线程在阻塞等待时,不会放弃已获取的资源。
  • 不剥夺条件:一个线程在获取锁以后,只能自己使用完释放,不可被提前抢占。
  • 循环等待条件:每个线程都在等待获取下一个线程占有的资源。

两个线程发生死锁现象的特点是互不相让,你占有了我需要的资源,我占有了你需要的资源,都想让对方先释放资源。而多个线程发生死锁现象通常会形成循环,A需要的资源被B占有,B需要的资源被C占有,C需要的资源被D占有,D需要的资源又被A占有。

为了解决这种情况,通常的做法是,针对锁进行编号,每次加锁按照顺序加:

例如上述死锁情况,只需要约定第一层加lock1,第二层加lock2即可:

synchronized锁

synchronized是JVM基于操作系统提供的mutex(互斥锁)实现的。synchronized既是乐观锁也是悲观锁,既是重量级锁也是轻量级锁,为轻量级锁时大概率也是自旋锁。synchronized同时也是非公平锁和可重入锁。

synchronized的锁升级

synchronized之所以可以同时是这么多种锁,主要在于synchronized的工作流程是基于锁升级实现的:

  1. 偏向锁:加偏向锁也是无锁状态,只是一种偏向标记,记录了当前线程,如果后续不存在锁竞争,则处于无锁状态,当后续存在锁竞争时可以第一时间加锁。
  2. 轻量级锁:当存在锁竞争时,由偏向锁升级为轻量级锁,轻量级锁通常也是自旋锁,通过CAS指令实现,CAS检测更新内存,更新成功则加锁成功,停止自旋,反之继续空转,直到更新成功。(这里的自旋锁是自适应的,如果长时间没有获取到锁,也会停止自旋)

伪代码实现:

public class spinLock {
private Thead cur = null;//代表当前锁对象没有被持有

public void lock() {
//通过CAS判断当前锁对象是否被占有
//this.cur == null代表该锁没有被占用,可以被获取到
//当前锁对象尝试对Thread.currentThread()加锁
while (!CAS(this.cur, null, Thread.currentThread())) {
}
}
public void unlock() {
this.cur = null;//置为null代表该锁没有被占用,也就是锁被释放
}
}
  1. 重量级锁:当锁竞争比较频繁时,自旋锁会耗费大量的CPU资源,因此轻量级锁会升级为重量级锁,重量级锁需要用到内核的mutex锁,在内核态判断锁是否被占用,没有则加锁成功并切换回用户态;反之则加锁失败,挂起等待,直到锁被释放再重新竞争。

JVM实现的synchronized只能发生锁升级,目前还不能实现降级,一旦锁升级为重量级,就不可能再降级为轻量级。

CAS指令

CAS(Compare and Swap)指令指的是一种通过硬件实现并发安全的常用技术,意为“比较和交换”,CAS是原子的,其实现步骤不可拆分,实现原理如下:

//address代表需要更新的内存,oldValue为旧值,newValue为需要更改的新值
boolean CAS(address, oldValue, newValue) {
if (&address == prevValue) {//&address意为获取到内存address处存放的值
&address = newValue;
return true;
}
return false;
}

注意上述过程是一步完成的,不受多线程的抢占式执行影响。

基于CAS指令的原子性,通常用来作为实现锁的底层原理,以及可以用来进行无锁并发编程。

编译器+JVM的其他优化

通过编译器+JVM还可以实现synchronized的锁消除锁粗化。

锁消除

在单线程环境下使用synchronized时,并不会真的加锁。Java源码中有很多方法的实现都会提供无锁版本和加锁版本,例如StringBulider类和StringBuffer类的区别就在于StringBulider是无锁版本,StringBuffer是加锁版本。当我们在单线程使用StringBuffer时,就会进行锁消除的优化。

锁粗化

当出现多次加锁时,编译器+JVM就会进行锁粗化的优化。锁粗化的是锁的粒度。锁的粒度是指锁定资源的细化程度。锁的粒度越大,则并发性越低,开销越大;粒度越小,并发性越高,开销越小。

虽然锁的粒度越细,开销越小。但是频繁加锁解锁的开销有时会更高。比如你妈妈让你去超市买东西A、B、C,你有两种方式:

方式一、去超市,买A,回家;去超市,买B,回家;去超市,买C,回家。

方式二、去超市,买A、B、C,回家。

显然呢,方式一是一个非常弱智的做法,明明可以一次性做完,却非得分成三次,吃力不讨好。

编程也同理,对于某些操作,如果需要频繁的加锁,编译器+JVM就会优化为只加锁一次,执行完所有操作后再解锁。

ReentrantLock锁

Java中虽然synchronized锁已经可以解决开发中的大部分需要加锁的场景,但是synchronized锁是一个非公平锁,与之相对的,Java源码也实现了ReentrantLock来补足这方面的缺点。

ReentrantLock为可重入锁,但是可以通过构造方法传入true变成公平锁。

ReentrantLock的用法

  • lock():获取锁。
  • unlock():释放锁。
  • tryLock():尝试获取锁,如果获取成功返回true,否则返回false
  • tryLock(long timeout, TimeUnit unit):在指定时间内尝试获取锁,如果获取成功返回true,否则返回false。
  • getHoldCount():查询当前线程保持此锁定的个数,即调用lock()方法的次数。
  • getQueueLength():返回正等待获取此锁定的线程估计数。
  • isHeldByCurrentThread():查询当前线程是否保持此锁定。
  • isLocked():查询此锁定是否由任意线程保持。

代码案例:

ReentrantLock lock = new ReentrantLock(true);
//传入true代表公平锁,不写参数默认非公平锁
lock.lock();//加锁
try {//执行代码
} finally {lock.unlock();//解锁
}

synchronized与ReentrantLock的区别

  • 底层实现不同:synchronized是关键字,基于JVM内部实现,ReentrantLock是Java的一个类,基于Java实现。
  • 使用方式不同:对象在获取synchronized锁时发生阻塞只能死等,而获取ReentrantLock锁可以定义等待的最大时间,超时就放弃锁;使用synchronized出代码块系统自动释放锁,而使用ReentrantLock必须使用lock()加锁,并在最后unlock()解锁,否则发生死锁。
  • 公平性不同:synchronized是非公平锁,ReentrantLock默认是非公平锁,构造时传入true变为公平锁。
  • 唤醒机制不同:synchronized只能通过wait()和notify()/notifyAll()随机唤醒某个线程或者唤醒全部线程;ReentrantLock通过与Condition搭配唤醒线程更加灵活,Condition与Lock绑定,通过await()方法让线程等待,通过signal()方法唤醒一个等待的线程。
//ReentrantLock唤醒
public class ReentrantLock_test {private static final ReentrantLock lockA = new ReentrantLock();private static final ReentrantLock lockB = new ReentrantLock();private static final Condition conditionA = lockA.newCondition();private static final Condition conditionB = lockB.newCondition();public static void main(String[] args) throws InterruptedException {lockA.lock();lockB.lock();conditionA.await();//使lockA锁等待conditionB.await();//使lockB锁等待conditionA.signal();//唤醒lockAconditionB.signal();//唤醒lockB}
}

没有说哪一种锁就一定优于其他的锁。在不同的情况下需要选择不同的锁,synchronized效率更高,ReentrantLock使用更灵活,具体使用哪一种锁还要根据实际情况判断。

相关文章:

JavaEE——锁相关

在开发过程中,如果需要开发者自主实现一把锁,就必须了解锁策略和锁的实现原理。 目录 锁策略 乐观锁和悲观锁 互斥锁和读写锁 轻量级锁和重量级锁 自旋锁和挂起等待锁 公平锁和非公平锁 可重入锁和不可重入锁 死锁 发生死锁的必要条件 synchr…...

C语言指针与数组 进阶

本章主要是补充 指针和数组方面的指示,把前面指针的知识补充下。参考前面的C语言基础—指针 C语言指针与数组 进阶用一级指针访问二维数组❗易错点: 不能直接指针变量数组名指向数组的指针1. 指向指针的指针2. 指向一维数组的指针 (*P)[4]—行指针二维数组名指针数组…...

Java连接SqlServer错误

Java连接SqlServer错误 🏠个人主页:shark-Gao 🧑个人简介:大家好,我是shark-Gao,一个想要与大家共同进步的男人😉😉 🎉目前状况:23届毕业生,目…...

Elastic 可观察性 - 适用于当今 “永远在线” 世界的解决方案

作者:Bahubali Shetti 当今世界,我们的生活很大程度上由应用程序控制。 无论是用于商业用途还是个人用途,我们都希望这些应用程序 “始终在线” 并能够立即做出响应。 这些高期望对开发人员和运营人员提出了巨大的要求。 管理这些应用程序需…...

Temu病毒式营销,如何在大红利时期快人一步?

从去年9月开始,拼多多推出海外版Temu,大手笔烧钱买量、大手笔补贴消费者,通过令人难以置信的超低价(比如一件卫衣2.44美元,且包邮),在北美市场迅速打开局面,并引发海外网友“人传人”…...

ChatGPT使用案例之写代码

ChatGPT使用案例之写代码 可以对于许多开发者而言又惊又喜的是我们可以使用ChatGPT 去帮我们完成一些代码,或者是测试用例的编写,但是正如我们提到的又惊又喜,可能开心的是可以解放一部分劳动力,将自己的精力从繁琐无聊的一些任务…...

蓝桥杯刷题第二十五天

第一题:全球变暖 题目描述 你有一张某海域 NxN 像素的照片,"."表示海洋、"#"表示陆地,如下所示: ....... .##.... .##.... ....##. ..####. ...###. ....... 其中"上下左右"四个方向上连在一起的一片陆地组成一…...

【牛客网】

目录知识框架No.1 前缀和NC14556:数圈圈NC14600:珂朵莉与宇宙NC21195 :Kuangyeye and hamburgersNC19798:区间权值NC16730:runNC15035:送分了qaqNo.2 字符串:小知识点:基于KMP算法的…...

SpringBoot中的事务

事务 Springboot有3种技术方式来实现让加了Transactional的方法能使用数据库事务,分别是"动态代理(运行时织入)"、“编译期织入”和“类加载期织入”。这3种技术都是基于AOP(Aspect Oriented Programming,面向切面编程)思想。(在网…...

Zookeeper客户端Curator5.2.0节点事件监听CuratorCache用法

Curator提供了三种Watcher: (1)NodeCache:监听指定的节点。 (2)PathChildrenCache:监听指定节点的子节点。 (3)TreeCache:监听指定节点和子节点及其子孙节点。…...

C++ using:软件设计中的面向对象编程技巧

C using:理解头文件与库的使用引言using声明a. 使用方法和语法b. 实际应用场景举例i. 避免命名冲突ii. 提高代码可读性c. 注意事项和潜在风险using指令a. 使用方法和语法b. 实际应用场景举例i. 将整个命名空间导入当前作用域ii. 代码组织和模块化using枚举a. C11的新特性b. 使用…...

修建灌木顺子日期

题目 有 N 棵灌木整齐的从左到右排成一排。爱丽丝在每天傍晩会修剪一棵灌 木, 让灌木的高度变为 0 厘米。爱丽丝修剪灌木的顺序是从最左侧的灌木开始, 每天向右修剪一棵灌木。当修剪了最右侧的灌木后, 她会调转方向, 下一天开 始向左修剪灌木。直到修剪了最左的灌木后再次调转方…...

深入学习JavaScript系列(七)——Promise async/await generator

本篇属于本系列第七篇 第一篇:#深入学习JavaScript系列(一)—— ES6中的JS执行上下文 第二篇:# 深入学习JavaScript系列(二)——作用域和作用域链 第三篇:# 深入学习JavaScript系列&#xff…...

Mybatis中的Map的使用和模糊查询的需求实现及其防SQL注入优化

文章目录一.Map的使用和模糊查询的需求实现及其防SQL注入优化1.1 Map的使用1.2 模糊查询的实现1.2.1 防SQL注入优化1.2.2 总结一.Map的使用和模糊查询的需求实现及其防SQL注入优化 1.1 Map的使用 替换之前的根据ID查询信息: 1.编写接口: User getUse…...

【redis】redis缓存更新策略

目录一、缓存更新策略二、主动更新策略三、Cache Aside Pattern3.1 删除缓存还是更新缓存?3.2 如何保证缓存与数据库的操作同时成功或失败?3.3 先操作缓存还是先操作数据库3.3.1 先删缓存再删库3.3.2 先删库再删缓存一、缓存更新策略 1.内存淘汰:不用自…...

LeetCode刷题--复制带随机指针的链表

复制带随机指针的链表1.题目2.解题思路3.完整代码1.题目 题目链接: https://leetcode.cn/problems/copy-list-with-random-pointer/ 给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。 …...

关于我的第一台电脑 华硕

2011年买的,第一台电脑是华硕 U36KI243SD 13.3英寸 白色 i5 1G独显 USB3.0 500G 当时花了5699,着实是一笔巨款,我同学看了一眼就说“我C,这本真好”。 买它主要还是因为好看。当时win7也才开始流行,感觉用上这个本&…...

【华为OD机试 2023最新 】 最大化控制资源成本(C++ 100%)

文章目录 题目描述输入描述输出描述备注用例题目解析C++题目描述 公司创新实验室正在研究如何最小化资源成本,最大化资源利用率,请你设计算法帮他们解决一个任务混部问题: 有taskNum项任务,每个任务有开始时间(startTime),结束时间(endTime),并行度(parallelism)…...

leetcode 有序数组的平方(977)

题目 给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。 示例 1: 输入:nums [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变…...

文本三剑客之awk

文本三剑客之awkawk命令的简要处理流程awk命令的执行过程NR输出分割符和输入分割符案例awk命令引用shell变量awk的几个内置函数流控数组awk命令的简要处理流程 awk命令的执行过程 awk BEGIN{commands} pattern{commands} END{commands}files 执行BEGIN {commands}语句块中的语…...

RK3568平台开发系列讲解(驱动基础篇)IS_ERR函数的使用

🚀返回专栏总目录 文章目录 一、IS_ERR函数二、内核错误码沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇将介绍 IS_ERR 函数的使用。 一、IS_ERR函数 对于任何一个指针来说,必然存在三种情况: 一种是合法指针一种是 NULL (也就是空指针)一种是错误指针(也就…...

特殊的类之注解

注解🚙注解的入门和作用以及原理示例注解的方法名就是属性名Retention的作用Target的作用注解的属性设置默认值天生我材必有用,千金散尽还复来。——唐代李白《将进酒》 在Java中,注解实际上是特殊类型的接口,当我们使用注解时&am…...

商业分享:盲盒电商开启电商新可能

盲盒,顾名思义,一个看不出里面装着什么东西的盒子。当你看不见盲盒里的商品时,你会思考盲盒里可能装着什么,它会诱发你的好奇心,而在好奇心的促使下,你会不由自主地买一个拆开来看,刚好大多数盲…...

【计算机架构】如何计算 CPU 时间

目录 0x00 响应时间和吞吐量(Response Time and Throughput) 0x01 相对性能(Relative Performance) 0x02 执行时间测量(Measuring Execution Time) 0x03 CPU 时钟(Clocking) 0x…...

银行数字化转型导师坚鹏:银行行长如何进行数字化转型

银行行长如何进行数字化转型 ——数字化转型背景下重塑银行行长核心竞争力 授课背景: 很多银行存在以下问题:银行行长不知道如何进行数字化转型?银行行长不清楚银行数字化能力模型的内涵?银行行长不知道如何通过数字化转型提…...

N32G45x学习笔记--- gpio引脚复用

关于gpio的引脚复用需要开启复用时钟 RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_AFIO, ENABLE);官方引脚复用: 芯片上电默认使能 SWD-JTAG 调试接口,调试接口被映射到 GPIO 端口上 禁止 JTAG 使能SWJ-DP /* 禁止 JTAG 使能SWJ-DP */GPIO_ConfigPinRemap(GPIO_RMP_SW_JTAG_S…...

ArcGIS Pro中使用深度学习的高分辨率土地覆盖制图

本文非常详细的讲解了利用深度学习在高分辨率土地覆盖制图的应用,本文作者:Amin Tayyebi,文章从数据准备到训练U-Net模型等等细节都有讲解。本译文只是使用谷歌翻译而成。文章可能有错误语句及不通顺情况,所以仅供参考学习。有需要…...

【学习笔记】「NOI2018」冒泡排序

从题解的角度来说,这是一道简单题。不过考场上在没有任何人提示的情况下要想出正确的结论其实并不容易。 我自己做这道题的时候,因为没有想清楚题目给出的下界能取到的充要条件是什么,所以到了很晚才猜到结论,以至于难以为继。 …...

【Ruby学习笔记】3.Ruby 语法及数据类型

前言 本章介绍Ruby的语法和数据类型。 Ruby 语法 让我们编写一个简单的 Ruby 程序。所有的 Ruby 文件扩展名都是 .rb。所以,把下面的源代码放在 test.rb 文件中。 实例 #!/usr/bin/ruby -wputs "Hello, Ruby!";在这里,假设您的 /usr/bin …...

华为OD机试题【字符匹配】用 Java 解 | 含解题说明

华为Od必看系列 华为OD机试 全流程解析+经验分享,题型分享,防作弊指南华为od机试,独家整理 已参加机试人员的实战技巧华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典本篇题目:字符匹配 题目 给你一个字符串…...