锁策略, cas 和 synchronized 优化过程总结

目录

一、锁策略

1. 乐观锁和悲观锁

2. 读写锁

3. 重量级锁和轻量级锁 

4. 自旋锁

5. 公平锁和非公平锁

6.可重入锁 vs 不可重入锁

二、CAS

1. CAS 是怎么实现的

1) 实现原子类 

2) 实现自旋锁

3. CAS 的 ABA 问题

三、Synchronized 原理

1.Synchronized 加锁工作过程

3.1 偏向锁

3.2 轻量级锁

3.3 重量级锁

2. Synchronized优化操作

一、锁策略

1. 乐观锁和悲观锁

悲观锁:(预测接下来冲突概率比较大)
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

乐观锁:(预测接下来冲突概率不大)
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做

举个栗子:A 和 B 想去KTV唱歌. 

A 认为 "这个是时间,KTV很多人,有可能没有房间". 因此A 会先给KTV前台打电话: "你好,现在还有空余的房间没有,我准备一会过去" (相当于加锁操作) 得到肯定的答复之后, 才会真的来KTV.  如果得到了否定的答复, 那就等一段时间, 一会再来和前台确定时间. 这个是悲观锁. 

B 认为 "这么大个KTV,肯定大概率是有房间的,". 因此 B 直接就来KTV.(相当于没加锁, 直接访问资源) 如果KTV确实比较闲, 那么直接问题就解决了. 如果KTV这会确实很忙, 那么 B , 就下次再来(虽然没加锁, 但是能识别出数据访问冲突). 这个是乐观锁. 

这两种思路不能说谁优谁劣, 而是看当前的场景是否合适. 

如果当前KTV确实比较忙, 那么使用悲观锁的策略更合适, 使用乐观锁会导致 "白跑很多趟", 耗费额外的资源. 

如果当前KTV确实比较闲, 那么使用乐观锁的策略更合适, 使用悲观锁会让效率比较低. 

2. 读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。

一个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.

  • 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
  • 两个线程都要写一个数据, 有线程安全问题.
  • 一个线程读另外一个线程写, 也有线程安全问题.

读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写 锁.

  • ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.

读写锁特别适合于 "频繁读, 不频繁写" 的场景中. (这样的场景其实也是非常广泛存在的)

3. 重量级锁和轻量级锁 

重量级锁:主要依赖了操作系统提供的锁,使用这种锁,就容易产生阻塞等待

轻量级锁:主要尽量的避免使用操作系统提供的锁,而是尽量在用户态来完成功能,尽量避免用户态和内核态的切换,尽量避免挂起等待

synchronized是自适锁,既是轻量级锁,又是重量级锁,是根据锁冲突的情况来的,冲突的不高就是轻量级,冲突的很高就是重量级

锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的。CPU 提供了 "原子操作指令";操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁;JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类。

重量级锁:

  • 加锁机制重度依赖了 OS 提供了 mutex
  • 大量的内核态用户态切换
  • 很容易引发线程的调度

轻量级锁:

  • 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex.
  • 少量的内核态用户态切换.
  • 不太容易引发线程调度.

4. 自旋锁

自旋锁是轻量级锁的具体实现.(自旋锁是轻量级锁,也是乐观锁)

挂起等待锁是重量级锁的具体实现(挂起等待锁是重量级锁,也是悲观锁)

自旋锁:当发现锁冲突时,不会挂起等待,会迅速再来尝试看这个锁能不能获取到

  • 一旦锁被释放,就可以第一时间获取到
  • 如果锁一直不释放,就会立即尝试获取锁,无限循环,直到获取到锁为止,就会消耗大量CPU

挂起等待锁:发现锁冲突,就挂起等待

  • 一旦锁被释放,不能第一时间获取到
  • 在锁被其他线程占用时,会放弃CPU资源

自旋锁是一种典型的 轻量级锁 的实现方式.

  • 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.

  • 缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是 不消耗 CPU 的).

5. 公平锁和非公平锁

公平锁: 遵守 "先来后到". 这样的规则就是公平的,”机会均等“的竞争反而是不公平的
非公平锁: 不遵守 "先来后到". 就是非公平的

synchronized是非公平锁

6.可重入锁 vs 不可重入锁

可重入锁是指可以重新进入的锁,一个线程针对一把锁,连续两次加锁不会出现死锁,这种就是可重入锁   synchronized就属于可重入锁

二、CAS

CAS 可以视为是一种乐观锁.
全称 Compare and swap, 即 "比较并交换". 相当于通过一个原子的操作, 同时完成 "读取内存, 比
较是否相等, 修改内存" 这三个步骤. 本质上需要 CPU 指令的支撑.

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

  1. 比较 A 与 V 是否相等。(比较)

  2. 如果比较相等,将 B 写入 V。(交换)

  3. 返回操作是否成功。

1. CAS 是怎么实现的

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

1) 实现原子类 

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();

2) 实现自旋锁

基于 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;}
}

3. CAS 的 ABA 问题

ABA问题是在分布式系统中,由于网络分区或节点故障导致的数据一致性问题。在CAS(Compare and Swap)操作中,ABA问题指的是,在执行CAS操作期间,数据从A变为B又变回A,导致CAS操作成功,但实际上数据并没有发生变化。

什么是ABA 的问题:
假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.
接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要
先读取 num 的值, 记录到 oldNum 变量中.
使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.
但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A
 

解决方案:

给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期. CAS 操作在读取旧值的同时, 也要读取版本号.

真正修改的时候,如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.

如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).

三、Synchronized 原理

1.Synchronized 加锁工作过程

3.1 偏向锁

第一个尝试加锁的线程, 优先进入偏向锁状态.偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程.如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)

如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别 当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.'


3.2 轻量级锁

随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).此处的轻量级锁就是通过 CAS 来实现.

通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)如果更新成功, 则认为加锁成功如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.也就是所谓的 “自适应”

3.3 重量级锁

如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁  执行加锁操作, 先进入内核态在内核态判定当前锁是否已经被占用 如果该锁没有占用, 则加锁成功, 并切换回用户态.如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒 这个线程, 尝试重新获取锁
 

 我们可以总结出, Synchronized 具有以下特性(只考虑 JDK 1.8):

  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
  3. 实现轻量级锁的时候大概率用到的自旋锁策略
  4. 是一种不公平锁
  5. 是一种可重入锁
  6. 不是读写锁

2. Synchronized优化操作

锁消除

锁消除,编译器自动判定,如果认为这个代码没必要加锁,就不加了.

这个操作不是所有情况下都会触发,在大部分情况下不能触发;而偏向锁,每一次加锁都会先进入偏向锁

举个例子,有些应用程序的代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)

StringBuffer tmp = new StringBuffer();
tmp.append("ab");
tmp.append("bc");
tmp.append("cd");
tmp.append("de");

此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加锁解锁操作是没有必要的, 白白浪费了一些资源开销。这就来到了锁粗化。

锁粗化

一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.

锁的粒度:表示synchronized包含的代码范围是大还是小,范围越大,粒度越粗;范围越小,粒度越细

锁的粒度细了,能够更好的提高线程的并发,但是也会增加“加锁解锁”的次数

for(int i = 0;i < 10000;i++) {synchronized (synchronized线程安全.class) {count++;}
}synchronized (synchronized线程安全.class) { //加锁for(int i = 0;i < 10000;i++) {count++;}
}// 释放锁public static void main(String[] args) {StringBuffer tmp = new StringBuffer();tmp.append("ab");tmp.append("bc");tmp.append("cd");//其中连续三次同时append,加锁---释放锁。System.out.println(sb.toString());}

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

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

相关文章

Vue自定义防重复点击指令(v-repeatClick)

&#xff01;&#xff01;&#xff01;Vue防抖节流方法&#xff1a;VUE使用节流和防抖_vue防抖节流_停留的章小鱼的博客-CSDN博客 新建js文件directive.js: // directive.js // 防重复点击(指令实现) //使用&#xff1a; 在需要的按钮中加 v-repeatClick 指令即可 <el-but…

【websocket - Tornado】简易聊天应用

1、背景 项目测试的过程中需要自己搭建一个webscoket站点,确保此类服务接入后台系统后访问不受影响。python的服务框架常用的有Flask、Django、Tornado,每个框架的侧重点不同,导致使用的场景就会有所差异。 Flask轻量级,采用常规的同步编程方式,需要安装其他模块辅助,主…

逆向时如何找到MingGW(GNU)编译程序的main函数

编译器是MingGW生成的可执行文件的显著特点是, 最终运行ZwContinue后程序就莫名其妙启动了, 也找不到main函数。 为了探究里面究竟怎么回事, 我找到了wrk-v1.2的源码, 其中包含了ZwContinue的实现, 首先先看一下注释, API界面包含了2个参数, 其中让人感兴趣的是PCONTEXT, 这是…

【MySQL】仓储--维护出入库流水、库存,去重数量逻辑修正

系列文章 C#底层库–MySQLBuilder脚本构建类&#xff08;select、insert、update、in、带条件的SQL自动生成&#xff09; 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/129179216 C#底层库–MySQL数据库操作辅助类&#xff08;推荐阅读&#xff0…

使用kickstart和anaconda自动化安装centos系统

使用kickstart和anaconda自动化安装centos系统 使用kickstart和anaconda自动化安装centos系统 anaconda 介绍 kickstart 介绍 实验过程 前提 1.已经安装好至少两台centos系统 2.需要实现自动安装的系统的光盘镜像 3.已安装的系统之间可以通讯(比如处于VMware中的NAT网络的…

数据结构【第3章】——线性表

线性表的定义 线性表&#xff1a;零个或多个数据元素的有限序列。 1&#xff09;线性表是一个序列。即元素之间是有顺序的&#xff0c;若元素存在多个&#xff0c;则第一个元素无前驱&#xff0c;最后一个元素无后继&#xff0c;其他每个元素都有且只有一个前驱和后继。 2&a…

GitHub上删除项目后,IDEA分享项目到GitHub提示Remote is already on GitHub

文章目录 一、错误信息二、解决方法1.删除GitHub上的项目2.找到项目里的.git隐藏文件3.找到config文件4.打开config文件&#xff0c;删除[remote "git-test"]及下面两行内容5.继续使用IDEA分享项目到GitHub即可 一、错误信息 二、解决方法 1.删除GitHub上的项目 2.…

MacBook Pro 16 M1 Max 升级 macOS Ventura 13.5 兼容测评

今天给大家带来了 MacBook Pro 16 M1 Max 升级 macOS Ventura 13.5 兼容 100 挑战赛 的视频&#xff0c;现在充电头再以文章的形式呈现给大家&#xff0c;让大家更清楚、直白的了解这款笔记本在升级系统后的兼容性如何。 MacBook Pro 16 M1 Max 配置了 140W 的 MagSafe 充电口&…

HCIP-Cloud Service V3.0 真题和机构资料

通过认证验证的能力 具备传统企业应用架构和云原生架构设计上云的能力 建议掌握的知识 对IT相关技术有基本的了解&#xff1b;具备一定的公有云服务基础知识&#xff1b;对云计算、网络、存储、数据库等知识有一定的了解&#xff0c;具备Linux操作系统的基础知识 机构的考试大…

如何创建优雅的对象

在Java中&#xff0c;优雅地创建对象可以通过几种方式来实现。以下是一些常用的优雅创建对象的方法&#xff1a; 使用构造方法&#xff1a; 最常见的创建对象的方式是使用类的构造方法。在Java中&#xff0c;每个类都有一个默认的无参构造方法&#xff0c;如果没有显式定义构…

人工智能如何应对 DevOps 监控和可观测性挑战

自 ChatGPT 横空出世之后&#xff0c;AIGC 已成为不可逆转的时代浪潮。在之前的文章中&#xff0c;我们介绍了DevOps 领域中AI的用例&#xff0c;需要回顾可以点击下方链接。在本篇文章中&#xff0c;我将简单聊聊人工智能&#xff08;AI&#xff09;如何通过分析日志和指标来预…

基于Matlab实现图像融合技术(附上多个仿真源码+数据)

图像融合技术是一种将多幅图像融合为一幅图像的方法&#xff0c;使得这幅融合图像包含原始图像的所有信息。近年来&#xff0c;图像融合技术已经广泛应用于图像分割、变换和裁剪等领域。本文将介绍如何使用Matlab实现图像融合技术。 实现步骤 首先&#xff0c;我们需要了解图…

C#实现int类型和字节流的相互在转化

通过TCP协议进行数据传输时&#xff0c;需要将所有传输的内容转为字节流&#xff0c;这里就用到了将int型的数据转为字节流的。代码如下&#xff1a; public static byte[] BytesConvertToInt(int vel) {byte[] hex new byte[4];hex[3] (byte)(vel >> 24) & 0xff)…

【机器学习】Cost Function for Logistic Regression

Cost Function for Logistic Regression 1. 平方差能否用于逻辑回归&#xff1f;2. 逻辑损失函数loss3. 损失函数cost附录 导入所需的库 import numpy as np %matplotlib widget import matplotlib.pyplot as plt from plt_logistic_loss import plt_logistic_cost, plt_two_…

GD32F103VE睡眠与唤醒

GD32F103VE睡眠与唤醒&#xff0c;兆易官网给的程序没有测试。等测试后&#xff0c;才发现有问题。 现修改&#xff0c;测试如下&#xff1a; #include "SleepMode.h" #include "delay.h"u8 WFE_CMD_EnterSleepModeFlag;void Enter_DeepSleepMode(void);…

github国内镜像站点。解决assets转圈加载不出来的问题

github镜像站 https://hub.nuaa.cf/ https://gitclone.com/ 下载加速 https://gh.api.99988866.xyz/ https://ghproxy.com/ https://github.ur1.fun/ assets转圈加载不出来 f12打开开发者工具&#xff0c;根据前面的loadinglaze&#xff0c;找到这个网址 可以直接看到assets…

【使用 DSP 滤波器加速速度和位移】使用信号处理算法过滤加速度数据并将其转换为速度和位移研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【计算机网络】数据链路层

文章目录 1. 数据链路层1.1 数据链路层简介1.2 数据链路层做了什么 2. 以太网协议2.1 以太网2.2 以太网帧的格式2.3 MAC地址2.4 MTU 3. 数据跨网络传输的整体过程4. ARP协议4.1 认识ARP协议4.2 ARP协议的格式4.3 ARP协议的工作流程 1. 数据链路层 1.1 数据链路层简介 数据链路…

MySql之索引

MySql之索引 1.索引概述 MySql官方对索引的定义为&#xff1a;索引是帮助MySql高效获取数据的数据结构。在数据之外&#xff0c;数据库系统还维护着满足特定查找算法的数据结构&#xff0c;这些数据结构以某种方式引用数据&#xff0c;这样就可以在这些数据结构上实现高级查找…

xiaoweirobot.chat

目录 1 xiaoweirobot.chat 1.1 引用文件 1.2 HttpGetDataListener 1.2.1 // 语音云参数 xiaoweirobot.chat 引用文件package com.shrimp.xiaoweirobot.chat; import java.io.UnsupportedEncodingExcep