当前位置: 首页 > news >正文

【JavaEE】多线程(2)

c95cbc70da094545b55d50d33cc484d4.png


一、线程安全

1.1 线程安全的概念

线程是随机调度执行的,如果多线程环境下的程序运行的结果符合我们预期则说明线程安全,反之,如果遇到其他结果甚至引起了bug则说明线程不安全

1.2 经典例子与解释

下面举一个经典的线程不安全的例子:

public class Demo2 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+ count);}
}

上述代码中t1和t2两个线程对count进行累加操作,在主线程中启动这两个线程然后通过join等待这两个线程都执行完后打印count,预期结果为100000,但打印结果如下:

759abc0e6c384d009e620f0c28defba8.png

上述结果不符合我们的预期,这便是产生了线程安全问题

接下来我们通过CPU指令的方式解释上述原因

count++这个行代码可以看作3个CPU指令:

  1. 把内存count总的值读取到CPU寄存器中 => load
  2. 把寄存器中的值+1,此时任然在寄存器中 =>add
  3. 把上述寄存器计算后的值写回到内存count里 =>save

由于线程随机调度,所以两个线程的CPU指令执行顺序也是随机的。

例如下图:

(画图,时间轴。。。。。。。。。。)

首先t1线程和t2线程分别将1加载到CPU寄存器中(假设此时count的值为1),然后在寄存器中将其加1变为2,最后t1先将2加载回内存中,t2也把2加载回内存中,所以两次加1操作只加了一次1

当然上述执行顺序只是无数可能中的一种,可能t1的一组指令还没有执行完,t2就执行了好几组

下面来总结一下线程不安全的原因

1.3 线程不安全的原因

  1. 线程是随机调度,抢占式执行的
  2. 修改共享数据,多个线程修改同一个变量
  3. 多个线程修改共享数据的操作不是原子性,(count++是3个CPU指令,但是赋值操作就是原子性的)
  4. 内存可见性问题
  5. 指令重排序

4和5后面再解释

1.4 解决线程安全问题

根据上述原因下手

原因1:无法干预

原因2:可以干预,但并不是一个普适的做法,因为有些代码就是要修改同一个变量

原因3:这是一个普适的做法,我们可以将一系列非原子的操作打包成一个原子性的操作->加锁

1.4.1什么是锁

锁是在多线程编程中用来控制线程对共享资源访问的一种机制

1.针对锁主要有这两个操作:

  • 加锁:线程t1加上锁之后,t2也尝试使用同一个锁进行加锁,就会阻塞等待

问:什么叫“t2也尝试使用同一个锁进行加锁”?

答:你可以理解为我们给t2里的操作加上了一种机制,这个机制就是必须加上锁才能进行操作,t1拿了一个锁,加锁后进行它的操作,如果t2也想拿这个锁来加锁就必须等t1操作完成解锁之后,再拿这个锁进行加锁进行它的操作,在此之前t2要阻塞等待,当然,如果t2选择拿别的锁进行加锁就不会阻塞等待(假设只有t1和t2两个线程)

比如,A在餐厅里定了一个包间,把门上锁之后来用餐,这样B来了就不会影响A用餐的过程;也就是t2不会对t1修改count的过程进行干扰,这样就保证了操作的原子性

  • 解锁:t1解锁之后,t2才有可能拿到锁,因为尝试竞争锁的线程可能不只一个

2. 锁的主要特性:互斥,一个线程获取到锁之后,另一个线程也尝试加这个锁,就会阻塞等待,这种现象叫锁竞争锁冲突,代码中也可以有多个锁,只有多个线程竞争同一个锁才会发生锁竞争,竞争不同的锁则不会发生锁竞争

1.5 synchronized关键字

1.5.1 synchronized解读

使用synchronized关键字,synchronized关键字解读:

synchronized (locker) {count++;
}
  1. 这是一个Java的关键字,不是方法
  2. synchronized后面括号里面写的是锁对象
  3. 锁对象的用途:用来区分两个线程是否针对同一个对象加锁,如果是,就会出现锁竞争/互斥就会引起阻塞等待,如果不是就不会出现锁竞争,也就不会阻塞等待
  4. synchronized的{ }:进入到代码块,就是对上述锁对象进行加锁操作,当出了代码块,就是对锁对象进行解锁

我们可以让t1和t2都使用同一个锁对象locker来对count变量的修改操作进行上锁

private static int count = 0;
public static void main(String[] args) throws InterruptedException {Object locker = new Object(); //锁对象Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+ count);
}

这样,虽然两个线程仍然是抢占式执行的,但是保证了count++;这个操作的原子性,结果为:count = 100000 

Java中随便拿一个对象,都可以作为加锁的对象

 1.5.2 synchronized使用示例

1)修饰代码块:指定锁哪个对象,也就是可以锁任意对象

public class SynchronizedDemo {private Object locker = new Object();public void method() {synchronized (locker) {}}
}

锁当前对象:()里直接写this

public class SynchronizedDemo {public void method() {synchronized (this) {}}
}

2)修饰普通方法:锁的SynchronizedDemo对象,谁调用method()方法,就锁谁(可以有多个)

public class SynchronizedDemo {public synchronized void methond() {}
}

3)修饰静态方法:锁的SynchronizedDemo类对象(一个java进程中,一个类只有唯一一个类对象)

public class SynchronizedDemo {public synchronized static void method() {}
}

1.5.3 synchronized特性

1)互斥

某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象,synchronized就会阻塞等待

2)可重入

for (int i = 0; i < 50000; i++) {synchronized (locker) {synchronized (locker) {count++;}}
}

上述线程先对locker进行第一次加锁,在第二次加锁的时候,locker对象已经被锁住了,按照之前的理解,尝试针对一个已经被锁的对象加锁时,就会阻塞等待,这种情况就叫死锁

但synchronized是可重入锁,可重入锁的内部包含了线程持有者计数器

  • 如果某个线程加锁的时候,发现这个锁已经被别人占用,但是恰好占用的是自己,那么仍然可以继续获取到锁,并让计数器自增
  • 解锁的时候(也就是每走出一个代码块)计数器就会递减,当减到0时才真正释放锁

这种机制就叫可重入锁

1.6 死锁

1.6.1 两个常见的场景

死锁有两个比较典型的场景

场景一:不可重入锁引起的死锁

一个线程对一个线程连续加锁两次且这个锁是不可重入锁就会引起死锁

场景二:两个线程两把锁

    public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized(locker1) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2) {System.out.println("t1获取了两把锁");}}});Thread t2 = new Thread(() -> {synchronized(locker2) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1) {System.out.println("t2获取了两把锁");}}});t1.start();t2.start();System.out.println("死锁ing....");}

上述代码中,locker1被t1占用,locker2被t2占用,接下来t1需要locker2,t2需要locker1,这样就陷入了死锁

运行结果显示程序一直没有结束:

1.6.2 如何避免死锁

死锁产生的四个必要条件:

1)锁具有互斥性

2)锁不可抢占:一个线程拿到锁之后,除非它主动释放锁,否则别人抢不走

以上这两点是锁的基本特性,无法干预

3)请求和保持:一个线程拿到一把锁之后,不释放这个锁的前提下,在尝试获取其他锁(嵌套加锁)

解决方法就是不要让两个sychronized嵌套式的占用两个不同的锁对象进行加锁

4)循环等待:多个线程获取多个锁的过程中,出现了循环等待,A等待B,B又等待A

这一点只要我们提前约定好获取锁的顺序,即使出现了嵌套也不会引起死锁,如下述代码t1和t2线程都先获取locker1再获取locker2,这样就不会出现死锁

    public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized(locker1) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2) {System.out.println("t1获取了两把锁");}}});Thread t2 = new Thread(() -> {synchronized(locker1) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2) {System.out.println("t2获取了两把锁");}}});t1.start();t2.start();}

任何一个死锁的场景,都必须同时具备上述四点,缺少一点都不会构成死锁


🙉本篇文章到此结束,下篇文章将继续对线程安全的知识进行讲解

相关文章:

【JavaEE】多线程(2)

一、线程安全 1.1 线程安全的概念 线程是随机调度执行的&#xff0c;如果多线程环境下的程序运行的结果符合我们预期则说明线程安全&#xff0c;反之&#xff0c;如果遇到其他结果甚至引起了bug则说明线程不安全 1.2 经典例子与解释 下面举一个经典的线程不安全的例子&…...

mac下Gpt Chrome升级成GptBrowser书签和保存的密码恢复

cd /Users/自己的用户名/Library/Application\ Support/ 目录下有 GPT\ Chrome/ Google/ GptBrowser/ GPT\ Chrome 为原来的chrome浏览器的文件存储目录. GptBrowser 为升级后chrome浏览器存储目录 书签所在的文件 Bookmarks 登录账号Login 相关的文件 拷贝到GptBrow…...

使用Grafana K6来测测你的系统负载能力

背景 近期我们有个号称会有很高很高并发的系统要上线&#xff0c;为了测试一下自己开发的系统的负载能力&#xff0c;准备了点海克斯科技&#xff0c;来看看抗不抗的住。 之前笔者写过用Apache JMeter进行压力测试的文章&#xff08;传送门&#x1f449;&#xff1a;https://…...

【论文复现】基于BERT的语义分析实现

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀ WRN: 宽度残差网络 概述语义分类文本分类情感分类 实现原理 核心逻辑pre_deal.pytrain.pytest_demo.py 实现方式&演示效果训练阶段测试阶…...

CTF-RE: STL逆向 [NewStarCTF 2023 公开赛道 STL] WP

多看看STL题就会了,很简单 int __fastcall main(int argc, const char **argv, const char **envp) {__int64 v3; // rbx__int64 v4; // raxchar v5; // bl_BYTE *v6; // rax_QWORD *v7; // rax__int64 v8; // rax__int64 v9; // raxint i; // [rsp0h] [rbp-250h]int j; // [r…...

实习冲刺第三十六天

46.全排列 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]示例 2&#xff1a; 输入&#…...

【Zemax光学设计实训三】---激光缩束镜的设计优化

前言与目录 技术设计要求&#xff1a; 设计一个激光扩束镜&#xff0c;使用的波长为1064nm&#xff0c;输入光束直径为10mm&#xff0c;输出光束的直径为2mm&#xff0c;且输入光束和输出光束平行&#xff08;即平行光入射&#xff0c;平行光出射&#xff09;。要求只使用两片…...

TCP/IP协议簇自学笔记

摘抄于大学期间记录在QQ空间的一篇自学笔记&#xff0c;当前清理空间&#xff0c;本来想直接删除掉的&#xff0c;但是感觉有些舍不得&#xff0c;因此先搬移过来。 曾经&#xff0c;我只知道socket函数能进行网络间数据的通信&#xff0c;知道tcp/ip协议也是用来进行网络数据…...

Spring Boot教程之十一:获取Request 请求 和 Put请求

如何在 Spring Boot 中获取Request Body&#xff1f; Java 语言是所有编程语言中最流行的语言之一。使用 Java 编程语言有几个优点&#xff0c;无论是出于安全目的还是构建大型分发项目。使用 Java 的优点之一是 Java 试图借助类、继承、多态等概念将语言中的每个概念与现实世…...

计算机网络(二)

ip地址&#xff1a;11010010&#xff1a;01011110:00100100:00010100 子网掩码:11111111:11111111:11111111:11000000 and &#xff1a;11010010&#xff1a;01011110&#xff1a;00100100&#xff1a;00000000 210.94.36.0的下一站为R1 因为255为11111111 192为&#xff…...

如何在Python中进行数学建模?

数学建模是数据科学中使用的强大工具&#xff0c;通过数学方程和算法来表示真实世界的系统和现象。Python拥有丰富的库生态系统&#xff0c;为开发和实现数学模型提供了一个很好的平台。本文将指导您完成Python中的数学建模过程&#xff0c;重点关注数据科学中的应用。 数学建…...

JavaSE——类与对象(5)

一、抽象类 1.1为什么需要抽象类 父类的某些方法&#xff0c;不确定怎么实现&#xff0c;也不需要实现。 class Animal{public String name;public Animal(String name){this.name name;}public void eat()//这里实现了也没有意义{System.out.println("这是一个动物&am…...

Istio笔记01--快速体验Istio

Istio笔记01--快速体验Istio 介绍部署与测试部署k8s安装istio测试istio 注意事项说明 介绍 Istio是当前最热门的服务网格产品&#xff0c;已经被广泛应用于各个云厂商和IT互联网公司。企业可以基于Istio轻松构建服务网格&#xff0c;在接入过程中应用代码无需更改&#xff0c;…...

面试小札:Java如何实现并发编程

多线程基础 继承Thread类 定义一个类继承自 Thread 类&#xff0c;重写 run 方法。在 run 方法中编写线程要执行的任务逻辑。例如&#xff1a; java class MyThread extends Thread { Override public void run() { System.out.println("线程执行的任务…...

java-a+b 开启java语法学习

代码 &#xff08;ab) import java.util.Scanner; //导入 java.util包中的Scanner 类&#xff0c;允许读取键盘输入数据public class Main { // 创建一个公共类 Mainpublic static void main(String[] args) {//程序入口点&#xff0c;main方法Scanner scanner new Scanner(…...

RNN模型文本预处理--数据增强方法

数据增强方法 数据增强是自然语言处理&#xff08;NLP&#xff09;中常用的一种技术&#xff0c;通过生成新的训练样本来扩充数据集&#xff0c;从而提高模型的泛化能力和性能。回译数据增强法是一种常见的数据增强方法&#xff0c;特别适用于文本数据。 回译数据增强法 定义…...

maven 中<packaging>pom</packaging>配置使用

在 Maven 项目的 pom.xml 文件中&#xff0c; 元素用于指定项目的打包类型。默认情况下&#xff0c;如果 元素没有被显式定义&#xff0c;Maven 会假设其值为 jar。但是&#xff0c;当您设置 pom 时&#xff0c;这意味着该项目是一个 POM&#xff08;Project Object Model&…...

【Python中while循环】

一、深拷贝、浅拷贝 1、需求 1&#xff09;拷贝原列表产生一个新列表 2&#xff09;想让两个列表完全独立开&#xff08;针对改操作&#xff0c;读的操作不改变&#xff09; 要满足上述的条件&#xff0c;只能使用深拷贝 2、如何拷贝列表 1&#xff09;直接赋值 # 定义一个…...

【深度学习】服务器常见命令

1、虚拟环境的安装位置 先进入虚拟环境 which python2、升序查看文件内容 ls -ltr3、查看服务器主机空间使用情况 df -hdf -h .4、查看本地空间使用情况 du -sh ./*du -sh * | sort -nr5、查找并删除进程 # 查找 ps aux# 删除 kill -KILL pid6、查看服务器配置 lscpuuna…...

技术分析模板

文章目录 概要整体架构流程技术名词解释技术细节小结 概要 提示&#xff1a;这里可以添加技术概要 例如&#xff1a; openAI 的 GPT 大模型的发展历程。 整体架构流程 提示&#xff1a;这里可以添加技术整体架构 例如&#xff1a; 在语言模型中&#xff0c;编码器和解码器…...

基于算法竞赛的c++编程(28)结构体的进阶应用

结构体的嵌套与复杂数据组织 在C中&#xff0c;结构体可以嵌套使用&#xff0c;形成更复杂的数据结构。例如&#xff0c;可以通过嵌套结构体描述多层级数据关系&#xff1a; struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...

synchronized 学习

学习源&#xff1a; https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖&#xff0c;也要考虑性能问题&#xff08;场景&#xff09; 2.常见面试问题&#xff1a; sync出…...

AI Agent与Agentic AI:原理、应用、挑战与未来展望

文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例&#xff1a;使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例&#xff1a;使用OpenAI GPT-3进…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)

文章目录 1.什么是Redis&#xff1f;2.为什么要使用redis作为mysql的缓存&#xff1f;3.什么是缓存雪崩、缓存穿透、缓存击穿&#xff1f;3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

06 Deep learning神经网络编程基础 激活函数 --吴恩达

深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...

多模态大语言模型arxiv论文略读(108)

CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文标题&#xff1a;CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文作者&#xff1a;Sayna Ebrahimi, Sercan O. Arik, Tejas Nama, Tomas Pfister ➡️ 研究机构: Google Cloud AI Re…...

排序算法总结(C++)

目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指&#xff1a;同样大小的样本 **&#xff08;同样大小的数据&#xff09;**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...

9-Oracle 23 ai Vector Search 特性 知识准备

很多小伙伴是不是参加了 免费认证课程&#xff08;限时至2025/5/15&#xff09; Oracle AI Vector Search 1Z0-184-25考试&#xff0c;都顺利拿到certified了没。 各行各业的AI 大模型的到来&#xff0c;传统的数据库中的SQL还能不能打&#xff0c;结构化和非结构的话数据如何和…...

嵌入式学习之系统编程(九)OSI模型、TCP/IP模型、UDP协议网络相关编程(6.3)

目录 一、网络编程--OSI模型 二、网络编程--TCP/IP模型 三、网络接口 四、UDP网络相关编程及主要函数 ​编辑​编辑 UDP的特征 socke函数 bind函数 recvfrom函数&#xff08;接收函数&#xff09; sendto函数&#xff08;发送函数&#xff09; 五、网络编程之 UDP 用…...

Vue3 PC端 UI组件库我更推荐Naive UI

一、Vue3生态现状与UI库选择的重要性 随着Vue3的稳定发布和Composition API的广泛采用&#xff0c;前端开发者面临着UI组件库的重新选择。一个好的UI库不仅能提升开发效率&#xff0c;还能确保项目的长期可维护性。本文将对比三大主流Vue3 UI库&#xff08;Naive UI、Element …...