【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

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

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

相关文章

【Python】(自定义类)计算语句执行时间

一个玩具&#xff0c;写着来玩的。 用的time模块&#xff0c;代码很简单(所以才说是个玩具) 代码&#xff1a; import time class TimeStamp:__timestampNone__keyNonedef __init__(self,tipsNone,keyNone):self.__timestamp{}self.NewStamp(tips,key)def NewStamp(self,tips,…

rfc4301- IP 安全架构

1. 引言 1.1. 文档内容摘要 本文档规定了符合IPsec标准的系统的基本架构。它描述了如何为IP层的流量提供一组安全服务&#xff0c;同时适用于IPv4 [Pos81a] 和 IPv6 [DH98] 环境。本文档描述了实现IPsec的系统的要求&#xff0c;这些系统的基本元素以及如何将这些元素结合起来…

电源控制系统架构(PCSA)之系统控制处理器组件

目录 6.4 系统控制处理器 6.4.1 SCP组件 SCP处理器Core SCP处理器Core选择 SCP处理器核内存 系统计数器和通用计时器 看门狗 电压调节器控制 时钟控制 系统控制 信息接口 电源策略单元 传感器控制 外设访问 系统访问 6.4 系统控制处理器 系统控制处理器(SCP)是…

AI创作工具:Claude2注册保姆级教程

最近软件打算多接入几个AI写作平台&#xff0c;包括讯飞星火&#xff0c;百度文心&#xff0c;Claude2&#xff0c;这样就能给用户提供更多的写作选择 经过半天的调研&#xff0c;讯飞星火&#xff0c;百度文心一言&#xff0c;接入都比较简单&#xff0c;毕竟是国内的。 在调…

【java】-D参数使用

在开发过程中我们使用开源工具经常会用到在启动命令时候加入一个 -Dxxx 类型的参数。到底-Dxxx是干什么用的了。 官方文档 地址&#xff1a;文档地址 java命令使用 下面是来源于官方文档&#xff1a; java [options] classname [args] java [options] -jar filename [args…

人力资源管理后台 === 基础环境+登陆

目录 1.人力资源项目介绍 1.1 项目架构和解决方案 1.2 课程安排 1.3 课程具备能力 1.4 课程地址 2. 拉取项目基础代码 3.项目目录和入口文件介绍 4.App.vue根组件解析 5.基础设置settings.js和导航守卫permission.js 6.Vuex的结构 7.使用模板中的Icon图标 8.扩展…

node fs模板及蓝桥案例实战

文章目录 介绍文件写入writeFile 异步写入writeFileSync 同步写入appendFile / appendFileSync 追加写入createWriteStream 流式写入 文件读取readFile 异步读取readFileSync 同步读取createReadStream 流式读取 文件移动与重命名文件删除文件夹操作mkdir / mkdirSync 创建文件…

Python基础:JSON保存结构化数据(详解)

1. JSON概念 JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的数据交换格式&#xff0c;易于人阅读和编写&#xff0c;也易于机器解析和生产。   虽然JSON使用JavaScript语法来描述数据对象&#xff0c;但是JSON仍然独立于语言和平台&#xff0c;JSON解…

三菱PLC编码器转速测量功能块(梯形图和ST代码)

编码器转速测量功能块算法公式详细讲解请参考下面文章链接: SMART PLC编码器转速测量功能块(高速计数器配置+梯形图)-CSDN博客文章浏览阅读427次。里工业控制张力控制无处不在,也衍生出很多张力控制专用控制器,磁粉制动器等,本篇博客主要讨论PLC的张力控制相关应用和算法,…

Springboot实现增删改差

一、包结构 二、各层代码 (1)数据User public class User {private Integer id;private String userName;private String note;public User() {super();}public User(Integer i, String userName, String note) {super();this.id i;this.userName userName;this.note note;…

抖音生态融合:开发与抖音平台对接的票务小程序

为了更好地服务用户需求&#xff0c;将票务服务与抖音平台结合&#xff0c;成为了一个创新的方向。通过开发票务小程序&#xff0c;用户可以在抖音平台上直接获取相关活动的票务信息&#xff0c;完成购票、预订等操作&#xff0c;实现了线上线下的有机连接。 一、开发过程 1…

线程池(用于处理Runnable任务或Callable任务)

一&#xff0c;线程池 二&#xff0c; 如何创建线程池 案例&#xff1a; //1,通过ThreadPoolExecuter创建一个线程池对象ExecutorService pool new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS,new LinkedBlockingQueue<>(4),Executors.defaultThreadFactory(),new Thr…