Java 多线程系列Ⅴ(常见锁策略+CAS+synchronized原理)

news2024/11/16 12:51:31

常见锁策略

  • 一、乐观锁 & 悲观锁
  • 二、重量级锁 & 轻量级锁
  • 三、自旋锁 & 挂起等待锁
  • 四、互斥锁 & 读写锁
  • 五、可重入锁 & 不可重入锁
  • 六、公平锁 & 非公平锁
  • 七、CAS
    • 1、CAS特点
    • 2、CAS的应用
    • 3、CAS 实现自旋锁
    • 4、CAS的ABA问题
  • 八、synchronized 原理
    • 1、synchronized 基本特征
    • 2、synchronized 锁升级策略
    • 3、synchronized 锁优化操作


一、乐观锁 & 悲观锁

锁的实现者,预测接下来锁冲突的概率,来决定接下来该怎么做。于是分为两大“门派”:

乐观锁:乐观锁是一种乐观的思想,预测接下来冲突概率不大或认为多个线程之间不会发生冲突,因此在访问数据时不会加锁,而是通过在读取数据时记录一个版本号,更新数据时如果版本号不一致,则认为数据已经被其他线程修改过,需要重新尝试更新(借助版本号或时间戳识别出当前的数据访问是否冲突)。例如 Java 中的 AtomicInteger 类,其内部实现使用了乐观锁机制。

悲观锁:悲观锁则是一种悲观的思想,预测接下来冲突概率比较大或认为多个线程之间会发生冲突,因此在访问数据时会对其加锁,以防止其他线程同时访问。

Synchronized 初始使用乐观锁策略,当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略。

通常来说悲观锁一般要做的工作要更多一些,效率会更低一些。乐观锁做的工作会更少一点,效率更高一点。

二、重量级锁 & 轻量级锁

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

提供原子操作指令
提供mutex互斥锁
提供synchronized等关键字
CPU
操作系统
JVM
Java代码

重量级锁:加锁解锁,过程更低效。加锁机制重度依赖了 OS 提供了 mutex。其中涉及到大量的内核态用户态切换,很容易引发线程的调度。而这个操作,相对来说成本都比较高。

轻量级锁:加锁解锁,过程更高效。加锁机制尽可能不使用 mutex,而是尽量使用用户态代码完成。实在搞不定了,再使用 mutex。涉及到少量的内核态用户态切换,不太容易引发线程调度。

一般情况下:一个乐观锁很可能是一个轻量级锁(不绝对),一个悲观锁很可能是一个重量级锁(不绝对)

需要注意的是,用户态的时间成本是比较可控的,而内核态的时间成本不太可控

用户态下的程序只能访问用户空间的数据和代码,无法直接访问内核空间中的数据和资源,而内核态下的程序可以访问并操作所有的系统资源。

用户态和内核态之间的切换需要通过系统调用来实现,也就是从用户态陷入内核态,通过执行内核代码完成一些特权操作,并返回结果到用户态。这种切换过程需要耗费大量的时间和资源,因此,减少用户态和内核态之间的切换次数,是优化系统性能的一条重要途径。

三、自旋锁 & 挂起等待锁

自旋和阻塞:
实现自旋就是为了忙等,就是为了能够最快速度拿到锁。而阻塞等待,意味着放弃了当前cpu使用权,即使后续被唤醒,也不保证该线程第一时间重新拿到CPU。

自旋锁:是轻量级锁的一种典型实现(通常是存用户态的,不需要经过内核态)。
自旋锁是指当前线程反复地检查锁标志位,如果发现该标志位已经被其他线程设置,则该线程就会不停地循环检查,直到获取到锁为止。自旋锁适用于共享数据区访问短、竞争强度不高的情况下,因为自旋等待并没有真正释放 CPU 给其他线程使用,而是一直占用 CPU 进行循环检查,所以如果自旋等待时间过长,会浪费 CPU 资源,影响系统性能。

挂起等待锁(也称为阻塞锁):是重量级锁的一种典型实现。(通常是通过内核机制来实现挂起等待)
挂起等待锁是指当一个线程请求锁时,若发现该锁已经被其他线程持有,则该线程会被挂起等待,直到锁被释放为止。在挂起等待锁的过程中,该线程会进入睡眠状态,释放 CPU 资源给其他线程使用。当持有锁的线程释放锁之后,等待的线程便会被唤醒,重新请求该锁(可能不会立即获取到锁,需要重新进行锁竞争)。

总体而言,自旋锁适用于共享数据区访问短、竞争强度不高的情况下,可以避免线程上下文切换所产生的开销;而挂起等待锁适用于共享数据区访问长、竞争强度较高的情况下,可以有效地利用 CPU 资源,减少 CPU 的空转时间。

四、互斥锁 & 读写锁

互斥锁(Mutex):是一种用于多线程编程中,防止两个或多个线程同时访问共享资源的机制。通过用锁包围多个线程所要访问的代码区域,只有一个线程能够占有锁,其他线程必须等待该线程释放锁后才能继续执行。互斥锁通常用于保护对共享资源的单线程访问,并且在保证数据正确性的同时保证程序的效率。

读写锁(ReadWrite Lock):则是一种更加高级的同步机制,它允许多个线程同时读取共享资源,但在写入共享资源时需要互斥锁的保护。

读写锁的实现方式是,在读取共享资源时,多个线程可以同时占有读锁;而在写入共享资源时,只允许一个线程占有写锁,其他线程必须等待其释放写锁后才能获取锁。读写锁的使用场景一般是读操作频繁,但写操作比较少的场景,如数据库、文件系统等。

  1. 读加锁和读加锁之间,不互斥。
  2. 写加锁和写加锁之间,互斥。
  3. 读加锁和写加锁之间,互斥。

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

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

五、可重入锁 & 不可重入锁

可重入锁:可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁,而不会被阻塞。这种锁可以在同一个线程获取同一把锁时避免死锁状态的发生,同时能够保证代码的高效性和正确性。

比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁

ReentrantLock 是一个常见的可重入锁的实现,它使用一个计数器来追踪锁的持有次数,每当一个线程获取一次锁时,计数器加 1,释放锁时计数器减 1。

不可重入锁:是指一个线程获取锁后,在未释放锁之前,再次请求获取该锁时将被阻塞,直到锁被释放。这种锁通常会导致死锁的情况,因为如果一个线程已经获取了锁并期望继续获取该锁,则会一直等待自己的锁被释放。

:JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

六、公平锁 & 非公平锁

约定:遵循先来后到就是公平锁,不遵循先来后到,就是非公平锁。

:系统对于线程的调度是随机的,sychronized 这个锁是非公平的。

七、CAS

1、CAS特点

CAS:全称Compare and swap,字面意思:”比较并交换“。
CAS(V,A,B); CAS操作包括三个参数:内存位置V、期望值A和新值B。它的执行过程如下:

  1. 首先,比较内存位置V中的值是否等于期望值A。
  2. 如果相等,则将内存位置V中的值替换为新值B,操作成功。否则,操作失败。
  3. 无论操作是否成功,都返回内存位置V当前的值。

特别注意:

  1. CAS 是一个原子的硬件指令完成的。CAS 的读内存,比较,写内存操作是一条硬件指令,是原子的。
  2. CAS 是直接读写内存的, 而不是操作寄存器。
  3. 当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。CAS 可以视为是一种乐观锁,或者可以理解成 CAS 是乐观锁的一种实现方式。

2、CAS的应用

实现原子类
标准库 java.util.concurrent.atomic 中的类,它们都是使用 CAS(Compare-And-Swap)技术实现的:

例如 AtomicInteger 类,这些类本身就是原子的,因此相关操作即使在多线程下也是安全的:

  1. num.getAndIncrement();// 此操作相当于num++

  2. num.incrementAndGet();// 此操作相当于++num

  3. num.getAndDecrement();// 此操作相当于num–

  4. num.decrementAndGet();// 此操作相当于–num

测试原子类:

public class CAS {
    public static void main(String[] args) throws InterruptedException {
        // 创建原子类,初始化值为0
        AtomicInteger num = new AtomicInteger(0);

        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                // num++ 操作
                num.getAndIncrement();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                num.getAndIncrement();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(num);

    }
}

num.getAndIncrement();操作伪代码:

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

说明:此处CAS操作中的参数 oldValue,可以将看做是工作内存(寄存器)中的值,value 看做是主内存中的值。如果value 和 oldValue 值相同,也就是在这次更新期间 value 值没变过,这时再将 oldValue+1 的值赋给 value 实现自增。如果比较时 value 不等于 oldValue 说明这次更新操作期间 value 被改变,所以此次更新失败,并刷新 oldValue 值进行下次更新操作。

3、CAS 实现自旋锁

使用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伪代码表示,如果当前锁持有者为空,就比较成功,就可以把锁获取权给当前线程,加锁完成循环结束。如果owner非空,说明当前锁被其他线程持有,此时CAS操作失败进入循环空转,持续询问当前锁持有者是否为空,此时一旦其它线程释放了锁,当前线程就能立即获取到锁。

4、CAS的ABA问题

CAS只能对比值是否相同,不能确定这个值是否中间发生过改变。可能导致线程对该值进行操作时出现误判或错误结果的问题。

举例来说,线程 T1 读取一个内存位置 V 的值为 A,然后执行一些操作,最后将值更新为 B。在此期间,线程 T2 将 V 的值从 A修改为 C,再修改回 A。此时,T1 再次执行 CAS 操作时,会发现 V 的值仍然是 A,于是认为它没有被其他线程修改过,就会将 V的值更新为 B。但实际上,在这个过程中 V 的值已经被其他线程修改过了,这样就造成了 ABA 问题。

例如再进行取钱操作时:假设此时我有100元存款,需要取出50块钱,此时建立了两个线程进行取钱操作。正常情况下,线程1、线程2读取当前存款100元,然后线程1将其修改为50元,扣款成功。线程2阻塞结束,比较当前存款50元和100元不同,线程2失败。但是如果在线程1扣款成功后,突然有人给我又转账了50,此时存款又变成了100元,然后线程2开始扣款操作时会发现,存款和读取值相同,就会再次扣款。这就是一个ABA问题。

解决方案
给要修改的数据引入版本号。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期,如果发现当前版本号和之前读到的版本号一致,就真正执行修改操作,并让版本号自增。如果发现当前版本号比之前读到的版本号大,就认为操作失败。

八、synchronized 原理

1、synchronized 基本特征

结合以上的锁策略,我们就可以总结出,Synchronized 具有如下特性:

  1. 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁。
  2. 开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁。
  3. 轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现。
  4. 是可重入锁。
  5. 不是读写锁。
  6. 是非公平锁。

2、synchronized 锁升级策略

从上述synchronized锁具有的策略可知,synchronized锁可根据实际场景进行锁升级,在JVM中对synchronized主要有以下锁升级策略:

遇到锁竞争
锁竞争更激烈
无锁
偏向锁
轻量级锁
重量级锁

上述锁策略中设计到偏向锁概念:

偏向锁:就是非必要不加锁。偏向锁不是真正的“加锁”,只是给对象头中做一个 “偏向锁的标记”,记录这个锁属于哪个线程:

  1. 如果整个代码执行过程中,都没有遇到别的线程竞争当前标记的对象的锁,此时就不用真加锁了。这时就节省了加锁和解锁带来的开销。
  2. 如果后续有其他线程来竞争该锁,由于已经在该锁对象中记录了当前锁属于哪个线程了,因此很容易识别当前申请锁的线程是不是之前记录的线程,如果不是,那就取消原来的偏向锁状态,进入一般的轻量级锁状态。

简单来说,偏向锁就相当于是“搞暧昧”,一旦发现潜在危险,就立即官宣!

总之synchronized的锁升级策略主要指:当一个线程访问共享资源时,秉承非必要不加锁, 优先进入偏向锁状态。随着其他线程进入竞争,偏向锁状态被消除,进入轻量级锁状态。如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁。

3、synchronized 锁优化操作

锁消除
在程序中,可能存在有些程序的代码,用到了 synchronized,但其实没有在多线程环境下。此时这些加锁操作是非常没有必要的,而且会白白浪费加锁和解锁的资源开销。(如单线程下使用StringBuffer)这时我们的编译器+JVM 就会判断锁是否可消除,如果可以,就直接消除。

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

这里的锁粗化(细化)是相对于锁的粒度的,锁粒度即synchronized代码块包含代码的多少(代码越多,粒度越粗。越少粒度越细)。一般写代码的时候,多数情况下,希望锁的粒度更小一点(串行执行的代码少一些,并发执行的代码多一些,充分利用CPU内核资源)。但是实际上可能并没有其他线程来抢占这个锁进行并发,这种情况 JVM 就会自动把锁粗化,避免频繁申请释放锁带来额外开销。

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

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

相关文章

讯飞开放平台--星火认知大模型--开发技术文档--js实例代码详解

阿丹&#xff1a; 之前调用写过调用百度的文心一言写网站&#xff0c;讯飞的星火认知模型开放了&#xff0c;这次尝试一下使用流式来进行用户的交互。 官网&#xff1a; 平台简介 | 讯飞开放平台文档中心 星火认知大模型Web文档 | 讯飞开放平台文档中心 简介&#xff1a; 本…

恒驰QA | 我们到底是做什么的?和恒大恒驰有什么关系?

5个关键问题解答&#xff0c;带您走进恒驰信息 Q&#xff1a;恒驰信息和恒大恒驰是什么关系&#xff1f; A&#xff1a;答案是没有关系。 这是我们被问到比较频繁的问题&#xff0c;只能说纯属同名啦&#xff01;恒驰信息成立于2005年&#xff0c;比恒大恒驰创立早上14年之久。…

【视觉SLAM入门】7.2. 从卡尔曼滤波到扩展卡尔曼滤波,引入、代码、原理、实战,C++实现以及全部源码

"觇其平生&#xff0c;岂能容物&#xff1f;" 0. 简单认识1. 公式对比解读2. 应用举例3. 解决方案(公式---代码对应)3.1 初始化3.2 EKF3.2.1 预测---状态方程3.2.2 系统协方差矩阵3.2.3 预测---系统协方差矩阵3.2.4 设置测量矩阵3.2.5 更新---状态变量&#xff0c;卡…

搞懂三极管

三极管的电流放大作用应该算是模拟电路里面的一个难点内容&#xff0c;我想用这几个动画简单的解释下为什么小电流Ib能控制大电流Ic的大小&#xff0c;以及放大电路的原理。 我这里的三极管也叫双极型晶体管,模电的放大电路和数电的简单逻辑电路里面都会用到。有集电极c、基极b…

docker-compose 升级

此方法针对Linux版本生效&#xff0c;请测试有效&#xff1b;记录以方面日后能使用到&#xff1b; ## 安装docker 使用常用命名安装即可, 以下命令安装若提示找不到安装包&#xff0c;直接update 即可。 yum install docker OR apt install docker OR apt install do…

2023 年全国大学生数学建模B题目-多波束测线问题

B题目感觉属于平面几何和立体几何的问题&#xff0c;本质上需要推导几何变换情况&#xff0c;B题目属于有标准答案型&#xff0c;没太大的把握不建议选择&#xff0c;可发挥型不大。 第一问 比较简单&#xff0c;就一个2维平面的问题&#xff0c;但有点没理解&#xff0c;这个…

【2023最新版】腾讯云CODING平台使用教程(Pycharm/命令:本地项目推送到CODING)

目录 一、CODING简介 网址 二、CODING使用 1. 创建项目 2. 创建代码仓库 三、PyCharm&#xff1a;本地项目推送到CODING 1. 管理远程 2. 提交 3. 推送 4. 结果 四、使用命令推送 1. 打开终端 2. 初始化 Git 仓库 3. 添加远程仓库 4. 添加文件到暂存区 5. 提交更…

【代码随想录】Day 48 动态规划9 (打家劫舍Ⅰ Ⅱ Ⅲ)

打家劫舍 https://leetcode.cn/problems/house-robber/ 注意要是i-1没偷&#xff0c;那么dp[i] dp[i-2] nums[i]&#xff0c;而不是dp[i-1]&#xff1a; class Solution { public:int rob(vector<int>& nums) {if (nums.size() 0) return 0;if (nums.size() 1…

【图文并茂】C++介绍之串

1.1串 引子—— ​ 字符串简称为串&#xff0c;串是由字符元素构成的&#xff0c;其中元素的逻辑关系也是一种线性关系。串的处理在计算机非数值处理中占用重要的地位&#xff0c;如信息检索系统&#xff0c;文字编辑等都是以串数据作为处理对象 串是由零个或多个字符组成的…

OSCP系列靶场-Esay-Sumo

OSCP系列靶场-Esay-Sumo 总结 getwebshell : nikto扫描 → 发现shellshock漏洞 → 漏洞利用 → getwebshell 提 权 思 路 : 内网信息收集 → 内核版本较老 →脏牛提权 准备工作 启动VPN 获取攻击机IP → 192.168.45.194 启动靶机 获取目标机器IP → 192.168.190.87 信息收…

【LeetCode75】第四十九题 数组中的第K个最大元素

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目很简单&#xff0c;就是给我们一个数组&#xff0c;让我们返回第K大的元素。 那么很直观的一个做法就是我们直接对数组进行降序排序…

【恒生电子内推码】

Hello&#xff0c;我是恒生电子股份有限公司的校园大使&#xff0c;不想简历投递后“泡池子”&#xff0c;登录链接&#xff1a;campus.hundsun.com/campus/jobs&#xff0c;填写我的推荐码&#xff1a;EZVJR0&#xff0c;投递&#xff0c;简历第一时间送到HR面前&#xff0c;快…

STM32-DMA

1 DMA简介 DMA&#xff08;Direct Memory Access&#xff09;,中文名为直接内存访问&#xff0c;它是一些计算机总线架构提供的功能&#xff0c;能使数据从附加设备&#xff08;如磁盘驱动器&#xff09;直接发送到计算机主板的内存上。对应嵌入式处理器来说&#xff0c;DMA可…

【2023年数学建模国赛】赛题发布

2023数学建模国赛赛题已经发布啦&#xff0c;距离赛题发布已经过去三个小时了&#xff0c;大家是否已经确定题目呢&#xff1f;学姐后续会持续更新赛题思路与代码~

TINA如何导入spice模型

本文介绍如何使用TINA仿真运算放大器电路。TINA是TI公司自己的spice仿真软件&#xff0c;各个大厂为了更好的让客户使用自己的器件&#xff0c;都纷纷推出自己的仿真软件&#xff0c;ADI也有类似的软件&#xff0c;有机会我们介绍&#xff0c;这期我们主要简单介绍下如何使用TI…

Jmeter系列-Jmeter面板介绍和常用配置(2)

Jmeter面板介绍 常用菜单栏 分布式运行相关的 选项&#xff0c;可以打开日志&#xff0c;修改语言、函数助手对话框&#xff0c;还有管理插件 常用的图标 从左到右依次 新建测试计划选择测试计划模板创建一个新的测试计划打开jmeter脚本保存jmeter脚本剪切复制粘贴展开目录…

大数据课程K18——Spark的ALS算法与显式矩阵分解

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 掌握Spark的ALS算法与显式矩阵分解; ⚪ 掌握Spark的ALS算法原理; 一、ALS算法与显式矩阵分解 1. 概述 我们在实现推荐系统时,当要处理的那些数据是由用户所提供的自身的偏好数据,这些…

Apache nginx解析漏洞复现

文章目录 空字节漏洞安装环境漏洞复现 背锅解析漏洞安装环境漏洞复现 空字节漏洞 安装环境 将nginx解压后放到c盘根目录下&#xff1a; 运行startup.bat启动环境&#xff1a; 在HTML文件夹下有它的主页文件&#xff1a; 漏洞复现 nginx在遇到后缀名有php的文件时&#xff0c;…

微信小程序组件的创建与引用

组件的创建 <view><swiper class"myswiper" interval"{{interval}}" circular autoplay"{{autoplay}}" indicator-dots"{{indicatorDots}}"><swiper-item><image mode"widthFix" src"/image/l…

【JAVA】面向对象的编程语言(继承篇)

个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【❤️初识JAVA】 文章目录 前言继承类的继承方式继承的各种类型多继承继承的特性各种继承关键字extends关键字implements关键字super 与 this 关键字super 关键字this 关键字 final 关键字 前言 在之前的…