线程(二)——线程安全

如何理解线程安全:

        多线程并发执行的时候,有时候会触发一些“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类的静态方法


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

思特奇政·企数智化产品服务平台正式发布,助力运营商政企数智能力跃迁

数字浪潮下,产业数字化进程加速发展,信息服务迎来更广阔的天地,同时也为运营商政企支撑系统提出了更高要求。12月4日,2024数字科技生态大会期间,思特奇正式发布政企数智化产品服务平台,融合应用大数据、AI等新质生产要素,构建集平台服务、精准营销、全周期运营支撑、智慧大脑于…

解决Windows与Ubuntu云服务器无法通过Socket(udp)通信问题

今天在写Socket通信代码的时候,使用云服务器自己与自己通信没有问题,但是当我们把客户端换为Windows系统的时候却无法发送信息到Linux当中,耗时一上午终于搞定了😒。 问题: 如上图,当我在windows的客户端…

面向金融场景的大模型 RAG 检索增强解决方案

概述 在现代信息检索领域,检索增强生成(Retrieval-Augmented Generation, RAG)模型结合了信息检索与生成式人工智能的优点,从而在特定场景下提供更为精准和相关的答案。在特定场景下,例如金融等领域,用户通…

【pyspark学习从入门到精通24】机器学习库_7

目录 聚类 在出生数据集中寻找簇 主题挖掘 回归 聚类 聚类是机器学习中另一个重要的部分:在现实世界中,我们并不总是有目标特征的奢侈条件,因此我们需要回归到无监督学习的范式,在那里我们尝试在数据中发现模式。 在出生数据…

渗透测试---burpsuite(5)web网页端抓包与APP渗透测试

声明:学习素材来自b站up【泷羽Sec】,侵删,若阅读过程中有相关方面的不足,还请指正,本文只做相关技术分享,切莫从事违法等相关行为,本人与泷羽sec团队一律不承担一切后果 视频地址:泷羽---bp&…

[LitCTF 2023]破损的图片(初级)

[LitCTF 2023]破损的图片(初级) 我们下载附件得到一个没有后缀的文件,拖去010看一看,发现本来应该是文件头的那部分不大对劲,结合后面四个点以及IHDR,大致也应该知道是啥了 修改第一行为png 89 50 4E 47 0D 0A 1A 0A 00 00 00 …

docker部署RustDesk自建服务器

客户端: Releases rustdesk/rustdesk GitHub 服务端: 项目官方地址:GitHub - rustdesk/rustdesk-server: RustDesk Server Program 1、拉取RustDesk库 docker pull rustdesk/rustdesk-server:latest 阿里云库: docker pu…

智慧银行反欺诈大数据管控平台方案(八)

智慧银行反欺诈大数据管控平台的核心理念,在于通过整合先进的大数据技术、算法模型和人工智能技术,构建一个全面、智能、动态的反欺诈管理框架,以实现对金融交易的全方位监控、欺诈行为的精准识别和高效处理。这一理念强调数据驱动决策&#…

关闭windows11的“热门搜索”

win10搜索栏热门搜索怎么关闭?win10搜索栏热门搜索关闭方法分享_搜索_onecdll-GitCode 开源社区 注册表地址是:计算机\HKEY_CURRENT_USER\SOFTWARE\Policies\Microsoft\Windows\ 最后效果如下:

14.在 Vue 3 中使用 OpenLayers 自定义地图版权信息

在 WebGIS 开发中,默认的地图服务通常会带有版权信息,但有时候我们需要根据项目需求自定义版权信息或添加额外的版权声明。在本文中,我们将基于 Vue 3 的 Composition API 和 OpenLayers,完成自定义地图版权信息的实现。 最终效果…

Dubbo应用篇

文章目录 一、Dubbo简介二、SSM项目整合Dubbo1.生产者方配置2.消费者方配置 三、Spring Boot 项目整合Dubbo1.生产者方配置2.消费者方配置 四、应用案例五、Dubbo配置的优先级别1. 方法级配置(Highest Priority)2. 接口级配置3. 消费者/提供者级配置4. 全…

数据结构与算法 五大算法

文章目录 1,时间复杂度与空间复杂度 2,插入排序 3,希尔排序 4,选择排序 1,单趟排序 2,选择排序PLUS版本 5,冒泡排序 6,快速排序 1,hoare版本 2,挖坑法 前言 …

子类有多个父类的情况下Super不支持指定父类来调用方法

1、Super使用方法 super()函数在Python中用于调用父类的方法。它返回一个代理对象,可以通过该对象调用父类的方法。 要使用super()方法,需要在子类的方法中调用super(),并指定子类本身以及方法的名称。这样就可以在子类中调用父类的方法。 …

Java项目实战II基于微信小程序的消防隐患在线举报系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、核心代码 五、源码获取 全栈码农以及毕业设计实战开发,CSDN平台Java领域新星创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 随着城市化进程的加快&…

python学opencv|读取视频(一)灰度视频制作和保存

【1】引言 上一次课学习了用opencv读取图像,掌握了三个函数:cv.imread()、cv.imshow()、cv.imwrite() 相关链接如下: python学opencv|读取图像-CSDN博客 这次课我们继续,来学习用opencv读取视频。 【2】学习资源 首先是官网…

buuctf:被嗅探的流量

解压后用wireshark查看 flag{da73d88936010da1eeeb36e945ec4b97}

数据清洗代码:缺失值,异常值,离群值Matlab处理

目录 基本介绍程序设计参考资料基本介绍 一、过程概述 本过程适用于处理SCADA系统采集到的数据,以及具有类似需求的数据集。处理步骤包括缺失值处理、异常值处理和离群值处理,旨在提升数据质量,增强数据的相关性,同时保持数据的原始特征和随机性。 二、缺失值处理 对于SC…

深入浅出:Go语言中的错误处理

深入浅出:Go语言中的错误处理 引言 在任何编程语言中,错误处理都是一个至关重要的方面。它不仅影响程序的稳定性和可靠性,还决定了用户体验的质量。Go语言以其简洁明了的语法和强大的并发模型而著称,但其错误处理机制同样值得关…

青海摇摇了3天,技术退步明显.......

最近快手上的青海摇招聘活动非常火热,我已经在思考是否备战张诗尧的秋招活动。开个玩笑正片开始: 先说一下自己的情况,大专生,20年通过校招进入杭州某软件公司,干了接近4年的功能测试,今年年初&#xff0c…

【计算机网络】 —— 数据链路层(壹)

文章目录 前言 一、概述 1. 基本概念 2. 数据链路层的三个主要问题 二、封装成帧 1. 概念 2. 帧头、帧尾的作用 3. 透明传输 4. 提高效率 三、差错检测 1. 概念 2. 奇偶校验 3. 循环冗余校验CRC 1. 步骤 2. 生成多项式 3. 例题 4. 总结 四、可靠传输 1. 基本…