【JavaEE】多线程(2)

一、线程安全
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,但打印结果如下:

上述结果不符合我们的预期,这便是产生了线程安全问题
接下来我们通过CPU指令的方式解释上述原因
count++这个行代码可以看作3个CPU指令:
- 把内存count总的值读取到CPU寄存器中 => load
- 把寄存器中的值+1,此时任然在寄存器中 =>add
- 把上述寄存器计算后的值写回到内存count里 =>save
由于线程随机调度,所以两个线程的CPU指令执行顺序也是随机的。
例如下图:
(画图,时间轴。。。。。。。。。。)
首先t1线程和t2线程分别将1加载到CPU寄存器中(假设此时count的值为1),然后在寄存器中将其加1变为2,最后t1先将2加载回内存中,t2也把2加载回内存中,所以两次加1操作只加了一次1
当然上述执行顺序只是无数可能中的一种,可能t1的一组指令还没有执行完,t2就执行了好几组
下面来总结一下线程不安全的原因
1.3 线程不安全的原因
- 线程是随机调度,抢占式执行的
- 修改共享数据,多个线程修改同一个变量
- 多个线程修改共享数据的操作不是原子性,(count++是3个CPU指令,但是赋值操作就是原子性的)
- 内存可见性问题
- 指令重排序
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++;
}
- 这是一个Java的关键字,不是方法
- synchronized后面括号里面写的是锁对象
- 锁对象的用途:用来区分两个线程是否针对同一个对象加锁,如果是,就会出现锁竞争/互斥就会引起阻塞等待,如果不是就不会出现锁竞争,也就不会阻塞等待
-
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 线程安全的概念 线程是随机调度执行的,如果多线程环境下的程序运行的结果符合我们预期则说明线程安全,反之,如果遇到其他结果甚至引起了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来测测你的系统负载能力
背景 近期我们有个号称会有很高很高并发的系统要上线,为了测试一下自己开发的系统的负载能力,准备了点海克斯科技,来看看抗不抗的住。 之前笔者写过用Apache JMeter进行压力测试的文章(传送门👉:https://…...
【论文复现】基于BERT的语义分析实现
📝个人主页🌹:Eternity._ 🌹🌹期待您的关注 🌹🌹 ❀ 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 ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1: 输入:nums [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]示例 2: 输入&#…...
【Zemax光学设计实训三】---激光缩束镜的设计优化
前言与目录 技术设计要求: 设计一个激光扩束镜,使用的波长为1064nm,输入光束直径为10mm,输出光束的直径为2mm,且输入光束和输出光束平行(即平行光入射,平行光出射)。要求只使用两片…...
TCP/IP协议簇自学笔记
摘抄于大学期间记录在QQ空间的一篇自学笔记,当前清理空间,本来想直接删除掉的,但是感觉有些舍不得,因此先搬移过来。 曾经,我只知道socket函数能进行网络间数据的通信,知道tcp/ip协议也是用来进行网络数据…...
Spring Boot教程之十一:获取Request 请求 和 Put请求
如何在 Spring Boot 中获取Request Body? Java 语言是所有编程语言中最流行的语言之一。使用 Java 编程语言有几个优点,无论是出于安全目的还是构建大型分发项目。使用 Java 的优点之一是 Java 试图借助类、继承、多态等概念将语言中的每个概念与现实世…...
计算机网络(二)
ip地址:11010010:01011110:00100100:00010100 子网掩码:11111111:11111111:11111111:11000000 and :11010010:01011110:00100100:00000000 210.94.36.0的下一站为R1 因为255为11111111 192为ÿ…...
如何在Python中进行数学建模?
数学建模是数据科学中使用的强大工具,通过数学方程和算法来表示真实世界的系统和现象。Python拥有丰富的库生态系统,为开发和实现数学模型提供了一个很好的平台。本文将指导您完成Python中的数学建模过程,重点关注数据科学中的应用。 数学建…...
JavaSE——类与对象(5)
一、抽象类 1.1为什么需要抽象类 父类的某些方法,不确定怎么实现,也不需要实现。 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是当前最热门的服务网格产品,已经被广泛应用于各个云厂商和IT互联网公司。企业可以基于Istio轻松构建服务网格,在接入过程中应用代码无需更改,…...
面试小札:Java如何实现并发编程
多线程基础 继承Thread类 定义一个类继承自 Thread 类,重写 run 方法。在 run 方法中编写线程要执行的任务逻辑。例如: java class MyThread extends Thread { Override public void run() { System.out.println("线程执行的任务…...
java-a+b 开启java语法学习
代码 (ab) import java.util.Scanner; //导入 java.util包中的Scanner 类,允许读取键盘输入数据public class Main { // 创建一个公共类 Mainpublic static void main(String[] args) {//程序入口点,main方法Scanner scanner new Scanner(…...
RNN模型文本预处理--数据增强方法
数据增强方法 数据增强是自然语言处理(NLP)中常用的一种技术,通过生成新的训练样本来扩充数据集,从而提高模型的泛化能力和性能。回译数据增强法是一种常见的数据增强方法,特别适用于文本数据。 回译数据增强法 定义…...
maven 中<packaging>pom</packaging>配置使用
在 Maven 项目的 pom.xml 文件中, 元素用于指定项目的打包类型。默认情况下,如果 元素没有被显式定义,Maven 会假设其值为 jar。但是,当您设置 pom 时,这意味着该项目是一个 POM(Project Object Model&…...
【Python中while循环】
一、深拷贝、浅拷贝 1、需求 1)拷贝原列表产生一个新列表 2)想让两个列表完全独立开(针对改操作,读的操作不改变) 要满足上述的条件,只能使用深拷贝 2、如何拷贝列表 1)直接赋值 # 定义一个…...
【深度学习】服务器常见命令
1、虚拟环境的安装位置 先进入虚拟环境 which python2、升序查看文件内容 ls -ltr3、查看服务器主机空间使用情况 df -hdf -h .4、查看本地空间使用情况 du -sh ./*du -sh * | sort -nr5、查找并删除进程 # 查找 ps aux# 删除 kill -KILL pid6、查看服务器配置 lscpuuna…...
技术分析模板
文章目录 概要整体架构流程技术名词解释技术细节小结 概要 提示:这里可以添加技术概要 例如: openAI 的 GPT 大模型的发展历程。 整体架构流程 提示:这里可以添加技术整体架构 例如: 在语言模型中,编码器和解码器…...
【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...
《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》
引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...
通过Wrangler CLI在worker中创建数据库和表
官方使用文档:Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后,会在本地和远程创建数据库: npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库: 现在,您的Cloudfla…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...
Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...
Spring数据访问模块设计
前面我们已经完成了IoC和web模块的设计,聪明的码友立马就知道了,该到数据访问模块了,要不就这俩玩个6啊,查库势在必行,至此,它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据(数据库、No…...
【Redis】笔记|第8节|大厂高并发缓存架构实战与优化
缓存架构 代码结构 代码详情 功能点: 多级缓存,先查本地缓存,再查Redis,最后才查数据库热点数据重建逻辑使用分布式锁,二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...
