什么是线程安全?如何保证线程安全?

news2024/12/24 3:06:13

目录

一、引入线程安全 👇

二、 线程安全👇

1、线程安全概念 🔍

2、线程不安全的原因 🔍

抢占式执行(罪魁祸首,万恶之源)导致了线程之间的调度是“随机的”

多个线程修改同一个变量 

 修改操作,不是原子的(不可分割的最小单位) 

内存可见性,引起的线程不安全 

指令重排序,引起的线程不安全

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

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

1)synchronized 的特性 

(1) 互斥

(2)刷新内存

(3) 可重入

2)synchronized 使用示例 

1) 直接修饰普通方法:

 2) 修饰静态方法:

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

2、volatile 关键字 (保证内存可见性) 🔍

 1)引入volatile

2)volatile不保证原子性

💡 总结:


一、引入线程安全 👇

执行以下代码:

package threading;
class Counter {
    public int count = 0;
    void increase() {
        count++;
    }
}

public class ThreadDemo23 {
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

可以观察代码,我们对两个线程分别累加50000次,结果应该为100000,但是大家看运行结果并非如此,而且可以发现,每次运行的结果都不一样,这是什么原因呢? 

答案就是涉及到了线程安全问题

 

 

本质原因:线程在系统中的调度是无序的/随机的(抢占式执行的) 


二、 线程安全👇

1、线程安全概念 🔍

    如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线 程安全的。 

2、线程不安全的原因 🔍

  • 抢占式执行(罪魁祸首,万恶之源)导致了线程之间的调度是“随机的”

  • 多个线程修改同一个变量 

一个线程修改同一个变量=>安全

多个线程读取同一个变量=>安全

多个线程修改不同变量=> 安全 

  •  修改操作,不是原子的(不可分割的最小单位) 

什么是原子性🐶

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

那我们应该如何解决这个问题呢?🐶

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

一条 java 语句不一定是原子的,也不一定只是一条指令 比如刚才我们看到的 n++,其实是由三步操作组成的: 1. 从内存把数据读到 CPU 2. 进行数据更新 3. 把数据写回到 CPU

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

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

  • 内存可见性,引起的线程不安全 

          可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.

  • 指令重排序,引起的线程不安全


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

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

1)对文章初始代码进行修改:既可以保证 ++ 操作就是原子的,不受影响啦

可以将{ }视为厕所,进表示加锁,出表示解锁

void increase() {
        //锁有俩个核心操作,加锁和解锁
        //进入该代码块就会触发加锁,出了代码块,就会触发解锁
        synchronized (this) {
            count++;
        }
    }

 注意:此处的this指的就是counter对象

因此,在上述代码中,两个线程实在竞争同一个锁对象,就会产生锁竞争。

再执行上述代码:就是100000

疑惑为啥以上操作为什么可以解决线程安全问题呢? 🐶

1)synchronized 的特性 

(1) 互斥

      synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待.

进入 synchronized 修饰的代码块, 相当于 加锁

退出 synchronized 修饰的代码块, 相当于 解锁 

理解 "阻塞等待". 针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝 试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的 线程, 再来获取到这个锁.

注意: 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这 也就是操作系统线程调度的一部分工作.

假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能 获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则. 

(2)刷新内存

synchronized 的工作过程: 

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁 所以 synchronized 也能保证内存可见性. 
(3) 可重入

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

理解 "把自己锁死" :一个线程没有释放锁, 然后又尝试再次加锁. 

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第 二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无 法进行解锁操作. 这时候就会死锁

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

 示例:

static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}

increase 和 increase2 两个方法都加了 synchronized,

此处的 synchronized 都是针对 this 当前对象加锁的.

在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释 放, 相当于连续加两次锁)

这个代码是完全没问题的. 因为 synchronized 是可重入锁.

注意:在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.

如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取 到锁, 并让计数器自增.

解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到) 

2)synchronized 使用示例 

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

1) 直接修饰普通方法:

锁的 SynchronizedDemo 对象🐶

public class SynchronizedDemo {
    public synchronized void methond() {
   }
}
 2) 修饰静态方法:

锁的 SynchronizedDemo 类的对象🐶

public class SynchronizedDemo {
    public synchronized static void method() {
   }
}
3) 修饰代码块: 明确指定锁哪个对象.

锁当前对象🐶

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

锁类对象🐶

类对象是啥:Counter.class

.java源代码文件,javac =>.class(二进制字节码文件),JVM就可以执行.class文件了,类对象就可以表示这个.class文件的内容~~(描述了类的方方面面的详细信息,比如诶的名字,类的属性,类的方法,)

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

2、volatile 关键字 (保证内存可见性)🔍

 1)引入volatile

所谓内存可见性,就是多线程环境下,编译器对于代码优化,产生了误判,从而引起了bug,进一步导致了代码的bug

因此我们可以加上 volatile(让编译器对这个场景暂停优化) , 强制读写内存. 速度是慢了, 但是数据变的更准确了.每次都是从内存中重新读取数据

观察以下代码:

package threading;

import java.util.Scanner;

public class ThreadDemo24 {
    public static int flag = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {

            }
            System.out.println("循环结束,t1结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            //t2通过控制台输入一个整数,yidanyonghushurule非0的值,此时t1的循环就会立即结束,从而t1线程就会退出
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

 我们预期的效果应该是输入一个非零的数,线程t1就会停止,但实际上仍然在执行,处在RUNNABLE状态

 为什么会出现以上问题呢?内存可见性的锅!!!

让我们分析一下代码:

直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度 非常快, 但是可能出现数据不一致的情况.

 

volatile public static int flag = 0;

加上volatile关键字,此时编译器就可以保证每次都是重新从内存读取flag变量的值,

此时t2修改flag,t1就可以立即感知到了,t1就可以正确退了

2)volatile不保证原子性

这个是最初的演示线程安全的代码.

给 increase 方法去掉 synchronized

给 count 加上 volatile 关键字.

static class Counter {
    volatile public int count = 0;
    void increase() {
        count++;
   }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);
}

此时可以看到, 最终 count 的值仍然无法保证是 100000 

💡 总结:
  • volatile不保证原子性
  • volatile也能禁止指令重排序
  • volatile 适用一个线程读,一个线程写的情况
  • synchronized则是多个线程写 
  • volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性(也能保证内存可见性), volatile 保证的是内存可见 性
static class Counter {
    public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (true) {
            synchronized (counter) {
  if (counter.flag != 0) {
                    break;
               }
           }
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}

上面代码:

去掉 flag 的 volatile

给 t1 的循环内部加上 synchronized, 并借助 counter 对象加锁. 

运行结果是可以正常结束的

因此 synchronized是可以保证内存可见性的

 

 补充: 指令重排序,也是编译器优化的策略,调整了代码的执行顺序,让程序更高效,前提也是保证整体逻辑不变

 💡 总结:(面试题)

线程不安全的原因:

【根本原因】==操作系统上的线程是“抢占式执行”的,线程调度是随机的,==这是线程不安全的一个主要原因。随机调度会导致在多线程环境下,程序的执行顺序不确定,程序员必须确保无论哪种执行顺序,代码都能正常运行。
【代码结构】共享资源:多个线程同时访问并修改共享的数据或资源。当多个线程同时访问和修改共享资源时容易引发竞态条件和数据不一致的问题。
①一个线程修改一个变量是安全的
②多个线程修改一个变量是不安全的
③多个线程修改不同变量是安全的
【直接原因】多线程操作不是“原子的”。多线程操作中的原子性指的是一个操作是不可中断的,要么全部执行完成,要么都不执行,不能被其他线程干扰。这对于并发编程非常重要,因为如果一个操作在执行过程中被中断,可能导致数据不一致或者其他意外情况发生。(在上述多线程操作中,count++操作不是“原子的”,而是由多个CPU指令组成的,一个线程执行这些指令时,可能会在执行过程中被抢占,从而给其他线程“可乘之机”。要保证原子性操作,每个CPU指令都应该是“原子的”,即要么完全执行,要么完全不执行。)
内存可见性问题:在多线程环境下调用不可重入的函数(即不支持多线程调用的函数),可能导致数据混乱或程序崩溃。
指令重排序问题:在多线程环境下,由于编译器或处理器对指令进行重排序优化,可能导致预期之外的程序行为。

 

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。


                        

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每日一题《leetcode--116.填充每个结点的下一个右侧结点》

https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/ 题目要求给每个结点的next指针进行填充&#xff0c;使每个结点的next指针指向其下一个右侧结点。如果右侧没有结点&#xff0c;则将next指针设置为空。 struct Node* connect(struct Node* root) {…

OpenP2P使用分享

软件介绍 大家好&#xff0c;今天我跟大家分享另一种内网穿透的方式。说起内网穿透&#xff0c;可能大家首先想到的是VPN&#xff0c;它是一种常见的内网穿透技术&#xff0c;能够让我们在远程访问内网资源。但是&#xff0c;VPN需要公网IP和服务器&#xff0c;而且配置相对复…

智能的PHP开发工具PhpStorm v2024.1全新发布——支持PHPUnit 11.0

PhpStorm是一个轻量级且便捷的PHP IDE&#xff0c;其旨在提高用户效率&#xff0c;可深刻理解用户的编码&#xff0c;提供智能代码补全&#xff0c;快速导航以及即时错误检查。可随时帮助用户对其编码进行调整&#xff0c;运行单元测试或者提供可视化debug功能。 立即获取PhpS…

路由聚合和VRRP技术

实验拓扑图&#xff1a; 实验需求 1、内网IP地址使用172.16.0.0/16 2、SW1和SW2之间互为备份&#xff1b; 3、VRRP/stp/vlan/eth-trunk均使用&#xff1b; 4、所有pc均通过DHCP获取IP地址&#xff1b; 5、ISP只配置IP地址&#xff1b; 6、所有电脑可以正常访问ISP路由器环…

什么是云计算安全?如何保障云计算安全

云计算彻底改变了数据存储的世界&#xff0c;它使企业可以远程存储数据并随时随地从任何位置访问数据。存和取变得简单&#xff0c;也使得云上数据极易造成泄露或者被篡改&#xff0c;所以云计算安全就显得非常重要了。那么什么是云计算安全&#xff1f; 其实&#xff0c;云计…

EN6347QI 开关稳压器 4A 贴片QFN-38 参数资料 应用案例 相关型号

EN6347QI 是一款直流/直流开关转换器。它是一款高效率的 buck (降压) 转换器&#xff0c;内置了电感器&#xff0c;能够提供高达 4A 的输出电流。其工作电压范围为 4.5V 至 12V&#xff0c;输出电压可调&#xff0c;最高可达 15V。EN6347QI 适合于各种电子设备中&#xff0c;用…

K8s deployment 进阶

文章目录 K8s deployment 进阶Deployment 更新策略RecreateRollingUpdatemaxSurge 和 maxUnavailable minReadySecondsprogressDeadlineSeconds Deployment 版本回滚Deployment 实现灰度发布 K8s deployment 进阶 Deployment 更新策略 Recreate 重建 (Recreate&#xff09;&…

Vue进阶之Vue项目实战(三)

Vue项目实战 图表渲染安装echarts图表渲染器(图表组件)图表举例:创建 ChartsRenderer.vue创建 ChartsDataTransformer.ts基于 zrender 开发可视化物料安装 zrender画一个矩形画一个柱状图基于svg开发可视化物料svg小示例使用d3进行图表渲染安装d3基本使用地图绘制本地持久化拓…

【堡垒机小知识】堡垒机资产监控能监控哪些东西呢?

堡垒机&#xff0c;重要的网络安全工具&#xff0c;其资产监控功能在保障系统稳定运行、防范潜在风险方面发挥着至关重要的作用。但不少小伙伴对于监控内容不清楚&#xff0c;这里我们就来一起简单看看&#xff0c;仅供参考~ 堡垒机资产监控能监控哪些东西呢&#xff1f; 【…

Chrome DevTools 使用指南- 控制台篇

1.引言 在当今的前端开发中&#xff0c;Chrome DevTools 已成为开发者们不可或缺的工具。其中控制台是 DevTools 中最常用和最重要的部分之一。 本指南将详细介绍 Chrome DevTools 控制台的各种功能和使用技巧&#xff0c;帮助您更好地利用控制台进行高效调试和开发。无论您是…

微服务Day7学习

文章目录 数据聚合聚合分类 自动补全DSL实现Bucket聚合DSL实现Metrics聚合RestAPI实现聚合多条件聚合对接前端接口拼音分词器自定义分词器自动补全查询实现酒店搜索框自动补全 数据同步数据同步思路分析利用mq实现mysql与elasticsearch数据同步 集群介绍搭建ES集群 数据聚合 聚…

flume使用实例

1、监听端口a1.sources.r1.type netcat 配置文件nc-flume-console.conf # Name the components on this agent a1 表示jvm进程名 a1.sources r1 a1.sinks k1 a1.channels c1 # Describe/configure the source a1.sources.r1.type netcat a1.sources.r1.bind node…

什么是固态继电器?

固态继电器是不需要使用任何机械部件的开关继电器。这通常使它们具有比普通机电继电器寿命更长的优势&#xff0c;然而&#xff0c;尽管固态继电器速度快且耐用&#xff0c;但仍具有某些设计规定。 固态继电器风靡全球&#xff0c;彻底改变了从农业自动化到航空航天等各个行业…

Pytorch梯度下降算法(Gradient Descent)

intro 其实对于我们将要学的梯度最小函数&#xff0c;目的就是先得到loss损失最小的值&#xff0c;然后根据这个最小的值去得到w。 初始点在initial guess这个位置&#xff0c;我们希望找到最小的权重点global cost minimum&#xff0c;我们到底是让这个点左移寻找还是右移寻…

Linux第三十九章

&#x1f436;博主主页&#xff1a;ᰔᩚ. 一怀明月ꦿ ❤️‍&#x1f525;专栏系列&#xff1a;线性代数&#xff0c;C初学者入门训练&#xff0c;题解C&#xff0c;C的使用文章&#xff0c;「初学」C&#xff0c;linux &#x1f525;座右铭&#xff1a;“不要等到什么都没有了…

【全开源】JAVA同城搬家系统源码小程序APP源码

JAVA同城搬家系统源码 特色功能&#xff1a; 强大的数据处理能力&#xff1a;JAVA提供了丰富的数据结构和算法&#xff0c;以及强大的并发处理能力&#xff0c;使得系统能够快速地处理大量的货物信息、司机信息、订单信息等&#xff0c;满足大规模物流的需求。智能路径规划&a…

【Redis】String的介绍与应用详解

大家好&#xff0c;我是白晨&#xff0c;一个不是很能熬夜&#xff0c;但是也想日更的人。如果喜欢这篇文章&#xff0c;点个赞&#x1f44d;&#xff0c;关注一下&#x1f440;白晨吧&#xff01;你的支持就是我最大的动力&#xff01;&#x1f4aa;&#x1f4aa;&#x1f4aa…

设置 sticky 不生效?会不会是你还是没懂 sticky?

官方描述 基本上可以看懂的就会知道。sticky 是相对于存在滚动条的内容的&#xff0c;啥意思&#xff1f; 就是不论你被谁包着&#xff0c;你只会往上找有 overflow 属性的盒子进行定位&#xff0c;包括&#xff1a;overflow:hidden; overflow:scroll; overflow:auto; overflo…

一键批量提取TXT文档前N行,高效处理海量文本数据,省时省力新方案!

大量的文本信息充斥着我们的工作与生活。无论是研究资料、项目文档还是市场报告&#xff0c;TXT文本文档都是我们获取和整理信息的重要来源。然而&#xff0c;面对成百上千个TXT文档&#xff0c;如何快速提取所需的关键信息&#xff0c;提高工作效率&#xff0c;成为了许多人头…

EI稳定检索--人文社科类会议(ICBAR 2024)

【ACM独立出版】第四届大数据、人工智能与风险管理国际学术会议 (ICBAR 2024) 2024 4th International Conference on Big Data, Artificial Intelligence and Risk Management 【高录用•快检索&#xff0c;ACM独立出版-稳定快速EI检索 | 往届均已完成EI, Scopus检索】 【见…