线程(二)——线程安全

news2025/5/12 9:55:55

如何理解线程安全:

        多线程并发执行的时候,有时候会触发一些“bug”,虽然代码能够执行,线程也在工作,但是过程和结果都不符合我们的开发时的预期,所以我们将此类线程称之为“线程安全问题”。

        例如:在多个线程并发的时候,操作系统对多线程的调度特性会导致结果存在偶然性,这个偶然性可能很小,但也不小(eg:假设偶然性为0.1‰,并发的线程有200_000个,那出现的偶然性结果也会有200个,也就是每20w个用户就会影响到200个用户体验,如果是更大体量的那就影响更大了)

代码实例:如图,我们的实例预期结果本来应该是5000+5000 =10000

多次运行结果都不一样,为什么不一样呢?         问题的关键就在于——并发执行会有偶然性,如果是串行执行那么就不会有问题~

  进一步来体会并发执行的过程:

 从内核的时间轴来看线程代码的执行

 

        通过上述的这些问题,我们再细致的说说线程不安全的原因:

1、内核对线程调度的随机性(非人力能干涉的不可控因素~)

2、当前代码有多个线程对变量进行操作:(变量也可以是硬盘上的数据/网络上的数据)

        ①多个线程修改同一个变量——>不安全,代码在执行时如果被其他的线程抢占执行,那么结果很有可能就是错的~

        ②多个线程读取同一个变量——>没事儿~只读不改,就相当于每个线程在内存中拷贝一份这个变量过来~

        ③多个线程修改不同的变量——>没事儿~你改你的,我改我的,井水不犯河水~

        ④单个线程修改同一个变量——>没事儿~每改一次就从内存拿出来一次,改完就放回内存去~

 3、线程针对变量的修改不是原子性的(如果线程不是抢占式执行,那么没有原子性也没有关系~)

什么是原子性?

        所谓“原子性”,就是不可拆分的最小单位,也就是说,当对一个变量的修改是执行一个最小量级的命令——CPU指令,则称这个操作是具有原子性的。

        拿上述count++举例,count++这个语句在CPU内核中是分为三步实现:在内存中将count拿出来,在CPU寄存器上进行count+1,将计算结果放回内存。这一句简单的语句需要三个指令来实现,那么对count这个变量的修改就不具备原子性~

可见性:

        多个线程在修改同一个变量的时候,能够让其他的线程同时看见这个变量。

JMM里的模拟内存:

        每一个线程都有各自的一块工作内存,在线程创造的时候申请分配工作内存,线程销毁的时候释放工作内存。对一个变量进行修改不会直接在主内存内对变量进行修改,而是在主内存中拷贝一份到线程的工作内存中进行修改,然后进行数据更新后再写入主内存~

内存的可见性:

众所周知,每个线程都会申请一块属于自己的工作内存,对于数据的修改,线程总是先从主内存拷贝一份到工作内存,然后在寄存器上进行操作之后再将数据放回内存。

当有多个线程对同一个变量操作时,如何让两个线程的操作都是有效的?这时候就涉及到内存的可见性~

例:现有两个线程,t1线程只有在线程t2通知之后才会停下,而我们利用控制台输入一个非0的整数来控制t2通知t1

 static class Counter {
        public int count = 0;
    }
    public static void main(String[] args) throws InterruptedException {

        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            while(counter.count == 0)
            {

            }
            System.out.println(Thread.currentThread().getName()+"接到通知,马上停下来...");
        },"t1");
        Thread t2 = new Thread(()->{
            Scanner sc = new Scanner(System.in);
            System.out.println(Thread.currentThread().getName()+"发出停止通知...");
            counter.count = sc.nextInt();

        },"t2");
     t1.start();
     t2.start();


    }

可是结果却是t1没有收到t2的通知

为什么会这样?这是因为t2将修改之后的变量刷新到内存,但是这个结果没有在t1中同步刷新,所以就产生了上述的结果

如何解决这一点?用volatile修饰count即可~

拿上面的例子思考一下,如何避免获得上述这种抢占式执行的结果?

①要想避免这种抢占式执行产生的结果,最好的做法就是给线程上锁

②当两个线程执行过程要对同一个变量进行修改的时候,修改的那段代码可以加上同一个锁,这样就会阻塞其中一个线程让其等待,等锁内的线程执行完工作内容再接着执行另一个线程

synchronized关键字:

        对于多个线程针对同一个对象,我们如果想要保证程序的原子性,那么就得给这个对象加一个锁,而synchronized就是干这件事的~

       进入synchronized(){}的作用域中表示加锁,执行完作用域中的代码就进行解锁。

        如果synchronized针对的是多个对象,那么就不会产生锁竞争,也就不会出现阻塞等待,线程各自干各自的活儿。

public class demo2 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"加锁前....");
            synchronized (locker)
            {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName()+"解锁后....");
        },"线程1");
        Thread t2 = new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"加锁前....");
            synchronized (locker2)
            {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName()+"解锁后....");
        },"线程2");

        t1.start();
        t2.start();

        t1.join();
        t1.join();
    }
}

结果就是两个线程是并发执行的,各干各的,没有产生阻塞等待~

synchronized具有不可抢占性——即如果有人已经持有这把锁,那么在这把锁释放之前其他的线程是拿不到的。

用实例体会一下抢锁的过程: 


public class demo3 {

        public static void main(String[] args) throws InterruptedException {
            Object locker = new Object();
            Thread t1 = new Thread(()->{
                synchronized (locker)
                {
                    System.out.println(Thread.currentThread().getName()+"抢到锁进行加锁....");
                }
            },"线程1");
            Thread t2 = new Thread(()->{
                synchronized (locker)
                {
                    System.out.println(Thread.currentThread().getName()+"抢到锁进行加锁....");
                }
            },"线程2");
          Thread t3 = new Thread(()->{
                synchronized (locker)
                {
                    System.out.println(Thread.currentThread().getName()+"抢到锁进行加锁....");
                }
            },"线程3");
            t1.start();
            t2.start();
            t3.start();
        }
    }

通过结果,我们可以明白——针对同一个对象进行加锁,谁能先拿到锁是随机的。但是,为什么这里的线程1能一直先拿到锁?这是因为后面t2、t3线程启动需要时间,在这短短的启动时间里,t1可以先获得锁~

在其他的语言中,加锁操作并不是一个synchronized就能搞定,而是线程{     lock();   其他代码.....;   unlock();   }~有时候往往会把unlock()给忘了,这时候就出错了,而synchronized在封装过程中这些都帮我们写好了,我们直接用没有后顾之忧~

wait()和notify():

调用wait()方法干的事:

①让当前线程进行阻塞

②释放当前的锁

③满足一定条件被唤醒,重新尝试获取这个锁(不一定唤醒了就能获取的到,依旧是和其他线程抢占执行)

class WaitTask implements Runnable{
    private Object locker  = new Object();

    public WaitTask(Object locker) {
        this.locker = locker;
    }

    @Override
    public void run() {
        synchronized (locker)
        {
            try {
                System.out.println("开始阻塞...");
                locker.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
    }
}
class NotifyTask implements Runnable{
    private Object locker = new Object();
    public NotifyTask(Object locker)
    {
       this.locker = locker;
    }
    @Override
    public void run() {
        synchronized (locker)
        {
               locker.notify();
            System.out.println("线程已经被唤醒...");
        }
    }
}
public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        Object locker  =  new Object();
        Thread t1 = new Thread(new WaitTask(locker));
/*        Thread t2 = new Thread(new WaitTask(locker));
        Thread t3 = new Thread(new WaitTask(locker));*/
        Thread t4 = new Thread(new NotifyTask(locker));

        t1.start();
//        t2.start();
//        t3.start();
        Thread.sleep(5000);
        t4.start();
    }
}

唤醒线程的方法:①调用该对象的notify()方法;②wait()等待超时;③其他线程调用Interrupted方法,抛出InterruptedExption异常。 

        notify()方法是唤醒等待中的线程,当有多个线程处于wait()时,由线程调度器随机挑选一个进行唤醒(依旧是没有先到先得的原则)
wait()要搭配synchronized一起使用,不然就会抛异常
当线程调用wait()之后需要调用notify()来唤醒线程,不然就是让程序死等

notify()一次只能唤醒一个线程,而notifyAll()能够一次性唤醒所有线程
一次性唤醒所有等待的线程之后依旧是抢占式执行,依旧有先后执行顺序

区分wait()和sleep():
wait()是用于线程之间的通信,而sleep()只是单纯地让线程阻塞一段时间
1.wait()需要和synchronized搭配使用,而sleep不需要
2.wait()是Object类的方法,而sleep()是Thread类的静态方法


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

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

相关文章

prometheusgrafana实现监控告警

Prometheus负责集群数据的监控和采集,然后传递给grafana进行可视化,集成睿象云可实现监控报警,为了方便操作,可以通过iframe嵌套grafana到指定的页面。 文章目录 1.Grafana集成Prometheus2.iframe内嵌grafana3.监控告警 1.Grafana…

双绞线直连两台电脑的方法及遇到的问题

文章目录 前言一、步骤二、问题总结:问题1:遇到ping不通的问题。问题2:访问其他电脑上的共享文件时提示输入网络凭证问题3:局域网共享文件时提示“没有权限访问,请与网络管理员联系请求访问权限” 前言 办公室里有两台电脑,一台装了显卡用于…

如何使用Java编写Jmeter函数

Jmeter 自带有各种功能丰富的函数,可以帮助我们进行测试,但有时候提供的这些函数并不能满足我们的要求,这时候就需要我们自己来编写一个自定义的函数了。例如我们在测试时,有时候需要填入当前的时间,虽然我们可以使用p…

练8:递归

欢迎大家订阅【蓝桥杯Python每日一练】 专栏,开启你的 Python数据结构与算法 学习之旅! 1 递归 在 Python 中,递归(Recursion) 是一种函数调用自身的编程技术。 递归通常用来解决可以分解为类似子问题的问题&#xff…

Python金融大数据分析概述

💂 个人网站:【 摸鱼游戏】【神级代码资源网站】【海拥导航】💅 想寻找共同学习交流,摸鱼划水的小伙伴,请点击【全栈技术交流群】 金融大数据分析在金融科技领域越来越重要,它涉及从海量数据中提取洞察,为金…

WSL2下如何部署CosyVoice并开启API服务

环境: WSL2 英伟达4070ti 12G Win10 Ubuntu22.04 问题描述: WSL下如何部署CosyVoice并开启API服务 解决方案: CosyVoice 下载不顺的时候,最好提前开科学 一、部署 1.拉取源码 git clone –recursive https://github.com/FunAudioLLM/CosyVoice.gitwsl下拉取 gi…

室联人形机器人:家政服务任务结构化、技术要点、深入应用FPGA的控制系统框架设计(整合版)

目录: 0 引言 1 人形机器人对室内家政服务任务的结构化 1.1人形机器人在室内家政服务中的比较优势 1.1.1 人形机器人拟人性的7个维度 1.1.2 拟人性在室内家政服务工作中的比较优势 1.1.3 潜在的重要用户:宠物爱好者 1.2 居所室内环境的特征与结构…

基于 Apache Dolphinscheduler3.1.9中的Task 处理流程解析

实现一个调度任务,可能很简单。但是如何让工作流下的任务跑得更好、更快、更稳定、更具有扩展性,同时可视化,是值得我们去思考得问题。 Apache DolphinScheduler是一个分布式和可扩展的开源工作流协调平台,具有强大的DAG可视化界…

物联网接入网关的数据安全和高效传输详解

物联网接入网关,作为连接物联网终端设备与云端或本地服务器的关键环节,不仅负责数据的汇聚与转发,更需确保数据在传输过程中的安全无虞与高效流畅。 一、数据安全:构筑坚实防线 1. 加密技术的应用 天拓四方物联网接入网关内置了…

双指针算法(超详细版)

希望大家多多关注,有三必回 1.双指针 1.1快慢双指针 快慢双指针常用来解决循环问题,或是查找中间节点 1.1.1循环链表(141. 环形链表 - 力扣(LeetCode)) 解题思路: 1.定义快慢指针fast和slo…

Rain后台权限管理系统,快速开发

这段时间一直没有更新,因为在写一个脚手架,今天Rain项目终于完工,已经发布到github,免费使用 项目github链接 https://github.com/Rain-hechang/Rain 简介 前端采用Vue3.x、Element UI。 后端采用Spring Boot、Spring Security、Redis &…

欧歌Web电视 1.2|全新修改版,新增更多频道,更稳定

欧歌Web电视App是一款功能强大的电视直播软件,通过WebView二次开发,对内置线路进行了优化和增加,让用户可以看到更多的频道。首次打开如果不会自动全屏,可以进入设置调整画面尺寸。该版本新增了多个地方频道和娱乐内容频道&#x…

嵌入式系统与移动设备开发

文章目录 1 嵌入式系统概述1.1 嵌入式系统基本概念1.1.1 嵌入式系统定义1.1.2 嵌入式系统的发展1.1.3 嵌入式系统的特点 1.2 嵌入式系统分类1.2.1 单个微处理器1.2.2 嵌入式处理器可扩展的系统1.2.3 复杂的嵌入式系统1.2.4 在制造或过程控制中使用的计算机系统 1.3 嵌入式处理器…

使用 Elastic 和 Amazon Bedrock 制作混合地理空间 RAG 应用程序

作者:来自 Elastic Udayasimha Theepireddy (Uday), Srinivas Pendyala, Ayan Ray 借助 Elasticsearch 及其向量数据库,你可以构建可配置的搜索和可信的生成式 AI (GenAI) 体验,这些体验可快速从原型扩展到生产。主要功能包括: 内…

【opencv入门教程】15. 访问像素的十四种方式

文章选自: 一、像素访问 一张图片由许多个点组成,每个点就是一个像素,每个像素包含不同的值,对图像像素操作是图像处理过程中常使用的 二、访问像素 void Samples::AccessPixels1(Mat &image, int div 64) {int nl imag…

Ansys Maxwell使用技巧

1、回到原点 点击Fit All 2、长方体做差 选中两个长方体, 点击Subtracct,就可以得到一个镂空的绕组。 3、电感仿真步骤 3.1 画磁芯 3.2 画绕组 3.3 加激励 选择截面积-右键绕组-Edit-Surface-Section-YZ 选择一个截面添加电流激励 3.4选材料 绕组一…

掌握谈判技巧,达成双赢协议

在当今竞争激烈且合作频繁的社会环境中,谈判成为了我们解决分歧、谋求共同发展的重要手段。无论是商业合作、职场交流,还是国际事务协商,掌握谈判技巧以达成双赢协议都具有极其关键的意义。它不仅能够让各方在利益分配上找到平衡点&#xff0…

MacOS 命令行详解使用教程

本章讲述MacOs命令行详解的使用教程,感谢大家观看。 本人博客:如烟花般绚烂却又稍纵即逝的主页 MacOs命令行前言: 在 macOS 上,Terminal(终端) 是一个功能强大的工具,它允许用户通过命令行直接与系统交互。本教程将详细介绍 macOS…

第十七章 使用 MariaDB 数据库管理系统

1. 数据库管理系统 数据库是指按照某些特定结构来存储数据资料的数据仓库。在当今这个大数据技术迅速崛起的年代,互联网上每天都会生成海量的数据信息,数据库技术也从最初只能存储简单的表格数据的单一集中存储模式,发展到了现如今存储海量…

11.17【大数据】Hadoop【DEBUG】

列出hdfs文件系统所有的目录和文件 主节点上 子结点 是一样的 *为什么能登进 slave 02 的主机,但是 master 当中依然显示 slave 02 为 DeadNode?* hadoop坏死节点的重启_hadoop3 子节点重启-CSDN博客 注意hadoop-daemon.sh 实际上位于 Hadoop 的 sbin 目录中,而不…