注:此博文为本人学习过程中的笔记

1.常见的锁策略

当我们需要自己实现一把锁时,需要关注锁策略。Java提供的synchronized已经非常好用了,覆盖了绝大多数的使用场景。此处的锁策略并不是和Java强相关的,只要涉及到并发编程,涉及到锁,都要关注锁策略。锁策略就是指这个锁在加锁的过程中有什么特点,有什么行为

1.乐观锁和悲观锁

这不是针对某一把具体的锁,而是一种特性,一把锁具有“乐观”或者“悲观”的特性。

悲观锁是指加锁的时候,预测接下来的锁竞争非常激烈,针对这样激烈的情况会做一些额外的工作。

乐观锁是指加锁的时候,预测接下来的锁竞争不激烈,不做额外的工作。

2.重量级锁和轻量级锁

这是指遇到以上场景之后的解决方案

重量级锁是指当悲观的场景下付出更多的代价,更低效

轻量级锁是指当乐观的场景下付出较少的代价,更高效

3.挂起等待锁和自旋锁

挂起等待锁是重量级锁的典型实现,是操作系统内核级别的,加锁的时候发现竞争,就会使线程进入阻塞状态,后续就需要内核进行唤醒了。竞争激烈,获取锁的周期更长,很难及时获取到锁,此时这个过程就不会消耗cpu资源,让cpu去做其他的事情。

自旋锁是轻量级锁的典型实现,是应用程序级别的,加锁的时候发现竞争,一般是不进入阻塞,而是通过忙等的形式进行等待。锁竞争不激烈,获取锁的周期更短,等待锁的过程中会一直消耗cpu资源。

4.普通互斥锁和读写锁

synchronized就是普通互斥锁,只有加锁和解锁。而读写锁存在读方式加锁,写方式加锁和解锁。多个线程读取一个数据本身就是线程安全的,但是当遇到多个线程读数据,而有一个线程写数据,那么就会涉及到线程安全问题了。当大部分操作在读,少部分操作在写,这时把锁设置成普通互斥锁就意味着锁冲突会非常严重。读写锁能确保读锁和读锁之间不互斥,而读锁和写锁,写锁和写锁之间产生互斥,在保证线程安全的前提下,减少锁冲突的概率,提高效率。

Java标准库中可以使用读写锁,使用的是经典的lock和unlock写法

5.可重入锁和不可重入锁

当一个线程针对一把锁连续加锁的时候,可重入锁不会造成死锁,synchronized就是可重入锁。可重入锁的要点有:1.锁要记录当前是哪个线程拿到锁,2.统计加锁的次数,在合适的时机释放锁。

6.公平锁和非公平锁

当多个线程争取一把锁的时候,锁被释放时,如果遵守先来后到的原则,那么这个锁就是公平锁。synchronized是非公平锁,锁在默认情况下被获取到的概率是均等的,因为操作系统的调度是随机的。要想实现公平锁需要付出额外的代价,比如用一个队列来记录各个线程获取锁的顺序。

7.synchronized

synchronized是自适应的锁,不是读写锁,是可重入锁和非公平锁。自适应是指,jvm会统计锁竞争的激烈程度,来决定锁是挂起等待锁还是自旋锁。锁自适应的过程存在锁升级,由无锁升为偏向锁,再身为自旋锁,最后升级为挂起等待锁。偏向锁就是指当synchronized的时候不是一上来就加锁,而是加一个标记,如果这个锁没有被竞争,就不会真正的加锁,在解锁的时候把这个标记删除即可。如果这个锁被竞争了,那就会真正加锁。这个标记是非常轻量的,比加锁解锁高效得多。当前jvm只提供了锁升级,不存在锁降级。

8.锁消除

这是编译器优化的一种体现,编译器会判定当前这个代码是否真的需要加锁,如果确实不需要加锁,就会自动把synchronized删去。这个优化的策略是比较保守的,所以我们加锁的时候还是要仔细辨别。

9.锁粗化

这里引入一个新的概念,锁的粒度。当加锁和解锁之间包含的代码越多(需要执行的指令),锁的粒度就越粗,反之越细。一个代码中反复针对细粒度的代码进行加锁,就可能优化成粗粒度的锁。因为每次加锁解锁都会增加锁的竞争,影响效率。

2.CAS

CAS是指比较和交换,compare and swap。

boolean CAS(address, expectedValue, swapValue) {if(&address == expectedValue) {address == swapValue;return true;}return false;
}

这里的伪代码是指判定内存中的值是否和寄存器1的值一致,如果一致就把内存中的值和寄存器2的值进行交换。由于这里基本只关心内存里的值,而不关注寄存器2的值,所以可以把这里理解成赋值,基于交换实现的赋值。 

CAS是cpu的一条指令,所以它是原子的,这就对我们编写多线程代码产生了很大的作用。

CAS本质上是cpu的指令,操作系统把这个指令进行了封装,提供了一些api,就可以在C++被调用了,而jvm又是C++实现的,所以jvm也能实现CAS操作。

1.原子类

CAS主要的应用是原子类。以下是Java标准库中提供的原子类。原子类是一个专有名词,特指atomic这个包里的这些类。

对boolean,int,long这些类型进行了封装,确保性能,又能确保线程安全。 

以AtomicInteger里的getAndIncrement为例(以下是伪代码)

class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;//可以把oldValue理解成寄存器while(CAS(oldValue, value, oldValue + 1) != true) {oldValue = value;}return oldValue;}
}

在计算的过程中,即使是多线程操作,因为CAS会不停对比寄存器和内存里的值,所以不会产生线程安全问题。 

2.基于CAS实现自旋锁

public class SpinLock {private Thread owner;private void lock() {//通过CAS判断当前锁是否被线程持有//如果这个锁已经被其他线程所持有,那么就自旋等待//如果这个锁没有被其他线程持有,那么锁的拥有者就设为当前线程while(!CAS(this.owner, null, Thread.currentThread)) {}}private void unlock() {this.owner = null;}
}

3.CAS的典型缺陷 

CAS有一个典型的缺陷,是ABA问题。使用CAS能够进行线程安全的编程的核心就是比较,比较内存和寄存器里的值。这里本质上就是在判定是否有其他线程插入进来进行了修改。我们认为如果内存和寄存器里的值一致,那么就没有线程穿插进来修改。但实际上存在另一种情况,另一个线程把内存里的A改成B,又把B改回A。

ABA问题一般不会产生什么大问题,只有在极端情况下才会产生严重问题。

1.事例

以一个取钱的问题举例,假设我的余额有1000,我在atm机前想取出500。由于我不当的操作,让机器里产生了两个线程来进行取钱操作。即使两个线程穿插操作,因为CAS有判断,所以不会产生什么问题。但如果在第一个线程的CAS执行完毕,余额变成500之后,刚好在那个瞬间有人往我的余额里转了500,那么余额又变成了1000,此时第二个线程进行判断后,就又会扣500。最终,我的余额就只有500了。

2.解决方法

在上述问题中,是使用余额来判定是否有其他线程插入,余额这个数值是既能增加又能减少,所以会产生ABA问题。如果这个时候我们引入一个其他指标,比如“版本号”,规定它只能增加,不能减少,每进行一次操作时,版本号就加1,就能有效避免ABA问题。

3.JUC相关的组件

这里的JUC指的是,java.util.concurrent这个包。这个包里封装了和并发编程相关的东西。

1.Callable接口

这个接口和Runnable接口是并列关系。Runnable接口里面的run方法没有返回值,Callable接口里面的call方法可以设置返回值

public Test {public static void main(String[] args) {Callable<Integer> callable = new Callable<>(){public int call() {int result = 0;for(int i = 0; i < 5000; i++) {result++;}return result;}};//Thread没有提供参数是Callable的构造方法,所以要借助FutureTask这个类//注意这里的泛型类要和Callable的一致FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();//get操作就是获取FutureTask的返回值,这个返回值来自Callable里的call方法//get是可能会阻塞的,如果线程没有执行完,get拿不到返回结果,那么它就会一直阻塞System.out.println(futureTask.get());}
}

2.ReentrantLock

ReentrantLock和synchronized是并列的,都是用来加锁的。

1.synchronized是关键字(内部是通过C++实现的),ReentrantLock是类(内部是Java代码实现的)。 

2.synchronized是通过代码块来加锁,ReentrantLock是通过lock和unlock来加锁的,注意unlock容易掉,搭配finally使用

3.ReentrantLock除了提供lock方法外,还提供了一个tryLock方法,这个方法加锁不会阻塞,会返回true或者false,由开发者根据判定结果决定后续的操作。

4.ReentrantLock还提供了公平锁的实现,它默认是非公平锁,在new的时候往括号里填个true就能获得一把公平锁。

5.ReentrantLock搭配的等待通知机制是Condition类,功能比wait/notify更强大

3.Semaphore 

Semaphore指的是信息量,能够协调多个线程之间的资源分配。

信息量表示的是“可用资源的个数”,申请一个资源(P操作,acquire)时就会+1,释放一个资源(V操作,release)时就会-1。当计数器为0时,继续申请就会阻塞等待。

Semaphore semaphore = new Semaphore(需要的信号量);

Semaphore有一种特殊情况,当信号量为1时,取值要么是1要么是0,此时就相当于一把锁,我们也可以通过信号量的设置来限制最多有几个线程来执行任务。 

4.CountDownLatch

使用多线程编程时,经常把一个大任务拆分成多个子任务,并发执行这些子任务,从而提高程序的效率。那我们要怎么衡量这些任务都完成了呢?这时就需要用到CountDownLatch。

1.基本使用

1.构造方法指定参数,描述一共有多少个任务

2.每个任务执行完毕之后,都调用countDown方法,当调用次数达到了设定的参数,则全部执行完

3.在主线程中调用await方法,等待所有任务执行完。

4.在多线程环境下使用集合类

我们在数据结构中学到的集合类大多都是线程不安全的

1.多线程下使用ArrayList

1.自行加锁(推荐)

自己分析清楚哪些部分需要加锁。

2.Collections synchronized(new ArrayList);

这个东西相当于套了一层壳,返回的所有List里的方法都是synchronized加锁的。

3.使用CopyOnWrite

这个是编程中常见的一种思想方法,写时拷贝。

假设我们有一个数组,现在有线程1对它进行修改,那么就会先复制一份这个数组,在复制的数组上进行修改,修改完之后在改变引用的指向。此时如果有其他线程来读取这个数组,我们能保证要么这个线程读到的时旧数组,或者是新数组,不会是修改到一半的数组。

缺陷

1.当这个数组非常大时,进行复制的开销会很大。

2.当有多个线程进行修改操作时,也会产生很大的问题。

这个方法使用于特定的场景,比如服务器重新加载配置的时候。

2.多线程下使用HashMap

HashMap是线程不安全的,虽然HashTable是线程安全的,但是它是给所有方法都加锁,效率不高。所以我们使用ConcurrentHashMap,它是按照桶级别进行加锁,不是加了一个全局锁,大幅降低了产生锁冲突的概率。

上图中的竖线标志对应的链表。

Concurrent的核心优化点

1.桶级别加锁

当有多个线程进行修改时,如果修改的是不同链表上的值,本身就不涉及线程安全问题。如果在同一个链表上修改才会产生线程安全问题。 在实际开发中,使用的哈希表可能是非常大的,那么桶也会有很多,大概率是不会产生线程安全问题的。

2.size使用原子类

修改不同链表的过程不会产生线程安全问题,但是它们一起修改哈希表的size时,就会有问题了,这个时候我们就可以使用原子类来设置size

3.分段扩容

当我们想对哈希表进行扩容时,一次把所有的数据搬运完会比较耗时间,这是就可以分段搬运数据,进行多次put/get。

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

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

相关文章

c++STL——stack、queue、priority_queue的模拟实现

文章目录 stack、queue、priority_queue的模拟实现使用部分模拟实现容器适配器deque的介绍原理真实结构deque的迭代器deque的操作deque的优缺点 stack的模拟实现按需实例化queue的模拟实现priority_queue的模拟实现为何引入仿函数代码实现 stack、queue、priority_queue的模拟实…

【深度学习—李宏毅教程笔记】Transformer

目录 一、序列到序列&#xff08;Seq2Seq&#xff09;模型 1、Seq2Seq基本原理 2、Seq2Seq模型的应用 3、Seq2Seq模型还能做什么&#xff1f; 二、Encoder 三、Decoder 1、Decoder 的输入与输出 2、Decoder 的结构 3、Non-autoregressive Decoder 四、Encoder 和 De…

C++镌刻数据密码的树之铭文:二叉搜索树

文章目录 1.二叉搜索树的概念2.二叉搜索树的实现2.1 二叉搜索树的结构2.2 二叉搜索树的节点寻找2.2.1 非递归2.2.2 递归 2.3 二叉搜索树的插入2.3.1 非递归2.3.2 递归 2.4 二叉搜索树的删除2.4.1 非递归2.4.2 递归 2.5 二叉搜索树的拷贝 3.二叉树的应用希望读者们多多三连支持小…

系统架构设计师:流水线技术相关知识点、记忆卡片、多同类型练习题、答案与解析

流水线记忆要点‌ ‌公式 总时间 (n k - 1)Δt 吞吐率 TP n / 总时间 → 1/Δt&#xff08;max&#xff09; 加速比 S nk / (n k - 1) | 效率 E n / (n k - 1) 关键概念 周期&#xff1a;最长段Δt 冲突‌&#xff1a; ‌数据冲突&#xff08;RAW&#xff09; → 旁路/…

强制重装及验证onnxruntime-gpu是否正确工作

#工作记录 我们经常会遇到明明安装了onnxruntime-gpu或onnxruntime后&#xff0c;无法正常使用的情况。 一、强制重新安装 onnxruntime-gpu 及其依赖 # 强制重新安装 onnxruntime-gpu 及其依赖 pip install --force-reinstall --no-cache-dir onnxruntime-gpu1.18.0 --extra…

桌面我的电脑图标不见了怎么恢复 恢复方法指南

在Windows操作系统中&#xff0c;“我的电脑”或在较新版本中称为“此电脑”的图标&#xff0c;是访问硬盘驱动器、外部存储设备和系统文件的重要入口。然而&#xff0c;有些用户可能会发现桌面上缺少了这个图标&#xff0c;这可能是由于误操作、系统设置更改或是不小心删除造成…

2025.04.20【Lollipop】| Lollipop图绘制命令简介

Customize markers See the different options allowing to customize the marker on top of the stem. Customize stems See the different options allowing to customize the stems. 文章目录 Customize markersCustomize stems Lollipop图简介R语言中的Lollipop图使用ggp…

docker-compose搭建kafka

1、单节点docker-compose.yml version: 3 services:zookeeper:image: zookeeper:3.8container_name: zookeeperports:- "2181:2181"volumes:- ./data/zookeeper:/dataenvironment:ZOO_MY_ID: 1ZOO_MAX_CLIENT_CNXNS: 100kafka:image: bitnami/kafka:3.7container_na…

【问题】一招解决vscode输出和终端不一致的困扰

背景&#xff08;闲话Trae&#xff09; Trae是挺好&#xff0c;用了几天&#xff0c;发现它时不时检查文件&#xff0c;一检测就转悠半天&#xff0c;为此我把当前环境清空&#xff0c;就留一个正在调的程序&#xff0c;结果还照样检测&#xff0c;虽然没影响什么&#xff0c;…

Git,本地上传项目到github

一、Git的安装和下载 https://git-scm.com/ 进入官网&#xff0c;选择合适的版本下载 二、Github仓库创建 点击右上角New新建一个即可 三、本地项目上传 1、进入 要上传的项目目录&#xff0c;右键&#xff0c;选择Git Bash Here&#xff0c;进入终端Git 2、初始化临时仓库…

从零开始配置spark-local模式

1. 环境准备 操作系统&#xff1a;推荐使用 Linux 或 macOS&#xff0c;Windows 也可以&#xff0c;但可能会有一些额外的配置问题。 Java 环境&#xff1a;Spark 需要 Java 环境。确保安装了 JDK 1.8 或更高版本。 检查 Java 版本&#xff1a; bash 复制 java -version 如果…

前端~地图(openlayers)绘制车辆运动轨迹(仿高德)

绘制轨迹路线轨迹路线描边增加起点终点图标绘制仿高德方向箭头模仿车辆动态运动动画 车辆运行轨迹 车辆轨迹经纬度坐标 const linePoints [new Point([123.676031, 43.653421]),new Point([123.824347, 43.697124]),new Point([124.197882, 43.946811]),new Point([124.104498…

分布式之CAP原则:理解分布式系统的核心设计哲学

声明&#xff1a;CAP中的P原则都是需要带着的 在分布式系统的设计与实践中&#xff0c;CAP原则&#xff08;又称CAP定理&#xff09;是开发者必须掌握的核心理论之一。它揭示了分布式系统在一致性&#xff08;Consistency&#xff09;、可用性&#xff08;Availability&#x…

IF=40.8|肿瘤免疫:从免疫基因组学到单细胞分析和人工智能

一、写在前面 今天分享的是发表在《Signal Transduction and Targeted Therapy》上题目为"Technological advances in cancer immunity: from immunogenomics to single-cell analysis and artificial intelligence"的文章。 IF&#xff1a;40.8 DOI:10.1038/s41392…

深入理解 Spring @Bean 注解

在 Spring 框架中,@Bean 注解是用于显式地声明一个或多个 Bean 实例,并将其注册到 Spring 容器中的重要工具。与 @Component 系列注解不同的是,@Bean 是方法级别的注解,通常与 @Configuration 注解结合使用。本文将详细介绍 @Bean 注解的功能、用法及其应用场景。 1. @Bean…

Pycharm 如何删除某个 Python Interpreter

在PyCharm中&#xff0c;点击右下角的“Interpreter Settings”按钮&#xff0c;或者通过菜单栏选择“File” > “Settings”&#xff08;macOS用户选择“PyCharm” > “Preferences”&#xff09;。在设置窗口中&#xff0c;导航到“Project: [Your Project Name]” >…

如何改电脑网络ip地址完整教程

更改电脑的网络IP地址以满足特定的网络需求&#xff0c;本文将为您提供一份详细的步骤指南。其实&#xff0c;改变IP地址并不是一件复杂的事&#xff0c;能解决因为IP限制带来的麻烦。以下是操作指南&#xff1a; 方法一&#xff1a;Windows 系统&#xff0c;通过图形界面修改 …

Oracle--SQL性能优化与提升策略

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 一、导致性能问题的内在原因 系统性能问题的底层原因主要有三个方面&#xff1a; CPU占用率过高导致资源争用和等待内存使用率过高导致内存不足并需…

【go】什么是Go语言中的GC,作用是什么?调优,sync.Pool优化,逃逸分析演示

Go 语言中的 GC 简介与调优建议 Go语言GC工作原理 对于 Go 而言&#xff0c;Go 的 GC 目前使用的是无分代&#xff08;对象没有代际之分&#xff09;、不整理&#xff08;回收过程中不对对象进行移动与整理&#xff09;、并发&#xff08;与用户代码并发执行&#xff09;的三…

【unity实战】Animator启用root motion根运动动画,实现完美的动画动作匹配

文章目录 前言1、动画分类2、如何使用根位移动画&#xff1f; 一、根位移动画的具体使用1、导入人形模型2、导入动画3、配置动画参数4、配置角色Animator动画状态机5、使用代码控制人物前进后退 二、问题分析三、Humanoid动画中的Root Motion机制及相关配置1、Humanoid动画中的…