[Java EE] 多线程(三):线程安全问题(上)

ops/2024/9/24 7:34:53/

1. 线程安全

1.1 线程安全的概念

如果多线程环境下代码运行的结果不符合我们的预期,则我们说存在线程安全问题,即程序存在bug,反之,不存在线程安全问题.

1.2 线程不安全的原因

我们下面举出一个线程不安全的例子:我们想要在两个线程中对count进行++操作

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

运行结果如下:
在这里插入图片描述
但是这里我们预期的结果是100000,这里我们看到,实际结果和预期结果相差甚远,这便是产生了线程安全问题,使得程序出现了bug,我们要想解决上述的bug,我们必须先了解清楚bug产生的原因.

  1. 线程调度是随机的
    这是线程安全问题的罪魁祸首
    随机调度使⼀个程序在多线程环境下,执行顺序存在很多的变数.
    程序猿必须保证在任意执行顺序下,代码都能正常工作.
    某个线程在执行指令的过程中,当他执行任何一个指令的时候,都有可能被其他线程抢占走CPU.
  2. 修改共享数据
    多个线程同时修改同一个变量.上面的代码中,就都是针对count进行修改.
  3. 原子性
    在前面,我们有给大家提到过事务的原子性,大家还记得我们的助教迪卢克姥爷吗?
    在这里,多线程的原子性其实和事务的原子性大相径庭.我们在这里首先要理解什么是多线程中的原子性:

有请助教:达达利亚,钟离
达达利亚和钟离都到了一台ATM机前来取钱,现在每一台ATM机前都有一个门,一把锁,当达达利亚进去之后,门就会自动上锁,这样钟离便不会对达达利亚取钱的过程造成干扰,在达达利亚取完钱之前,钟离只可以在外面排队等待,在达达利亚取完钱之后,钟离才可以进入.也就是在tread线程对count进行修改的时候,tread1线程不可以对tread修改count的过程进行干扰,这便保证了原子性.反之如果钟离对达达利亚取钱的过程造成了干扰,这便不保证原子性.
在这里插入图片描述

一条Java语句不一定是原子的,也不一定是一条指令:
我们回到线程这里,那么如果拿上面这个存在线程安全问题的代码(不保证原子性的代码),那么他的底层原理是什么样子的呢:

  • 首先tread和tread1同时读到count=0
  • tread线程对count进行++之后放入内存之后,count变为1
  • tread1对线程进行++之后,对上一个count=1的值进行了覆盖,count还是1.
  • 这便会引起bug
    在这里插入图片描述
  1. 内存可见性
  2. 指令重排序
    后续介绍

1.3 解决线程安全问题

要想解决线程安全问题,我们必须要从原因来入手:

  • 从原因一入手:这是多线程已有的特性,无法干预.
  • 从原因二入手:这是一个切入点,**但是不普适,只针对特殊的场景可以做到,**比如String把变量设置为不可变对象,就是为了保证线程安全问题.在对上一个String进行修改的时候,其实在底层又new了一个新的String,修改的实际上不是同一个变量.
  • 从原因三入手:这是一个普适性比较高的切入点,我们想象,我们是否也可以有一把向ATM机那样的锁,来保证线程的原子性呢,答案是有.我们可以使用synchronized关键字来对线程进行上锁.通过上锁操作来把非原子的操作打包为一个原子的操作.保证tread线程对count计算的结果写入内存中在tread1线程读取内存中的count之后,使得它们呈现串行化执行.
  • 从原因四和五入手,后续介绍.

1.4 synchronized关键字—>监视器锁

为了解决上述线程安全问题,我们使用synchronized对上述代码的线程进行加锁:

java">public class Demo11 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object o = new Object();//锁对象Thread thread = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o) {//线程上锁count++;}}});Thread thread1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o){//拿到的都是o锁,产生锁互斥count++;}}});thread.start();thread1.start();thread.join();thread1.join();System.out.println(count);}
}
  • 首先,什么是锁:
    锁本质上是一个OS提供的功能,通过API给到了应用程序,JVM再对这样的API进行包装.这里我们就可以把锁简单地理解为一个不管类型,不管名字,不管是否存在泛型的任意变量,作用上有且只有一个,就是用来区分两个线程知否针对同一个对象加锁.
  • 如何对线程上锁:
    在一个线程中,在某一行使用synchronized ( )关键字,并在括号中传入锁对象,就证明从这一行的{开始,就开始对线程进行了上锁,直到}解锁.

当我们了解完synchronized的第一个特性之后,我们就知道上述上锁的过程是怎么回事了.

1.4.1 synchronized的使用实例

  1. 修饰代码块
java">public class SynchronizedDemo {private Object locker = new Object();public void method() {synchronized (locker) {//一系列操作}}
}
  1. 锁当前对象
java">public class SynchronizedDemo {public void method() {synchronized (this) {}}
}
  1. 直接修饰普通方法
java">public class SynchronizedDemo {public synchronized void methond() {}
}

一旦有线程调用该方法,就会上锁.

  1. 修饰静态方法
java">public class SynchronizedDemo {public synchronized static void method() {}
}

1.4.2 synchronized的特性

  1. 互斥性与锁竞争
    在tread线程对count进行++的时候,在count++的外围,我们使用synchronized关键字对count++进行了包裹,由于tread线程启动比tread1早,也就是在此时,线程tread已经拿到了o这把锁.此时由于tread1线程也在RUNNABLE状态,它也想拿到o这把锁.但是发现,o这把锁已经被tread线程占用了,只能阻塞等待,等待tread解锁.tread1进入BLOCKED状态.此时锁就产生了互斥性.
    解锁之后,由于系统调度线程的随机性,tread和tread1继续竞争o锁,便会产生锁竞争.
    在这里插入图片描述

我们举个例子来说明:
有请助教: 小乔,周瑜,兰陵王
由于兰陵王比周瑜先到一步,所以小乔先和兰陵王贴贴 了一段时间.在这里插入图片描述
兰陵王完事之后,兰陵王对小乔解锁,但是兰陵王又觉得自己还没有和小乔贴贴够,但是周瑜又向进去和自己的爱人贴贴,此时兰陵王和周瑜便产生了锁竞争,谁都向对小乔上锁.
在这里插入图片描述

如果两个线程对于两个不同的锁进行引用加锁,也就不会出现锁竞争问题:

java">public class Demo12 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object o1 = new Object();Object o2 = new Object();Thread thread = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o1) {//o1对线程上锁count++;}}});Thread thread1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o2){//拿到的是o2锁,不会产生锁互斥count++;//线程安全问题仍然存在}}});thread.start();thread1.start();thread.join();thread1.join();System.out.println(count);}
}

但是这样还是会产生线程安全问题.
运行结果:
在这里插入图片描述

讨论:join()和上锁的区别
join是在tread全部执行完成之后,再去执行tread1,而加锁是并发执行.
在join等待的时候是WAITING状态,而在上锁过程中是BLOCKED状态.在这里插入图片描述在这里插入图片描述

  1. 可重入与不可重入(死锁)
    我们思考这样几个场景:
  • 场景一:一个线程一把锁
java">public class Demo13 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object o = new Object();Thread thread = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o) {//对线程上锁synchronized (o){//又上了一次锁count++;}}}});thread.start();thread.join();System.out.println(count);}
}

上面的代码tread两次利用o上锁,我们来思考,在第二次上锁的时候,会不会因为锁的互斥性,而使得tread线程产生阻塞,那就自己把自己锁死了,产生便了死锁.

举例说明:
有请助教:钟离
假如钟离在上厕所…在这里插入图片描述

如果产生上述情况,我们称该锁为不可重入锁.如c++,python中自带的锁,都是不可重入锁,一旦像上面那样写,就锁死了.
但是Java中的锁是可重入锁,对一个线程使用相同的锁进行多次加锁之后,不会出现锁死的情况.不会产生锁冲突.可见Java的创始者为了不让我们Java程序员写出bug,真的是操碎了心!!!
在这里插入图片描述
可重入锁的原理:
在可重⼊锁的内部,包含了"线程持有者"和"计数器"两个信息.
• 如果某个线程加锁的时候,先判断这个线程是否被加锁,如果没有,则加锁,如果发现锁已经被⼈占⽤,但是恰好占⽤的正是⾃⼰,那么仍然可以继续获取到锁,并让计数器⾃增.
• 解锁的时候计数器递减为0的时候,才真正释放锁.(才能被别的线程获取到)

举例说明:
有请助教:小乔,周瑜,兰陵王

在这里插入图片描述
在这里插入图片描述

那么什么时候Java会产生死锁呢?

  • 场景二:两个线程两把锁
java">public class Demo14 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object o = new Object();Object o1 = new Object();Thread thread = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o) {//1.拿到o锁synchronized (o1){//3.与tread1的o1锁互斥count++;}}}});Thread thread1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o1){//2.拿到o1锁synchronized (o){//4.与tread的o锁互斥count++;}}}//3,4相互等待,最终卡死});thread.start();thread1.start();thread.join();thread1.join();System.out.println(count);}
}

运行结果:毛都没有!
在这里插入图片描述
这就说明,这里的的进程卡死,产生了死锁.
什么原理呢?由于tread一首先启动,tread拿到o锁,并上锁,此时tread1启动,拿到o1锁,当tread想要拿到o1锁的时候,发现o1锁被占用,阻塞等待,当tread1想要拿到o锁的时候,发现o锁被占用,阻塞等待,这时候tread1和tread相互循环相互等待,就产生了死锁.
这就像两个相互暗恋的人一样,都彼此暗恋着对方,但是都不敢鼓起勇气去表白,这样就会彼此错过.
在这里插入图片描述

  • 那么我们如何规避死锁呢?(重点面试题)
    首先我们要知道参数死锁的4个必要条件:
  1. 锁具有互斥性
  2. 锁不可剥夺
    上述是锁的两个基本的特性,我们无法干预
  3. 请求锁和保持锁
    一个线程拿到一把锁之后,不释放这个锁,就尝试获取其他锁.
  4. 循环等待:
    多个线程获取多个锁过程中,A等待B,B等待A.
    上述两个条件,我们都可以通过干预代码结构来解除死锁.
    我们需要约定好加锁顺序,让所有的线程按照一定的顺序加锁.
    我们尝试使用上面的方法对上面的场景二的死锁进行解除:调换lock1和lock2的位置,让tread执行完所有的逻辑之后释放锁之后,再轮到tread1执行.
java">public class Demo14 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object o = new Object();Object o1 = new Object();Thread thread = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o) {synchronized (o1){count++;}}}});Thread thread1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o){synchronized (o1){count++;}}}});thread.start();thread1.start();thread.join();thread1.join();System.out.println(count);}
}

http://www.ppmy.cn/ops/14398.html

相关文章

缓存神器-JetCache

序言 今天和大家聊聊阿里的一款缓存神器 JetCache。 一、缓存在开发实践中的问题 1.1 缓存方案的可扩展性问题 谈及缓存&#xff0c;其实有许多方案可供选择。例如&#xff1a;Guava Cache、Caffine、Encache、Redis 等。 这些缓存技术都能满足我们的需求&#xff0c;但现…

基于python+django+mysql农业生产可视化系统

博主介绍&#xff1a; 大家好&#xff0c;本人精通Java、Python、C#、C、C编程语言&#xff0c;同时也熟练掌握微信小程序、Php和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验&#xff0c;能够为学生提供各类…

如何调节电脑屏幕亮度?让你的眼睛更舒适!

电脑屏幕亮度的调节对于我们的视力保护和使用舒适度至关重要。不同的环境和使用习惯可能需要不同的亮度设置。可是如何调节电脑屏幕亮度呢&#xff1f;本文将介绍三种不同的电脑屏幕亮度调节方法&#xff0c;帮助您轻松调节电脑屏幕亮度&#xff0c;以满足您的需求。 方法1&…

验证二叉搜索树 - LeetCode 热题 43

大家好&#xff01;我是曾续缘&#x1f618; 今天是《LeetCode 热题 100》系列 发车第 43 天 二叉树第 8 题 ❤️点赞 &#x1f44d; 收藏 ⭐再看&#xff0c;养成习惯 验证二叉搜索树 给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜…

Python闭包:深入理解与应用场景解析

Python闭包&#xff1a;深入理解与应用场景解析 在Python中&#xff0c;闭包是一个强大的概念&#xff0c;它允许函数记住并访问所在作用域之外的变量。闭包通常由嵌套函数构成&#xff0c;内层函数引用了外层函数的变量&#xff0c;并且外层函数返回内层函数。本文将深入探讨…

社交媒体数据恢复:KaokaoTalk

KaokaoTalk数据恢复方法 一、数据恢复的基本步骤 在进行KaokaoTalk数据恢复时&#xff0c;首先需要确保你已经停止使用该设备&#xff0c;以防止新的数据覆盖丢失的数据。接下来&#xff0c;你可以按照以下步骤进行操作&#xff1a; 备份数据&#xff1a;在尝试恢复数据之前&a…

【Linux-进程状态】

文章目录 1.进程状态1.运行状态2.阻塞状态3.挂起 2.Linux系统中的进程状态1.前台进程和后台进程深度睡眠 2.停止状态 1.进程状态 想要理解进程状态&#xff0c;我们要先看看课本中的进程有哪些状态。 进程状态用大白话说&#xff0c;就是pcb中的一个字段&#xff0c;就是pcb中…

PHP之内置web服务器

1. 前言 PHP从5.4开始&#xff0c;就提供了一个内置的web服务器。 这个主要是用来做本地的开发测试用的&#xff0c;不能用于线上环境。 将PHP的安装路径配置到电脑的系统环境变量Path中&#xff0c;下图是win7&#xff0c;win10中会看的更清楚 2. 进入项目目录&#xff0c;执…