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

【JavaEE初阶】线程安全问题及解决方法

目录

一、多线程带来的风险-线程安全

1、观察线程不安全

2、线程安全的概念

3、线程不安全的原因

4、解决之前的线程不安全问题 

5、synchronized 关键字 - 监视器锁 monitor lock

5.1 synchronized 的特性

5.2 synchronized 使用示例  

5.3 Java 标准库中的线程安全类 


一、多线程带来的风险-线程安全

1、观察线程不安全

public class ThreadDemo2 {private static long count = 0;public static void main(String[] args) throws InterruptedException{Thread t1 = new Thread(()->{for (int i = 1;i <= 500000;i++) {count++;}});Thread t2 = new Thread(()->{for (long i = 0;i < 500000;i++) {count++;}});t1.start();t2.start();t1.join();t2.join();//存在线程安全问题,输出的结果可能不准确System.out.println("count= "+count);}
}

其运行结果:

明显这个结果和我们的预期是不一样的,这是因为存在线程安全问题。若把count++的操作在一个单线程环境下运行 ,便不会出现这样的问题。下面我们来说一下线程安全问题。

2、线程安全的概念

想给出⼀个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
线程安全,在单线程环境下和多线程环境下都不会出现问题。

3、线程不安全的原因

  • 线程调度是随机的
这是线程安全问题的根本原因 ;
随机调度使⼀个程序在多线程环境下,执行顺序存在很多的变数;
程序猿必须保证 在任意执行顺序下 , 代码都能正常工作。
  •  修改共享数据

多个线程修改同⼀个变量

上面的线程不安全的代码中,涉及到多个线程针对 count 变量进行修改, 此时这个 count 是⼀个多个线程都能访问到的 "共享数据" 。

  • 原子性  

什么是原子性

我们把⼀段代码想象成⼀个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

⼀条 java 语句不⼀定是原子的,也不一定只是一条指令

比如,刚才我们看到的 count++,其实是由三步操作组成的:
  1. 从内存把数据读到 CPU 寄存器中
  2. 进行数据更新
  3. 把数据写回到内存

那么不保证原子性会给多线程带来什么问题呢?

如果不保证原子性,⼀个线程正在对⼀个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
这点也和线程的抢占式调度密切相关,如果线程不是 "抢占" 的,就算没有原子性,问题也不⼤。
  •  可见性

 可见性指,⼀个线程对共享变量值的修改,能够及时地被其他线程看到。这里先不过多介绍。

  • 指令重排序 
什么是代码重排序?
假设有⼀段代码是这样的:
  1. 去前台取下 U 盘
  2. 去教室写 10 分钟作业
  3. 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑⼀次前台。这种叫做指令重排序

编译器对于指令重排序的前提是 "保持逻辑不发⽣变化". 这⼀点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

重排序是⼀个比较复杂的话题, 涉及到 CPU 以及编译器的⼀些底层⼯作原理, 此处不做过多讨论。 

4、解决之前的线程不安全问题 

 解决之后的代码:

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

这时的结果就一定是1000000,如图:

下面就给大家解释一下,这个线程不安全的问题是如何解决的。

5、synchronized 关键字 - 监视器锁 monitor lock

5.1 synchronized 的特性

1) 互斥  

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同⼀个对象 synchronized 就会 阻塞等待
  • 进入 synchronized 修饰的代码块,相当于 加锁
  • 退出 synchronized 修饰的代码块,相当于 解锁

synchronized用的“锁”是存在Java对象“头”里面的。

可以粗略理解成, 每个对象在内存中存储的时候, 都存有⼀块内存表示当前的 "锁定" 状态(类似于厕所 的 "有人/无人").
如果当前是 "无人" 状态, 那么就可以使用, 使用时需要设为 "有人" 状态.
如果当前是 "有人" 状态, 那么其他人无法使用, 只能排队

 

理解 "阻塞等待":

针对每⼀把锁, 操作系统内部都维护了⼀个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试 进行加锁, 就加不上了, 就会阻塞等待, ⼀直等到之前的线程解锁之后, 由操作系统唤醒⼀个新的线程, 再来获取到这个锁。
注意:
  • 上⼀个线程解锁之后, 下⼀个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这也就是操作系统线程调度的⼀部分工作.
  • 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不⼀定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

 synchronized的底层是使用操作系统的mutex lock实现的。

2) 可重入

synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题; 

理解 "把自己锁死" :
一个线程没有释放锁, 然后又尝试再次加锁。
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第⼀次的锁被释放, 才能获取到第二 个锁. 但是释放第⼀个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行 解锁操作. 这时候就会死锁。

这样的锁称为 不可重入锁。

Java 中的 synchronized 是 可重入锁, 因此没有上面的问题。

for (int i = 0; i < 50000; i++) {synchronized (locker) {synchronized (locker) {count++;}}
}
在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.
  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

5.2 synchronized 使用示例  

synchronized 本质上要修改指定对象的 "对象头",从使用度来看,synchronized 也势必要搭配⼀个具体的对象来使用。

 1) 修饰代码块: 明确指定锁哪个对象.

 锁任意对象:

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

锁当前对象:

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

2) 直接修饰普通方法: 锁的 SynchronizedDemo 对象 

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

3) 修饰静态方法: 锁的 SynchronizedDemo 类的对象 

public class SynchronizedDemo {public synchronized static void method() {}
}
我们重点要理解,synchronized 锁的是什么. 两个线程竞争同⼀把锁, 才会产生阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产⽣竞争.

5.3 Java 标准库中的线程安全类 

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施:
  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder
但是还有⼀些是线程安全的. 使用了⼀些锁机制来控制:
  • Vector (不推荐使⽤)
  • HashTable (不推荐使⽤)
  • ConcurrentHashMap
  • StringBuffer

StringBuffer 的核心方法都带有 synchronized .  

还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的: String

相关文章:

【JavaEE初阶】线程安全问题及解决方法

目录 一、多线程带来的风险-线程安全 1、观察线程不安全 2、线程安全的概念 3、线程不安全的原因 4、解决之前的线程不安全问题 5、synchronized 关键字 - 监视器锁 monitor lock 5.1 synchronized 的特性 5.2 synchronized 使用示例 5.3 Java 标准库中的线程安全类…...

uniapp高德、百度、腾讯地图配置 SHA1

uniapp高德、百度、腾讯地图配置 SHA1 当winr弹出cmd弹框后输入 keytool -list -v -keystore debug.keystore 显示keytool 不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。可以先看看是否有下载jdk且配置了环境变量&#xff0c;具体操作如下&#xff1a;keyto…...

[AutoSAR存储] 车载存储层次 和 常用存储芯片概述

公知及经验整理&#xff0c;原创保护&#xff0c;禁止转载。 专栏 《AutoSAR存储》 <<<< 返回总目录 <<<< 1 存储系统层次 先抛个问题&#xff0c; 为什么要划分存储器的层次&#xff1f; 速度越快&#xff0c;但成本越高&#xff0c;从经济的角度规…...

进程并发-信号量经典例题-吸烟者问题

1 题目描述 吸烟者问题 在一个房间内有三个吸烟者和一个香烟供应者。为了制造并抽掉香烟&#xff0c;每个吸烟者需要三样物品&#xff1a;烟草、纸和火柴&#xff0c;供应者有丰富物品提供。在三个吸烟者中&#xff0c;第一个有自己的烟草&#xff0c;第二个有自己的纸&#…...

[ruby on rails] array、jsonb字段

一、jsonb # 新增 add_column :shi_tis, :setting, :jsonb, default: {}# string转jsonb def changechange_column :users, :setting, :jsonb, using: setting::jsonb, default: {} end# 加索引 add_index :users, :setting, using: :gin # 这样就为setting jsonb字段创建了一…...

Feign接口请求返回异常 no suitable HttpMessageConvert found for response type

问题场景&#xff1a; 后端调用feign接口请求, 接口返回异常, no suitable HttpMessageConvert found for response type 问题描述 报错异常如下&#xff1a; //根据图片特征 去查询人员信息ResultVo<List> personVos ipbdFaceLibPersonApi.queryFacePersonByFeatur…...

【brpc学习实践九】mbvar及bvar可观测

概念、学习地址 mbvar中有两个类&#xff0c;分别是MVariable和MultiDimension&#xff0c;MVariable是多维度统计的基类&#xff0c;MultiDimension是派生模板类。也是主要用来多多线程计数用的。这里用到再详细去了解即可 https://github.com/luozesong/brpc/blob/master/do…...

Vue 3

Vu3 简述: 快速上手,开发即用,具体知识参考官方文档 具备知识 Vite 了解即可,使用时按需配置,更多参考官方文档( https://cn.vitejs.dev) 简述: 极速响应工具 (构建、启动、更新、插件使用等) 核心思想: 依赖 和 源码 工作原理: 引入rollup: 灵活,相比webpack速度快,…...

GitHub Copilot 替代品?

应该没人不知道代码补全这个东西了吧&#xff0c;第一次使用 GitHub Copilot 之后&#xff0c;只觉得真香&#xff0c;现在居然还有一点离不了了。后面因为收费原因&#xff0c;就没再用了&#xff0c;找了一个 tabnine 替代&#xff0c;用了几天&#xff0c;体验是真的比不上 …...

设计循环队列(详解)

呀哈喽&#xff0c;我是结衣 今天给大家带来的内容如标题所述&#xff0c;我们来设计环形队列&#xff0c;虽然队列没有讲&#xff0c;但是我就是想讲啊。那么环形队列现在开始。 队列的属性 在设计环形队列前&#xff0c;我们先要了解队列的特点&#xff08;先进先出&#x…...

【Python】Vscode解决Python中制表符和空格混用导致的缩进问题

【Python】Vscode解决Python中制表符和空格混用导致的缩进问题 文章目录 【Python】Vscode解决Python中制表符和空格混用导致的缩进问题1. 问题来源2. 解决Reference 1. 问题来源 在python中使用缩进来进行代码块的分区&#xff0c;通常来说python的一个缩进包含4个空格&#…...

CocosCreator 面试题(十六)Cocos Creator 节点池的基本原理是什么?如何使用?

一、Cocos Creator 节点池的基本原理是什么&#xff1f; Cocos Creator 是一个游戏开发引擎&#xff0c;它提供了节点池&#xff08;Node Pool&#xff09;的功能&#xff0c;用于管理和重用游戏中的节点对象。节点池的基本原理如下&#xff1a; 创建初始节点&#xff1a;在游戏…...

VUE留言板

效果预览图 完整代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>作业</title><styl…...

【办公软件】电脑开机密码忘记了如何重置?

这个案例是家人的电脑&#xff0c;已经使用多年&#xff0c;又是有小孩操作过的&#xff0c;所以电脑密码根本不记得是什么了&#xff1f;那难道这台电脑就废了吗&#xff1f;需要重新装机吗&#xff1f;那里面的资料不是没有了&#xff1f; 为了解决以上问题&#xff0c;一般…...

PTA NeuDS-数据库题目集

一.判断题 1.在数据库中产生数据不一致的根本原因是冗余。T 解析&#xff1a;数据冗余是数据库中产生数据不一致的根本原因&#xff0c;因为当同一数据存储在多个位置时&#xff0c;如果其中一个位置的数据被修改&#xff0c;其他位置的数据就不一致了。因此&#xff0c;在数据…...

Redis深入理解-内核请求处理流程、数据传输协议

Redis 内核级请求处理流程 Redis Server 其实就是 Linux 服务器中的一个进程 主要还是下图的流程 应用先和 server 端建立 TCP 连接建立连接之后&#xff0c;server 端就会有一个与该客户端通信的 socket&#xff0c;客户端的读写请求发送到服务端的 socket那么通过 IO 多路…...

Mac电脑卸载/删除nodejs

使用命令行卸载 Node.js 第一步&#xff1a;打开终端&#xff0c;输入以下命令显示 Node.js 的安装路径&#xff1a; which node执行该命令后&#xff0c;会显示安装路径&#xff1a; /usr/local/bin/node第二步&#xff1a;输入以下命令删除 Node.js 相关的文件&#xff1a;…...

C语言之内存函数

C语言之内存函数 文章目录 C语言之内存函数1. memcpy 使⽤和模拟实现1.1 memcpy 函数的使用1.3 memcpy的模拟实现 2. memmove 使⽤和模拟实现2.1 memmove 函数的使用2.2 memmove的模拟实现 3. memset 函数的使用4. memcmp 函数的使⽤ 1. memcpy 使⽤和模拟实现 函数声明如下&a…...

基本数据结构二叉树(1)

目录 1.树概念及结构 1.1树的概念 1.2 树的相关概念 1.3 树的表示 1.4 树在实际中的运用&#xff08;表示文件系统的目录树结构&#xff09; 2.二叉树概念及结构 2.1概念 2.2现实中的二叉树&#xff1a; 2.3 特殊的二叉树&#xff1a; 2.5 二叉树的存储结构 2. 链式存…...

【python】Python将100个PDF文件对应的json文件存储到MySql数据库(源码)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…...

SkyWalking 10.2.0 SWCK 配置过程

SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外&#xff0c;K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案&#xff0c;全安装在K8S群集中。 具体可参…...

【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表

1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...

Frozen-Flask :将 Flask 应用“冻结”为静态文件

Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是&#xff1a;将一个 Flask Web 应用生成成纯静态 HTML 文件&#xff0c;从而可以部署到静态网站托管服务上&#xff0c;如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...

相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)

【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...

Python如何给视频添加音频和字幕

在Python中&#xff0c;给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加&#xff0c;包括必要的代码示例和详细解释。 环境准备 在开始之前&#xff0c;需要安装以下Python库&#xff1a;…...

【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)

1.获取 authorizationCode&#xff1a; 2.利用 authorizationCode 获取 accessToken&#xff1a;文档中心 3.获取手机&#xff1a;文档中心 4.获取昵称头像&#xff1a;文档中心 首先创建 request 若要获取手机号&#xff0c;scope必填 phone&#xff0c;permissions 必填 …...

push [特殊字符] present

push &#x1f19a; present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中&#xff0c;push 和 present 是两种不同的视图控制器切换方式&#xff0c;它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...

快刀集(1): 一刀斩断视频片头广告

一刀流&#xff1a;用一个简单脚本&#xff0c;秒杀视频片头广告&#xff0c;还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农&#xff0c;平时写代码之余看看电影、补补片&#xff0c;是再正常不过的事。 电影嘛&#xff0c;要沉浸&#xff0c;…...

破解路内监管盲区:免布线低位视频桩重塑停车管理新标准

城市路内停车管理常因行道树遮挡、高位设备盲区等问题&#xff0c;导致车牌识别率低、逃费率高&#xff0c;传统模式在复杂路段束手无策。免布线低位视频桩凭借超低视角部署与智能算法&#xff0c;正成为破局关键。该设备安装于车位侧方0.5-0.7米高度&#xff0c;直接规避树枝遮…...

前端中slice和splic的区别

1. slice slice 用于从数组中提取一部分元素&#xff0c;返回一个新的数组。 特点&#xff1a; 不修改原数组&#xff1a;slice 不会改变原数组&#xff0c;而是返回一个新的数组。提取数组的部分&#xff1a;slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...