Java线程安全

目录

一.引入

二.介绍

1.概念

2.产生的原因

三.修改操作不是原子性

1.分析问题

2.解决问题(锁)

四.可重入与不可重入

五.死锁

1.引入

2.死锁的三种情况

3.构成死锁的必要条件

六.内存可见性

1.引入

2.产生原因

3.解决问题

七.指令重排序


一.引入

首先用一个经典了例子引出线程安全的问题:

我们创建两个线程和一个count,在每个线程内写一个循环,每一个线程内 count++ 一万次,最后输出count。

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

我们预期的输出结果应该是20000,但事实恰恰相反:

如果我们多运行几次,会发现每次输出的结果都不一样。

那么为什么会这样呢,这就引出了线程安全的问题。

二.介绍

1.概念

简单来说,像上述这种使用了多线程,预期结果和实际结果不同,出现bug,这就是线程不安全。如果一段代码预期结果与实际结果相同,没有bug,那就可以说线程安全。

2.产生的原因

1)操作系统对线程的调度是随机的,是抢占式执行的(根本原因);

2)多个线程同时修改同一个变量;

3)修改操作不是原子性的;

4)内存可见性问题;

5)指令重排序。

三.修改操作不是原子性

操作系统规定好的规则我们不方便修改,多个线程同时修改同一个变量与代码结构强相关,我们通过调整代码结构来解决问题是不通用的。

我们可以通过让修改的操作变成原子性的,这一个解决线程安全的主要方案。

1.分析问题

用的还是上面那个count的例子。

为什么这个例子中的修改操作不是原子性的?

因为如果修改操作只是对应一个cpu指令的话,就认为的原子性的;反之则不是原子性的。像什么:++、--、+=、-=等等都不是原子性的,像什么:基本数据类型的赋值操作(move指令)、单线程操作等都是原子性的。

可见count++这个操作不是原子性的。

count++这个操作对应了三个cpu指令:

1)load,把内存中的值读取到cpu寄存器中;

2)add,把寄存器中的值++;

3)save,把寄存器的值写回内存中。

由于操作系统对线程的调度是随机的,执行任何一个指令都可能会进行线程切换的操作,如下面这个例子:

解释:我们简化一下,假设每个线程就循环一次。t1先load从内存中获得了count值,count=0。这时操作系统突然把线程切换到了t2,t2也load从内存获得了count=0;紧接着t2进行了add操作,此时t2中的count变成了1;最后t2将count值写入了内存中,此时内存中的count的值是1。

这时线程切回t1,t1进行add操作,count变成1;再进行save操作,count值写入内存,内存中的count值是1。

进行完上述操作我们发现:哎?count的值是1。正确答案应该是2。

这里只是举了简单的一个例子,其实三个cpu指令可以随机搭配(因为线程可以随机切换)。如果进行更多的count++,像上述例子一样,count++循环10000次,那变数更多了,可以变成组合就太多太多了。

如果我们将修改操作变成了原子性,那么不就可以实现线程安全了。

补充:

为什么赋值操作不用从内存中读取到cpu,再cpu赋值,最后写回内存这三步?

1)寄存器优化—寄存器复用。一些场景下,源值可能已经在寄存器中,因此赋值操作直接在寄存器中执行即可。

2)局部性原理。最近访问过的数据很可能在不久的将来再次被访问,因此cpu会缓存这部分数据,如果被访问了直接用;

3)硬件特性—存储缓冲区。现代 CPU 通常具有存储缓冲区,允许在不立即写回内存的情况下进行多次赋值操作。CPU 可以将这些赋值操作暂存在存储缓冲区中,然后在合适的时候批量写回内存。这样可以提高 CPU 的执行效率,因为它不需要每次赋值都立即访问内存。同时,存储缓冲区也可以合并多个对同一内存地址的写操作,减少内存访问的次数;

4)编译器优化—常量折叠。如果赋值操作的源值是一个常量,编译器可能会在编译时直接将常量值赋给目标变量,而不需要在运行时进行从内存读取和写回的操作;

5)编译器优化—优化的指令选择。编译器会根据目标 CPU 的架构和指令集选择最有效的指令来实现赋值操作。

2.解决问题(锁)

解决这个问题就引出了锁这个东西。我们可以通过加锁操作将不是原子性的操作,打包变成一个原子性的操作。

加锁的基本思想是通过限制对共享资源的访问,使得在同一时间只有一个线程能够访问和修改该资源。当一个线程获取到锁后,其他线程如果想要访问被锁定的资源,就必须等待锁被释放。

Java中使用了 synchronized 来进行加锁。

首先先介绍一下 synchronized :

synchronized (锁对象){//要执行的操作
}

锁对象就是用来加锁的对象,这个对象可以是任何一种对象,什么都行。

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

上面是使用了 synchronized 进行修改,现在代码可以正常输出预期的答案了:

但是要注意了,这个锁对象一定要是同一个,如果不同的话,跟没加锁有什么区别。比如一个厕所隔间有两个门,你锁了一个,另一个没锁,那不就等于没锁嘛。

另外,一个对象用作了锁对象,但其还可以正常使用,但不鼓励这么做,让一个对象只当锁对象比较好。

上面是给代码块加锁,我们还可以给方法加锁:

class Counter{int count;public synchronized void add(){count++;} 
}

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

四.可重入与不可重入

首先介绍一下不可重入锁,如果再锁里再加一个锁,不可重入锁就是会阻塞等待。对于不可重入锁,在锁里再加锁会引起死锁的问题。

为了解决不可重入锁的问题,Java的 synchronized 是可重入锁。那什么是可重入锁,简单来说就是在锁里还可以再加一个锁,但是并不会阻塞等待。

举个例子:

Object lock=new Object();
Thread t=new Thread(()->{synchronized (lock){synchronized (lock){for (int i = 0; i < 10000; i++) {count++;}}}
});

按照其他的锁,上面就形成死锁了,但是 synchronized 加锁成功后再次要加锁时会直接跳过。

在可重入锁内部记录了“线程持有者”和“计数器”两个信息。

线程持有者的信息保证了如果线程获取锁时发现占用锁的是自己,会直接获取锁,并且让计数器+1。当计数器内的数为0时释放锁,并不是出一个 synchronized 就释放锁了。

五.死锁

1.引入

Object lock1=new Object();
Object lock2=new Object();
Thread t=new Thread(()->{synchronized (lock1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2){for (int i = 0; i < 10000; i++) {count++;}}}
});
Thread t2=new Thread(()->{synchronized (lock2){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock1){for (int i = 0; i < 10000; i++) {count++;}}}
});
t.start();
t2.start();
t.join();
t2.join();
System.out.println(count);

这就是一个典型的死锁场景,把上面的代码翻译成现实中的例子是:小明要玩会手机才写作业,而妈妈要小明写完作业后才能完手机,这就是死锁了。找工作要有实习经验,找实习要有工作经验,这也是死锁。

从上述例子可以看出死锁是一个很严重的事情,死锁会造成系统资源浪费、系统性能下降、系统可靠性下降等问题。

2.死锁的三种情况

1)一个线程,一把锁,连续锁两次;

2)两个线程,两把锁,每个线程获取一把锁后,尝试获取另一把锁,这就是上面举的那个例子;

3)n个线程,m把锁。

对于第三种情况有一个非常经典的模型方便大家理解,哲学家模型。

问题是这样的:一张圆桌上坐着五名哲学家,在每两名哲学家中间放着一根筷子,哲学家们的生活方式只做两件事:思考和进餐。饥饿时哲学家必须同时拿起两只筷子时才能进餐,进餐完毕后,放下筷子,进行思考。如果筷子被紧挨着的一名哲学家使用着,则不能争抢,必须等待,当这名哲学家就餐完毕后,放下筷子,才能使用。

当五个哲学家同时想进餐,他们都拿起了自己右手边的筷子,那么就是产生死锁问题,大家都无法吃饭->无法吃饭就无法放下->无法放下就无法获得两支筷子去吃饭,“闭环了”。

3.构成死锁的必要条件

1)锁是互斥的(锁的基本性质),如果一个线程拿到了锁,另外的线程想要拿到锁必须等待;

2)锁是不可抢占的(锁的基本特性),一个锁被线程A拿了,线程B也想要这个锁,线程B是不能强抢过来的;

3)请求和保持,当一个进程已经占有了一些资源,同时又去请求新的资源,但是在新资源尚未得到满足之前,该进程不会释放已经占有的资源;

4)循环等待,存在一组进程,其中每个进程都在等待另一个进程所占有的资源,形成一个循环等待的链条。

要想出现死锁,必须同时满足上面的四个条件,只要有一个不满足,死锁就不会出现了。

上述四个条件中,1和2是锁的基础的东西,我们无法左右。但是我们可以通过破坏3和4来避免死锁。

还是上面哲学家的例子,有一个非常明显的条件就是大家都在等对方把筷子放下。如果我们规定好了拿筷子的顺序,那么就可以破除这个循环等待的条件了。

我们给每个哲学家和筷子编号,哲学家从2到5到1开始获取筷子,每次都是先获取编号小的筷子,后获取编号大的筷子。哲学家2获取了1号筷子,哲学家3获取了2号筷子...到哲学家5时获得1号筷子,到哲学家1想获得1号筷子,但是1号筷子已经被哲学家2拿走了,所以哲学家1没有拿到筷子。

新一轮开始,大家开始拿大号的筷子,但是2号筷子被哲学家3拿走,因此哲学家2无法吃饭...哲学家5可以获得5号筷子,因为大家没有取5号筷子,因此哲学家5号吃上了饭。吃完饭后哲学家5退出。因此类推,直到大家都吃完饭了。

六.内存可见性

1.引入

还是通过一个例子来引出内存可见性问题:

private static int fg;
public static void main(String[] args){Thread t1=new Thread(()->{while(fg==0){}System.out.println("线程t1结束");});Thread t2=new Thread(()->{Scanner scan=new Scanner(System.in);System.out.println("输入fg的值:");fg=scan.nextInt();});t1.start();t2.start();
}

按照我们的期望,当我们输入fg(随便一个不等于0的数)时,线程t1会结束。但当我们输入一个不等于0的数的时候,t1并没有结束。

2.产生原因

要想解决这个问题,首先要明白这个问题是怎么产生的。

上述代码产生问题的罪魁祸首是编译器和JVM的优化。编译器会对我们写的代码进行优化,但是在多线程的环境,优化完的代码可能与实际的逻辑有偏差。

while(fg==0){}

while每次循环都会去内存中读取fg的值来判断是否退出循环,JVM发现,每次取的fg值都是0,每次去内存中取值都要消耗不少时间。因此JVM对此进行了优化,将从内存中读取值的操作换成从寄存器中读取值这样就可以提高效率了。其实这种优化在上面修改操作不是原子性那里有提到过。

此时,我们输入fg的值,我们输入的值是存储在内存中的,而while的fg值是从寄存器中读取的,所以无法改变while中的fg值。

3.解决问题

Java中引入了 volatile 关键词。

使用 volatile 修饰的变量在编译器对这个变量进行读取操作时,不会被优化到寄存器中。使用 volatile 关键字修饰的变量,当一个线程对其进行修改时,这个修改会立即被写入主内存,并且其他线程能够立即看到这个修改。

因此我们可以修改一下前面的代码了:

private static volatile int fg;
public static void main(String[] args){Thread t1=new Thread(()->{while(fg==0){}System.out.println("线程t1结束");});Thread t2=new Thread(()->{Scanner scan=new Scanner(System.in);System.out.println("输入fg的值:");fg=scan.nextInt();});t1.start();t2.start();
}

此时的结果就是我们想要的了:

七.指令重排序

属于编译器优化的一种。其会保证在原有的逻辑不变的情况下,调整原来代码的顺序,以达到提升效率。举一个例子:我想泡茶喝,我有一系列任务想要完成,洗茶具->烧水->泡茶。当我真正执行这些任务的时候发现我完全可以先烧水,在烧水的时候去洗茶具,这样可以节省时间,提升效率。不论是原来的顺序还是后来我们调整的顺序,我都能完成喝茶这个任务。

这就是指令重排序,编译器会根据情况对指令进行重排序。这个行为在单线程来看没有上面问题,但是在多线程中就会出现问题。

比如我们想要new一个对象,这个行为会经历一下几个指令:1.申请内存空间;2.在空间中构造对象;3.将内存空间的首地址赋值给引用变量。编译器如果对上面的三步进行重排序,让他们的顺序变成了132,如果先执行了13,这时线程切换了,另一个线程使用了还没有创建的对象(因为2没有执行),这时就出现错误了。

这个问题也可以使用 volatile 来解决。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

让你的 IDEA 使用更流畅 | IDEA内存修改

随着idea使用越来越频繁&#xff0c;笔者最近发现使用过程中有时候会出现卡顿现象&#xff0c;例如&#xff0c;启动软件变慢&#xff0c;打开项目的速度变慢等&#xff1a; 因此如果各位朋友觉得最近也遇到了同样的困惑&#xff0c;不妨跟着笔者一起来设置IDEA的内存大小吧~ …

虚拟现实在制造业中的应用

当你想到制造业中的虚拟现实技术时&#xff0c;你脑海中闪过的第一个念头是什么&#xff1f;从目前来看&#xff0c;只需几年时间&#xff0c;制造业就将离不开虚拟现实技术的帮助。实施虚拟现实应用对制造业来说都有诸多好处。通常情况下&#xff0c;制造设施都是由各种机器组…

基于neo4j的学术论文关系管理系统

正在为毕业设计头疼&#xff1f;又或者在学术研究中总是找不到像样的工具来管理浩瀚的文献资料&#xff1f;今天给大家介绍一款超实用的工具——基于Neo4j的学术论文关系管理系统&#xff0c;让你轻松搞定学术文献的管理与展示&#xff01;&#x1f389; 系统的核心是什么呢&a…

一个基于.NET8+WPF开源的简单的工作流系统

项目介绍 AIStudio.Wpf.AClient 是一个基于 WPF (Windows Presentation Foundation) 构建的客户端框架&#xff0c;专为开发企业级应用而设计。该项目目前版本为 6.0&#xff0c;进行了全面优化和升级&#xff0c;提供了丰富的功能和模块&#xff0c;以满足不同场景下的开发需…

清华大学《2022年+2021年822自动控制原理真题》 (完整版)

本文内容&#xff0c;全部选自自动化考研联盟的&#xff1a;《清华大学822自控考研资料》的真题篇。后续会持续更新更多学校&#xff0c;更多年份的真题&#xff0c;记得关注哦~ 目录 2022年真题 2021年真题 Part1&#xff1a;2022年2021年完整版真题 2022年真题 2021年真题…

图层之间的加减法

如右图所示&#xff0c;正方形ABCD的边长为1cm&#xff0c;AC、BD分别是以点D和点C为圆心、1cm为半径作的圆弧。问阴影部分a的面积比阴影部分b小____? 这道题首先是固定的图形&#xff0c;形状已经确定了&#xff0c;按理说a和b的面积都可以求出来&#xff0c;但是题目问的是b…

ALIGN_ Tuning Multi-mode Token-level Prompt Alignment across Modalities

文章汇总 当前的问题 目前的工作集中于单模提示发现&#xff0c;即一种模态只有一个提示&#xff0c;这可能不足以代表一个类[17]。这个问题在多模态提示学习中更为严重&#xff0c;因为视觉和文本概念及其对齐都需要推断。此外&#xff0c;仅用全局特征来表示图像和标记是不…

颐驰06持续交付,明日科技赋能出行生活

在全球智能出行领域&#xff0c;自动驾驶技术的发展一直是行业关注的焦点。不久前&#xff0c;特斯拉发布的自动驾驶出租车引发了全球关注&#xff0c;但由于缺乏具体的技术细节&#xff0c;导致投资者信心受挫&#xff0c;特斯拉股票一度下跌近10%。与此同时&#xff0c;中国车…

springboot079信息化在线教学平台的设计与实现(论文+源码)_kaic

信息化在线教学平台的设计与实现 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了信息化在线教学平台的开发全过程。通过分析信息化在线教学平台管理的不足&#xff0c;创建了一个计算机管理信息化在线教学平台…

家政服务管理系统小程序ssm+论文源码调试讲解

第2章 开发环境与技术 基于微信小程序的家政服务管理系统的编码实现需要搭建一定的环境和使用相应的技术&#xff0c;接下来的内容就是对基于微信小程序的家政服务管理系统用到的技术和工具进行介绍。 2.1 MYSQL数据库 本课题所开发的应用程序在数据操作方面是不可预知的&…

Java后端面试题:Java基础篇

目录 Java基础 1.请你说说Java中基本数据类型的bit长度&#xff1f; 2.switch支持哪些数据类型&#xff1f;支持long么&#xff1f; 3.讲一下常见编码方式&#xff1f; 4.char能不能存储中文&#xff1f; 5.为什么数组索引从0开始呢&#xff1f;假如从1开始不行吗&#xf…

探秘 MySQL 数据类型的艺术:性能与存储的精妙平衡

文章目录 前言&#x1f380;一、数据类型分类&#x1f380;二、整数类型&#xff08;举例 TINYINT 和 INT &#xff09;&#x1f3ab;2.1 TINYINT 和 INT 类型的定义2.1.1 TINYINT2.1.2 INT &#x1f3ab;2.2 表的操作示例2.2.1 创建包含 TINYINT 和 INT 类型的表2.2.2 插入数据…

【JavaSE】认识String类,了解,进阶到熟练掌握

#1024程序员节 | 征文# 下面就让博主带领大家一起解决心中关于String类的疑问吧~~~ 1.字符串构造&#xff1a; 第一种和第二种&#xff08;有一定的区别&#xff0c;在常量池上&#xff09; public static void main(String[] args) { // 使用常量串构造 String s1 "h…

最新PHP网盘搜索引擎系统源码 附教程

简介 最新PHP网盘搜索引擎系统源码 附教程 这是一个基于thinkphp5.1MySQL开发的网盘搜索引擎&#xff0c;可以批量导入各大网盘链接&#xff0c;例如百度网盘、阿里云盘、夸克网盘等。 功能特点&#xff1a;网盘失效检测&#xff0c;后台管理功能&#xff0c;网盘链接管理&a…

(三)第一个Qt程序“Qt版本的HelloWorld”

一、随记 我们在学习编程语言的时候&#xff0c;各种讲解编程语言的书籍中通常都会以一个非常经典的“HelloWorld”程序展开详细讲解。程序虽然简短&#xff0c;但是“麻雀虽小&#xff0c;五脏俱全”&#xff0c;但是却非常适合用来熟悉程序结构、规范&#xff0c;快速形成对编…

axure中继器

学习了一点中继器&#xff0c;完成管理后台左侧菜单的功能设置。 样式不太好看&#xff0c;只分享功能&#xff01;这篇写的有点潦草&#xff0c;只供参考。 点击展开隐藏一级菜单 下面是配置交互信息 二级菜单要组合&#xff0c;加载时隐藏&#xff0c;点击一级菜单切换显隐…

在linux系统中查看具体文件大小命令

#!/bin/bash# 检查是否提供了路径 if [ "$#" -ne 1 ]; thenecho "用法: $0 <路径>"exit 1 fiDIRECTORY$1# 检查路径是否存在 if [ ! -d "$DIRECTORY" ]; thenecho "错误: 目录 $DIRECTORY 不存在."exit 1 fi# 定义命令数组 comm…

Linux:定时任务

目录 服务 配置命令 配置格式 定时任务案例 每2分钟同步时间 每天半夜备份文件 服务说明 相关目录&#xff1a; /var/spool/cron/ 用户的定时任务配置文件目录&#xff08;用户制定的任务都在该目录&#xff09; /var/log/cron 定时任务日志 /etc/crontab 系统定时任…

Ajax:请求 响应

Ajax&#xff1a;请求 & 响应 AjaxjQuery的Ajax接口$.get$.post$.ajax PostMan 接口测试getpost Ajax 浏览器中看到的数据&#xff0c;并不是保存在浏览器本地的&#xff0c;而是实时向服务器进行请求的。当服务器接收到请求&#xff0c;就会发回一个响应&#xff0c;此时浏…

基于信号分解和多种深度学习结合的上证指数预测模型

大家好&#xff0c;我是带我去滑雪&#xff01; 为了给投资者提供更准确的投资建议、帮助政府和监管部门更好地制定相关政策&#xff0c;维护市场稳定&#xff0c;本文对股民情绪和上证指数之间的关系进行更深入的研究&#xff0c;并结合信号分解、优化算法和深度学习对上证指数…