【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理

news2024/12/28 20:04:37

在这里插入图片描述

文章目录

  • 常见锁策略
    • 乐观锁 & 悲观锁
    • 轻量级锁 & 重量级锁
    • 自旋锁 & 挂起等待锁
    • 互斥锁 & 读写锁
    • 公平锁 & 非公平锁
    • 可重入锁 & 不可重入锁
      • synchronized对应以上的锁策略
      • 锁策略中的面试题:
  • CAS
    • CAS的介绍
    • CAS如何实现
    • CAS的应用场景
    • CAS的典型问题:ABA问题
  • Synchronized原理
    • 1.锁升级/锁膨胀
    • 2.锁消除
    • 3.锁粗化

常见锁策略

乐观锁 & 悲观锁

乐观锁:预测锁竞争不是很激烈。
悲观锁:预测锁竞争会很激烈。

以上定义并不是绝对的,具体看预测锁竞争激烈程度的结论。

轻量级锁 & 重量级锁

轻量级锁加锁解锁开销比较小,效率更高。
重量级锁加锁解锁开销比较大,效率更低。

多数情况下,乐观锁也是一个轻量级锁。
多数情况下,悲观锁也是一个重量级锁。

自旋锁 & 挂起等待锁

自旋锁:是一种典型的轻量级锁。
挂起等待锁:是一种典型的重量级锁。

举个🌰:
我给男神表白了,然后喜提好人卡一张o(╥﹏╥)o,男神告诉我:你是个好人,但是我有对象了。接下来我可以有两种操作。
自旋锁:每天给男神发早安午安晚安。一旦男神分手,我就可以知道。(一旦锁被释放,就能第一时间感知到,从而有机会获得锁。)自旋锁,占用了大量得系统资源。
挂起等待锁:我说我愿意等,一个人默默的等男神很久。这时候,如果男神分手了,有可能想起我,他分手了。但是也可能(大概率),当男神想起我的时候,已经过了很久很久了。(当真的被唤醒,中间已经是沧海桑田了。)省下了CPU资源,可以做别的事情。

互斥锁 & 读写锁

互斥锁:一个线程加锁了,另一个线程尝试加锁时,就会阻塞等待。(例如synchronized,提供了加锁和解锁的操作。)
读写锁:提供了三种操作

  1. 针对读加锁
  2. 针对写加锁
  3. 解锁

基于一个事实:多线程对同一个变量并发读,这个时候没有线程安全问题,不需要加锁控制。(读写锁就是针对这种情况锁采取的特殊处理。)

读锁和读锁之间没有互斥。
写锁和写锁之间存在互斥。
写锁和读锁之间存在互斥。
(假如当前有一组线程都去读(加读锁),这些线程之间没有锁竞争,也没有线程安全问题。)

公平锁 & 非公平锁

此处将公平定义为先来后道

举个🌰:
在这里插入图片描述

公平锁:一号沸羊羊先追,当美羊羊分手后,就由等待队列中,最早来的沸羊羊上位。
在这里插入图片描述
非公平锁:雨露均沾。
在这里插入图片描述
操作系统和synchronized原生都是“非公平锁”
操作系统这里的针对加锁的控制,本身就以来于线程调度的顺序的。这个调度顺序也是随机的,不会考虑到这个线程等待锁多久。

可重入锁 & 不可重入锁

不可重入锁:一个线程针对一把锁,连续加锁两次,出现死锁。
可重入锁:一个线程针对一把锁,连续加锁多次都不会出现死锁。

synchronized对应以上的锁策略

  1. synchronized既是一个悲观锁,也是一个乐观锁。
    synchronized默认是乐观锁,但是如果发现当前锁竞争比较激烈,就会变成悲观锁。
  2. synchronized既是轻量级锁,也是一个重量级锁。
    synchronized默认是轻量级锁,但是如果发现当前锁竞争比较激烈,就会转化成重量级锁。
  3. synchronized这里的轻量级锁,是基于自旋锁的方式实现的。
    synchronized这里的重量级锁,是基于挂起等待锁的方式实现的。
  4. synchronized不是读写锁
  5. synchronized是非公平锁。
  6. synchronized是可重入锁。

总结:上述谈到的六种锁策略,可以理解为“锁的形容词”。

锁策略中的面试题:

  1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?

悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁。
乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数
据. 在访问的同时识别当前的数据是否出现访问冲突。
悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就
等待.
乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突.

  1. 介绍下读写锁?

读写锁就是把读操作和写操作分别进行加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要用在 “频繁读, 不频繁写” 的场景中

  1. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝
试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.
相比于挂起等待锁,
优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场
景下非常有用.
缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源.

  1. synchronized 是可重入锁么?

是可重入锁.
可重入锁指的就是连续两次加锁不会导致死锁.
实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁
的线程就是持有锁的线程, 则直接计数自增.

CAS

CAS的介绍

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较 A 与 V 是否相等。(比较)
  2. 如果比较相等,将 B 写入 V。(交换)
  3. 返回操作是否成功。

此处最特别的地方,上述这个CAS的过程,并非是通过一段代码实现的。而是通过一条CPU指令完成的。也就是说**CAS操作是原子的。**原子的也就可以在一定程度上回避线程安全问题。
在这里插入图片描述
小结:CAS可以理解为CPU给我们提供的一个特殊指令,通过这个指令,就可以一定程度的处理线程安全问题。
CAS的伪代码(辅助理解,并不是真的代码):

boolean CAS(V, A, B) {
	if (V == A) {
		V = B;
		return true;
	}
	return false;
}

CAS如何实现

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:

  • java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子 性。

CAS的应用场景

  1. 实现原子类
    标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.
    典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo28 {
    public static void main(String[] args) {
        AtomicInteger count = new AtomicInteger(0);
        System.out.println(count.getAndIncrement());
        System.out.println(count.getAndDecrement());
    }
}

伪代码实现:

class AtomicInteger {
	private int value;
	public int getAndIncrement() {
		int oldValue = value;
		while ( CAS(value, oldValue, oldValue+1) != true) {
		oldValue = value;
	}
	return oldValue;
	}
}

在这里插入图片描述
寄存器每一个线程都有自己的一份上下文。
在这里插入图片描述

  1. 实现自旋锁
    基于CAS 实现更灵活的锁, 获取到更多的控制权.
    自旋锁伪代码:
public class SpinLock {
	private Thread owner = null;
	public void lock(){
	// 通过 CAS 看当前锁是否被某个线程持有.
	// 如果这个锁已经被别的线程持有, 那么就自旋等待.
	// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
		while(!CAS(this.owner, null, Thread.currentThread())){
		}
	}
	public void unlock (){
		this.owner = null;
		}
}

在这里插入图片描述

CAS的典型问题:ABA问题

CAS在运行中的核心,是检查value和oldValue是否一致,如果一致,就视为value中途没有被修改过,所以进行下一次交换操作。但是在判断value和oldValue是否一致时,这里的值可能改过,但是还原回来了。
也就是:把value的值设为A的话
CAS判定value为A,此时可能确实是A。但是也可能本来是A,被改成了B,但是又还原为A。
ABA这个情况,大部分情况下,不会对代码/逻辑产生影响的。但是不排除极端情况。

举个🌰:
当前滑稽老铁要去ATM上取钱给老婆买情人节礼物:假设滑稽的账户余额1000元,滑稽准备取500元。当按下取款这一瞬间,机器卡了,滑稽就多按了几下,可能就会产生bug,可能就会产生重复扣款的操作。此时可以考虑使用CAS的方式来扣款。
在这里插入图片描述
此时正确扣款。
但是如果当t2线程在执行CAS之前,有人给滑稽老铁转账500,导致之前扣除的500又变为了1000。此时CAS条件满足,执行扣款操作,导致扣款成功。这就出现了bug。在这里插入图片描述
针对当前问题,采取的方案,就是加入一个版本号。假设初识版本号为1,每次修改版本号都+1.然后进行CAS的时候,不是以金额为准,而是以版本号为准。
在这里插入图片描述

Synchronized原理

两个对象,针对同一个对象加锁,就会产生阻塞等待。synchronized内部还有一些优化机制,存在的目的就是为了是synchronized锁更加高效。

1.锁升级/锁膨胀

  1. 无锁
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

在这里插入图片描述

synchronized(locker){
}

以上代码块就可以经历前面说的几个阶段。
进行加锁的时候,首先会进入到偏向锁的状态。偏向锁并不是真正加锁,只是标记一下。有需要再加锁。
举个🌰:
有一只老虎,他的捕食能力很强。他捕到了很多猎物,但是他一次性吃不完。所以就吃一部分,留一部分。但是留着的一部分有别的动物想要来抢。所以留下来的部分他要看着(标记),当别的动物来抢的时候,老虎就立即扑上去保护食物。对留下来的食物进行加锁。

上述例子,就是偏向锁的过程。
synchronized的时候,并不是真正加锁,先偏向锁状态,做个标记。(这个过程是非常轻量的)如果整个使用锁的过程中,都没有出现锁竞争,在synchronized执行完之后,取消偏向锁即可。
但是,如果使用过程中,另一个线程也尝试加锁,在它加锁之前,迅速的把偏向锁升级成真正的加锁状态。另一个线程也就只能阻塞等待。

当synchronized发生锁竞争的时候,就会从偏向锁升级成轻量级锁。此时,synchronized相当于是通过自旋的方式,来进行加锁的。
要是别人很快就释放锁,自旋是划算的。但是如果迟迟拿不到锁,就不划算。synchronized自旋不是无休止的自旋,自旋到一定程度之后,就会再次升级成为重量级锁。(挂起等待锁)

重量级锁(挂起等待锁):基于操作系统原生的API来进行加锁。Linux原生提供了mutex一组API,操作系统内核提供的加锁功能,这个锁会影响到线程的调度。

此时,如果线程进行了重量级锁的加锁,并且发生锁竞争,此时线程就会被放到阻塞队列中。暂时不参与CPU调度了。直到锁被释放,这个线程才可能会被调用到。

值得注意的是:一旦当前线程被切出CPU,就比较低效。

锁能升级,不能降级。

2.锁消除

编译器智能的判断,看当前代码是否真的需要加锁。如果这个场景不需要加锁,但是程序员加了,就自动将锁去掉了。
例如:StringBuffer 是线程安全的,关键方法中都带有synchronized。但是如果在单线程中使用StringBuffer,synchronized加锁操作是没有意义的。所以就会将锁优化掉。

3.锁粗化

锁的粒度:synchronized包含的代码越多,粒度就越粗,包含的代码越少,粒度就越细。

通常情况下,认为锁的粒度细一点比较好。加锁的部分的代码,并不能并发执行的。锁粒度越细,能并发的代码就越多。反之则越少。

但是有些情况下,锁的粒度反而粗一点更好。

比如:两次加锁解锁之间,间隙非常小,此时,就用一把大锁来解决。

在这里插入图片描述

举个🌰: 麻麻👩要我去买菜菜。要包玉米猪肉饺子。
我买了猪肉,给麻麻打电话汇报买了猪肉。
我买了玉米,给麻麻打电话汇报买了玉米。
我买了饺子皮,给麻麻打电话汇报买了饺子皮。
当我的第三个电话汇报完,就挨骂了。理由是一根筋,为什么不全部买完再汇报?
当我第二次要我买菜菜的时候,我就一次买完给麻麻汇报。

显然第二次的方法更为高效。

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

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

相关文章

Excel公式:将日期转换为月份年

Excel公式:将日期转换为月份年 在Excel中,您可以将日期单元格格式化为多种类型的日期格式,但是在某些情况下,您希望将日期转换为文本格式,仅转换为月,年,日或月年,日月或年日。 在本…

Java全栈学习路线总结,科班程序员搬砖逆袭

🌇文章目录 前言一、前置知识二、 Web前端基础示例:1.文本域2.密码字段 三、后端基础一. Java基础二. 数据库技术三. Web开发技术四. 框架技术五. 服务器部署 四、其他技术五、全栈开发六、综合实践七、学习教程一、前端开发二、后端开发三、数据库开发四…

VUE 学习笔记(三) Vue 渲染流程详解

在 Vue 里渲染一块内容,会有以下步骤及流程: 第一步,解析语法,生成AST 第二步,根据AST结果,完成data数据初始化 第三步,根据AST结果和DATA数据绑定情况,生成虚拟DOM 第四步&…

ESP32设备驱动-Si1145红外接近-紫外 (UV) 指数和环境光传感器驱动

Si1145红外接近-紫外 (UV) 指数和环境光传感器驱动 文章目录 Si1145红外接近-紫外 (UV) 指数和环境光传感器驱动1、Si1145介绍2、硬件准备3、软件准备4、驱动实现1、Si1145介绍 Si1145/46/47 是一款低功耗、基于反射的红外接近、紫外 (UV) 指数和环境光传感器,具有 I2C 数字接…

电脑百度网盘打不开怎么办 电脑百度网盘双击没反应处理方法

有时候我们想要在电脑浏览器上下载一些文件时,打开的文件下载链接有些需要通过百度网盘来存储下载,然而当用户在电脑中安装完百度网盘工具之后,双击想要打开时却总是没反应,对此电脑百度网盘打不开怎么办呢?接下来小编…

Java反射和动态代理

反射 反射允许对封装类的成员变量、成员方法和构造方法的信息进行编程访问 成员变量:修饰符、名字、类型、get/set值 构造方法:修饰符、名字、形参、创建对象 成员方法:修饰符、名字、形参、返回值、抛出的异常、获取注解、运行方法 获取…

【云原生进阶之PaaS中间件】第一章Redis-1.1简介

1 Redis概述 1.1 Redis 简介 Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API,可用作数据库&#…

高质量毕业答辩PPT模板+PPT网站

文章目录 前言一、iSlide二、office plus三、优品PPT总结 前言 提示:这里可以添加本文要记录的大概内容: 又是一年毕业季,又到了快要答辩的时候,最近有一些小伙伴找我要毕业答辩PPT模板,本着这不能拒绝啊的心态&…

20230507使用python3批量转换DOCX文档为TXT

20230507使用python3批量转换DOCX文档为TXT 2023/5/7 20:22 WIN10使用python3.11 # – coding: gbk – import os from pdf2docx import Converter from win32com import client as wc """这里需要安转包pywin32com""" # 读取pdf文件文本内容 …

探秘二分查找中的数学奇迹:如何手动求解整数x的平方根

本篇博客会讲解力扣“69. x 的平方根”这道题的解题思路。这是题目链接。 大家先来审下题: 以及示例: 还有提示: 本题常规的思路有:暴力查找、转换成指数和对数、二分查找、牛顿迭代。 转换成指数和对数的方法非常简单&#…

接口自动化测试框架9项必备功能有哪些?你一定不知道

当你准备使用一个接口测试框架或者自造轮子的时候,或许你需要先了解下一个接口自动化测试框架必须具备什么功能。 一、校验   这个很好了解,如果没有校验,单纯的执行接口的话,那就谈不上测试了。所以支持对返回值校验是一个必须…

[Golang] 爬虫实战-获取动态页面数据-获取校招信息

😚一个不甘平凡的普通人,致力于为Golang社区和算法学习做出贡献,期待您的关注和认可,陪您一起学习打卡!!!😘😘😘 🤗专栏:算法学习 &am…

Solr(1):Solr概述

1 概述 Solr 是一个基于 Apache Lucene 之上的搜索服务器,它是一个开源的、基于 Java 的信息检索库。它旨在驱动功能强大的文档检索应用程序 - 无论您需要根据用户的查询将数据服务到何处,Solr 都可以为您服务。Solr与应用程序的集成以为您服务。 下面…

es 7.x 通过DSL语句添加doc数据

一 在es中doc数据的crud操作 1.1 说明 本案例操作 接上一篇的基础上进行操作。 1.2 添加doc 方式为post http://localhost:9200/order_item/_doc 添加文档数据 必须是post提交,不能是put 1.3 查看文档数据 http://localhost:9200/order_item/_doc/_searc…

118-Linux_数据库_索引

文章目录 一.索引是什么?二.索引为什么选择b树三.测试索引1.在mysql中创建数据库 test_indexdb2.在test_indexdb中创建表 test_index3.运行程序向表中插入1万条数据,都是字符串4. 查询验证 一.索引是什么? 索引是一种特殊的文件,它包含着对数据表里所…

浅谈osgEarth操控器类的createLocalCoordFrame函数如何将局部坐标系的点转为世界坐标系下的Martix(ENU坐标)

在osgEarth操控器类的EarthManipulator中的如下函数: void EarthManipulator::setLookAt(const osg::Vec3d& center,double azim,double pitch,double range,const osg::Vec3d& posOffset) {setCenter( center );.... //…

二、PEMFC基础之电化学与反应动力学

二、PEMFC基础之电化学与反应动力学 1.电流、电流密度2.反应速率常数3.交换电流密度4.电化学动力学奠基石B-V方程5.活化损失计算Tafel公式6.计算案例 1.电流、电流密度 由法拉第定律 i d Q d t n F d N d t i\frac{dQ}{dt}\frac{nFdN}{dt} idtdQ​dtnFdN​ j i A j\frac{…

查询缓存实现、缓存更新策略选择、解决缓存穿透缓存雪崩缓存击穿问题

文章目录 1 什么是缓存?1.1 为什么要使用缓存1.2 如何使用缓存 2 给商户信息查询业务添加缓存2.1 缓存模型和思路2.2 代码如下 3 缓存更新策略3.1 数据库缓存不一致解决方案:3.2 数据库和缓存不一致采用什么方案3.3 删除缓存还是更新缓存?3.4 如何保证缓…

MySQL --- DQL

使用DDL语句来操作数据库以及表结构(数据库设计)使用DML语句来完成数据库中数据的增、删、改操作(数据库操作) 学习数据库操作方面的内容:查询(DQL语句)。 查询操作我们分为两部分学习&#…