【JavaEE】【多线程】进阶知识

news2024/11/8 23:52:24

目录

  • 一、常见的锁策略
    • 1.1 悲观锁 vs 乐观锁
    • 1.2 重量级锁 vs 轻量级锁
    • 1.3 挂起等待锁 vs 自旋锁
    • 1.4 普通互斥锁 vs 读写锁
    • 1.5 可重入锁 vs 不可重入锁
    • 1.6 不公平锁 vs 公平锁
  • 二、synchronized特性
    • 2.1 synchronized的锁策略
    • 2.2 synchronized加锁过程
    • 2.3 其它优化措施
  • 三、CAS
    • 3.1 CAS概念
    • 3.2 CAS应用场景
      • 3.2.1 实现原子类。
      • 3.2.2 实现自旋锁
    • 3.3 CAS的ABA问题
      • 3.3.1 ABA问题简介
      • 3.3.2 解决方案
  • 四、JUC组件
    • 4.1 Callable接口
    • 4.2 ReentrantLock类
    • 4.3 Semaphore类
    • 4.4 CountDownLatch类

一、常见的锁策略

1.1 悲观锁 vs 乐观锁

悲观和乐观是指锁竞争的激烈情况。

  1. 悲观锁:加锁的时候预测接下来的锁竞争会非常激烈,就需要针对这样的激烈情况额外做工作;
  2. 乐观锁:加锁的时候预测接下来的锁竞争不激烈,就不需要额外做工作去处理锁竞争情况;

1.2 重量级锁 vs 轻量级锁

这两种锁就对应上诉的悲观乐观情况下的处理机制。

  1. 重量级锁:应对上面的锁竞争激烈的悲观情况,效率更低;
  2. 轻量级锁:应对上面的锁竞争不激烈的情况,效率更高。

1.3 挂起等待锁 vs 自旋锁

这又是对上面的重量级与轻量级锁的典型实现。

  1. 挂起等待锁:重量级锁的典型实现,是操作系统内核级别的,加锁发生竞争,线程进入阻塞后,就需要内核进行唤醒。获取锁的周期会变长,但是这期间不会消耗CPU资源。
  2. 自旋锁:轻量级锁的典型实现,是应用程序级别的,加锁时发生竞争,一般不进行阻塞,而是通过忙等,等待后续程序唤醒。获取锁的周期很短,可以及时获取到锁,但是这期间会一直消耗CPU资源。

1.4 普通互斥锁 vs 读写锁

这两种锁是针对加锁解锁时的线程安全问题。

  • 普通互斥锁只有加锁,解锁操作,并且读操作不会出现线程安全问题;
  • 而读写锁有读加锁,写加锁,和解锁操作,
    读锁与读锁之间不互斥;
    读锁与写锁之间存在互斥;
    写锁与写锁之间也存在互斥。
    读写锁主要是针对读操作多,写操作少的情况服务。

1.5 可重入锁 vs 不可重入锁

这组锁就是针对同一个线程多次嵌套获取同一把锁。

  • 可重入锁:字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。
  • 不可重入锁:字面意思是“不可以重新进入的锁”,即同一个线程多次获取同一把锁,会报错。

1.6 不公平锁 vs 公平锁

这组锁是针对线程获取锁的概率设置的。

  • 不公平锁:在Java中不公平锁是概率均等的随机分配锁;
  • 公平锁:在Java中公平锁是按照等待的时间先来后到分配锁。因为操作系统是随机调度,为了实现公平锁就需要数据结构来记录先后调度顺序。

二、synchronized特性

2.1 synchronized的锁策略

锁策略如下:

  • synchronized是一个自适应的锁,开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁。
  • 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁。
  • 是一种普通互斥锁。
  • 是一种不公平锁。
  • 是一种可重入锁。

2.2 synchronized加锁过程

synchronized的自适应过程称为锁升级:无锁 -> 偏向锁 -> 自旋锁 -> 重量级锁。

  • 偏向锁:偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程.
    如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)
    如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别
    当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.
    就像跟你搞暧昧,但是不正式跟你确认关系,如果没人来追求你就一直不确定关系,如果有人追求你就确认关系。

  • 无锁 -> 偏向锁 :这个阶段是进入synchronized代码块;

  • 偏向锁 -> 自旋锁:这个阶段是拿到偏向锁后,遇到其他线程来竞争这个锁;

  • 自旋锁 -> 重量级锁:这个阶段是JVM发现当前的锁竞争非常激烈。

2.3 其它优化措施

除了上面的synchronized锁升级以外,还有以下的优化措施:

  • 锁消除:编译器优化措施,编译器会对加锁的代码进行判断(但这种判断是非常保守的,只有100%确定当前是单线程),如果当前逻辑不需要加锁,编译器就会自动去除synchronized。

  • 锁粗化:一个代码对细粒度的代码反复加锁解锁,就会将这个步骤优化为更粗粒度的加锁解锁。

  • 锁的粒度:加锁和解锁之间,包含的代码执行的逻辑/时间越多,则锁的粒度就越粗,反之越细。

三、CAS

3.1 CAS概念

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

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

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

伪代码表示如下:

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

3.2 CAS应用场景

3.2.1 实现原子类。

原子类:特指java.util.concurrent.atomic 包下的类,这些类中的操作都是原子的。

像我们前面Thread类详解,这篇文章介绍的非原子的操作带来的线程安全问题,如果使用原子类就不存在了。
例如执行下面这段代码:结果就一直是100000。


import java.util.concurrent.atomic.AtomicInteger;

public class Demo {
    private static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.getAndAdd(1);
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.getAndAdd(1);
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count.get());
    }
}

原子类线程安全的原理:

假设两个线程同时调用 getAndIncrement :

  1. 两个线程都读取 value 的值到 oldValue 中。 (oldValue 是一个局部变量, 在栈上. 每个线程有自己的栈)
  2. 线程1 先执行 CAS 操作. 由于 oldValue 和 value 的值相同, 直接进行对 value 赋值。
  3. 线程2 再执行 CAS 操作, 第一次 CAS 的时候发现 oldValue 和 value 不相等, 不能进行赋值。因此需要进入循环。在循环里重新读取 value 的值赋给 oldValue。
  4. 线程2 接下来第二次执行 CAS, 此时 oldValue 和 value 相同, 于是直接执行赋值操作。
  5. 线程1 和 线程2 返回各自的 oldValue 的值即可。

3.2.2 实现自旋锁

基于 CAS 实现更灵活的自旋锁, 获取到更多的控制权。

主要逻辑如下:

  • 通过 CAS 看当前锁是否被某个线程持有。
  • 如果这个锁已经被别的线程持有, 那么就自旋等待。
  • 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.

伪代码描述如下:

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

3.3 CAS的ABA问题

3.3.1 ABA问题简介

假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A。
接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要先读取 num 的值, 记录到 oldNum 变量中.。
使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z。
但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A。
线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过又改成 A 了. 这
个时候 t1 究竟是否要更新 num 的值为 Z 。就不符合预期出现bug。

过程图:

bug例子:

假如你去取钱有1000,取500,不小心按了两次取款,我们只想扣一次500,第一次按相当于T2,第二次按相当于T1,先执行T2读取操作修改为500,这时又正好有人给你汇款了500,内存中与寄存器中又相等了,T1扣款的操作也会执行成功。

造成ABA问题的原因就是内存值既有加又有减,所以我们的解决ABA的方式如下。

3.3.2 解决方案

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

  • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1。
  • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了)。

四、JUC组件

JUC组件就是指 java.util.concurrent 这个包下面的类。

4.1 Callable接口

Callable 接口就和java.lang包下的Runnable接口的定位差不多,只不过给了我们一个返回值,在Runnable接口要重写run方法,而在Callable接口中要重写call方法。

在使用Callable接口传入线程时需要借助JUC下的另一个类FutureTask。

使用例子一般如下:

public class Demo {
    public static void main(String[] args) {
        //创建实现了Callable接口的方法
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return null;
            }
        };
        //将实现Callable接口的对象作为参数传入FutureTask,泛型参数要一致
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        //将FutureTask对象作为参数传入
        Thread thread = new Thread(futureTask);
    }
}

4.2 ReentrantLock类

ReentrantLock类可重入互斥锁,和synchronized的定位是差不多的。

ReentrantLock 的用法:

  • public void lock(): 加锁, 如果获取不到锁就死等。
  • public boolean trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁。
  • public void unlock(): 解锁。
    使用这个类加锁需要我们手动解锁,为避免解锁执行不到的情况,我们一般将unlock放在finally代码块中。
		ReentrantLock locker = new ReentrantLock();
        try {
            locker.lock();
        }finally {
            locker.unlock();
        }

ReentrantLock与synchronized的区别:

  • ReentrantLock是类,是Java代码实现的,synchronized是关键字由JVM使用c++代码实现的。
  • ReentrantLock是需要lock()方法加锁,unlock()方法解锁。synchronized是通过进出代码块加锁解锁。
  • ReentrantLock还提供了tryLock()方法可以设置超时时间,不会一直阻塞,加锁成功返回true,调用者可以根据返回值自己操作。
  • ReentrantLock还提供了公平锁的实现,默认是非公平锁,但是构造方法传参true实现公平锁。
  • ReentrantLock搭配的通知机制是Condition类,可以更精确控制唤醒某个指定的线程,相比wait / notify功能更强大。

4.3 Semaphore类

信号量,:用来表示 “可用资源的个数”。 本质上就是一个计数器,能够协调多个线程之间的资源调配。
最主要的就是申请资源(P操作,调用acquire方法)释放资源(V操作,调用release方法)

  • 可以把信号量想象成是停车场的展示牌: 当前有车位 100 个。表示有 100 个可用资源。
  • 当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)
  • 当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)
  • 如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

4.4 CountDownLatch类

CountDownLatch类的作用:同时等待 N 个任务执行结束。(就像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。)

使用多线程的时候,经常会把一个大任务拆分为多个子任务,使用CountDownLatch衡量子任务完成,让整个任务完成。

  • 构造 CountDownLatch 实例, 参数传入任务个数,初始化 10 表示有 10 个任务需要完成。
  • 每个任务执行完毕, 都调用 countDown()方法 . 在 CountDownLatch 内部的计数器同时自减.
  • 主线程中使用 await()方法; 阻塞等待所有任务执行完毕后wait结束。 相当于计数器为 0 了。

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

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

相关文章

炫酷的登录框!(附源码)

大家想看什么前端效果请留言 预览效果 源码 <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>登录页…

RK3568平台开发系列讲解(内存篇)ioremap进行物理内存的映射

🚀返回专栏总目录 文章目录 一、使用案例:二、ioremap 函数的使用三、volatile 的使用一、使用案例: ioremapdevm_ioremap (你不用担心资源的释放)二、ioremap 函数的使用

中小企业设备资源优化:Spring Boot系统实现

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

基于SSM(spring+springmvc+mybatis)+MySQL开发的新闻推荐系统

基于内容的新闻推荐系统 一、环境搭建 开发框架&#xff1a;SSM(springspringmvcmybatis)开发语言&#xff1a;Java、HTML5、JavaScript开发工具&#xff1a;MyEclipse软件依赖&#xff1a;tomcat8、MySQL 1.1 新建工程 打开 myeclispe&#xff0c;新建一个 maven 工程&…

STM32 HAL ADC FIR 窗函数实现低通滤波

FIR 窗函数实现低通滤波 文章目录 FIR 窗函数实现低通滤波1.窗的分类与选择2.matlab设计3.代码编写4.结果欣赏与群延迟5.其他可能报错5.1.adc采集的数据没问题&#xff0c;vofa打印成-nan5.2.vofa打印的滤波数据为初始化的0 6.代码备忘 1.窗的分类与选择 2.matlab设计 主页命令…

例程学习(学习笔记)

project括号里面表示工程的名称&#xff0c;决定了最后生成的bin文件的名称&#xff0c;比如我们现在编译后的bin文件名称就是hello_world.bin文件&#xff0c;如果改成了project(xxx)&#xff0c;编译后的名称就是xxx.bin 这个文件用来设置路径&#xff0c;咱们得例程案例里面…

uniapp:上拉加载更多、下拉刷新、页面滚动到指定位置

提醒 本文实例是使用uniapp进行开发演示的。 一、需求场景 在开发商品&#xff08;SKU&#xff09;列表页面时&#xff0c;通常有三个需求&#xff1a; 页面下拉刷新&#xff0c;第一页展示最新数据&#xff1b;上拉加载更多数据&#xff1b;列表页面可以滚动到指定位置&#x…

5G在汽车零部件行业的应用

5G技术在汽车零部件行业的应用正在不断深入&#xff0c;为行业的智能化、自动化和高效化转型提供了强大的技术支持。 1、5G技术特点与优势 5G技术具有高速度、低延迟、大连接和切片技术等特点与优势。这些特性为汽车零部件行业提供了稳定、可靠、高效的通信连接&#xff0c;使…

jmeter的基本使用

Jmeter基本使用 一、变量 1.用户定义变量 2.用户参数 二、函数 1.计数器${__counter(,)} 2.时间函数 3.加密函数${__digest(,,,,)} 4. 整数相加${__intSum(,,)} 5.属性函数&#xff0c;${__P(,)}、${__property(,,)}、${__setProperty(,,)} 6.V函数 三、获取响应数据…

算法的学习笔记—和为 S 的连续正数序列(牛客JZ74)

&#x1f600;前言 在牛客网的《剑指 Offer》系列题中&#xff0c;有一道关于输出和为给定值 S 的连续正数序列的问题。这是一道典型的双指针问题&#xff0c;考察我们对连续数列求和的理解和双指针的应用。本文将详细解析这道题的思路&#xff0c;并展示如何实现代码。 &#…

D53【python 接口自动化学习】- python基础之模块与标准库

day53 自定义模块 学习日期&#xff1a;20241030 学习目标&#xff1a;模块与标准库 -- 67 自定义模块&#xff1a;如何编写一个完整功能&#xff1f; 学习笔记&#xff1a; 创建自定义模块 自定义模块注意事项 自定义模块 def func1():return this is a functionclass Cl…

上市公司企业数字金融认知数据集(2001-2023年)

一、测算方式&#xff1a;参考C刊《经济学家》王诗卉&#xff08;2021&#xff09;老师的做法&#xff0c;数字金融认知使用每万字年报描述中包含的对数字金融相关关键词的提及次数&#xff0c;关键词为&#xff1a;互联网、数字化、智能、大数据、电子银行、金融科技、科技金融…

4.2-7 运行MR应用:词频统计

文章目录 1. 准备数据文件2. 文件上传到HDFS指定目录2.1 创建HDFS目录2.2 上传文件到HDFS2.3 查看上传的文件 3. 运行词频统计程序的jar包3.1 查看Hadoop自带示例jar包3.2 运行示例jar包里的词频统计 4. 查看词频统计结果5. 在HDFS集群UI界面查看结果文件6. 在YARN集群UI界面查…

How to Train Neural Networks for Flare Removal

Abstract 当相机指向强光源时&#xff0c;生成的照片可能包含镜头眩光伪影。 耀斑以多种形式出现&#xff08;光晕、条纹、渗色、雾霾等&#xff09;&#xff0c;这种外观的多样性使得去除耀斑变得具有挑战性。 现有的分析解决方案对伪影的几何形状或亮度做出了强有力的假设&a…

Kafka如何控制消费的位置?

大家好&#xff0c;我是锋哥。今天分享关于【Kafka如何控制消费的位置?】面试题&#xff1f;希望对大家有帮助&#xff1b; Kafka如何控制消费的位置? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在 Kafka 中&#xff0c;控制消费位置主要通过以下几个机制来实…

shell脚本实例(4)while实现1+...+100,linux新增用户

while实现1到100求和 #!/bin/bash/ s0 i1 #-le小于等于 while [ $i -le 100 ] dos$[ $s$i ]i$[ $i1 ] done echo $s echo $i 执行结果如下 修改用户名密码脚本 #!/bin/bash/ #提示用户输入用户名 read -p "请输入用户名&#xff1a;"username useradd $username #提…

Qt 实战(10)模型视图 | 10.5、代理

文章目录 一、代理1、简介2、自定义代理 前言&#xff1a; 在Qt的模型/视图&#xff08;Model/View&#xff09;框架中&#xff0c;代理&#xff08;Delegate&#xff09;是一个非常重要的概念。它充当了模型和视图之间的桥梁&#xff0c;负责数据的显示和编辑。代理可以自定义…

lenovo联想小新 潮7000-14AST(81GE)笔记本原厂Win10系统镜像安装包下载

适用机型&#xff1a;【81GE】 链接&#xff1a;https://pan.baidu.com/s/1ciGya7OjTN73rHFJs52WpQ?pwdkgk4 提取码&#xff1a;kgk4 联想原装出厂系统自带所有驱动、出厂主题壁纸、系统属性联机支持标志、系统属性专属LOGO标志、Office办公软件、联想电脑管家、联想浏览器…

C语言部分输入输出(printf函数与scanf函数,getchar与putchar详解,使用Linux ubuntu)

1.输入输出 1.1.按格式输入输出 printf 可以在man手册中查看 int printf(const char *format, ...); printf:函数名(参数)int:函数的返回值 功能&#xff1a;按格式在终端输出 参数&#xff1a;多参 返回值&#xff1a;输出字符个数 格式&#xff1a; %d int %c char…

Jmeter自动化实战

一、前言 由于系统业务流程很复杂,在不同的阶段需要不同的数据,且数据无法重复使用,每次造新的数据特别繁琐,故想着能不能使用jmeter一键造数据 二、创建录制模板 可参考:jmeter录制接口 首先创建一个录制模板 因为会有各种请求头,cookies,签名,认证信息等原因,导致手动复制…