Java编程--synchronized/死锁/可重入锁/内存可见性问题/wait()、notify()

news2025/7/31 20:59:58

       前言 

        逆水行舟,不进则退!!!     


目录

       线程安全

       synchronized原子锁

       可重入锁(递归锁)

       死锁

       内存可见性问题

       wait()、notify()


       线程安全

        线程安全是指在多线程环境下,程序的行为表现仍然符合我们预期,也就是说,在单线程环境下应该的结果,在多线程环境下也能保证。如果多线程环境下代码运行的结果是不符合我们预期的,即出现数据污染等意外情况,则这个程序就是线程不安全的。

        导致线程不安全的主要因素包括抢占式执行、共享变量等。当存在多个线程并行执行且可能会同时访问和修改同一块内存区域时,如果没有进行适当的同步控制,就可能出现线程安全问题。

        线程安全问题的罪恶之源就是多线程之间的抢占式执行。由于线程的前瞻是执行,导致当前执行到任意一个指令的时候,线程都可能被调度走,cpu 让别的线程来执行。

      


       synchronized原子锁

        synchronized 也叫做 同步机制,要解决线程安全问题,我们就需要将那些有抢占式执行安全隐患的代码原子化,也就是将代码的执行过程变的不可拆分。这样就杜绝了抢占式执行的安全隐患。 

public class ThreadDemo2 {
    //静态成员属性
    static int count = 0;
    /*public static synchronized void counter() {
        count++;
    }*/

    // 静态成员方法 不加锁
    public static void counter() {
        count++;
    }
    
    
    public static void main(String[] args) throws InterruptedException {
        
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000; i++) {
                counter();
            }
        });

        Thread t2 = new Thread(() -> {
            for(int i = 0; i <50000; i++) {
                counter();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

        上面代码中, 有一个静态成员属性,还有一个静态方法对这个静态成员属性进行自增操作,然后呢,创建了两个线程,同时调用这个静态方法,每个线程调用50000次,按照这样的逻辑来说,最后count的值应该是100000,但是实际运行结果表示,100000只是在概率上可能达到。多次运行结果后,count值的范围出现在50000 到 100000之间。

        要弄清楚上面代码中的线程安全问题,我们首先要清楚一个知识点:在Java中,自增操作看上去是一步执行完毕的,

        实际上分为3个步骤:

                1,先把内存中的值 读取到 CPU 的寄存器中   load 操作

                2,把 CPU 寄存器里的数值进行 +1 运算    add 操作

                3,把得到的结果写回到内存中                  save 操作

        

         

        在抢占式执行的环境下,多线程之间的执行顺序由无数种可能。synchronized 的出现,可以让一次自增操作变得原子化,将自增的这个操作变得不能分割。给自增操作上了锁之后,当线程1在进行自增操作时,若是线程2 也要进行自增操作,那就只能阻塞等待,等待线程1的自增操作执行完毕,释放锁之后,然后线程2 才能进行 自增操作。

        注意:synchronized 只是将执行步骤锁住,并不是说在线程在执行上锁代码时不能被CPU调度,线程是可以被调度走的,若是没有执行完就被调度走,其他阻塞等待的线程 也就只能继续等待。 直到锁被释放

        如果两个线程针对同一个对象进行加锁,就会出现所竞争/ 锁冲突。一个线程能够获取到锁(先到先得),另一个线程则阻塞等待,等待到上一个线程解锁,它才能获取锁成功。

        如果两个线程针对不同对象加锁,此时不会发生锁竞争/ 锁冲突,这俩线程都能获取到各自的锁,不会有阻塞等待了。

        现在,在上述代码中 用synchronized 修饰 counter() 方法,这样就是对counter() 方法上了锁,这时再执行代码,结果就是我们预期的 100000 次了。

public class ThreadDemo2 {
    static int count = 0;
    /*public static synchronized void counter() {
        count++;
    }*/
    public static synchronized void counter() {
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000; i++) {
                counter();
            }
        });

        Thread t2 = new Thread(() -> {
            for(int i = 0; i <50000; i++) {
                counter();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}


       可重入锁(递归锁)

        一个线程对同一个对象,连续加锁两次,是否会有问题,如果没有问题,就说明该锁是可重入锁;如果有问题,就说明是不可重入锁。

        也就是,一个线程对一段代码加锁了,但是还未释放锁;紧接着获取到了锁。这连着两次加锁如果可以成功,就说明是可重入。

        现在有个问题是: 可重入加锁为什么能成功? 一般来说,一个线程对一个对象进行加锁后,再有线程想要对这个线程进行加锁操作,就会阻塞。然而随着业务的需求,有一种特殊情况还需要考虑,就是第二次加锁的线程和第一次加锁的线程是同一个。这个时候就要考虑开个绿灯行方便。 这个时候会有一个计数器来计算锁的个数,每加锁一次,计数器自增。同样的每解锁一次,计数器自减,直到计数器自减为0后,其他线程才可进入该对象。       也就是说,在对象被上锁的同时,会记录下,当前的锁是被那个线程所持有。

                

        可重入锁的应用场景:

                定时任务:执行定时任务时,如果任务执行时间可能超过下次计划执行时间,可以使用可重入锁来忽略任务的重复触发,确保该任务只有一个线程在执行。


       死锁

        什么是死锁?

                答:死锁指的是两个或两个以上的进程/线程/运算单元 因为互相竞争资源而导致的一种阻塞现象。具体来说,是这些线程互相持有对方所需的资源,导致它们都无法向前推进,若无外力作用,这些进程都将无法推进下去,从而产生永久阻塞的问题。

        死锁产生的四个必要条件如下:

                1. 互斥条件:线程1 拿到了锁,线程2 就得等着。

                2. 请求与保持条件:线程1拿到了锁A,尝试获取锁B,没得逞,然后锁A也不释放。

                3. 不可剥夺条件:线程1拿到了锁之后,只能等线程1主动释放锁,别的线程抢不走。

                4. 循环等待条件:线程1 拿到了锁A ,申请获取锁B,线程2拿到了锁B,申请获取锁A,线程1在等线程2 释放锁B,线程2 在等 线程1 释放锁A。一直等等等。

                这四个条件是死锁发生的必要条件,只要系统发生死锁,这些条件必然成立。

        虽然说死锁产生的原因有四个,而且是缺一不可的,但是呢,我们若想做到预防死锁,其实就只能破坏第四个条件,因为前三个条件都是锁的基本特性,(至少是针对synchronized 这把锁来说,前三点,想动也动不了。)循环等待是这4个条件里,唯一一个和代码结构相关的,也是程序员可以控制的。


       内存可见性问题

        内存可见性问题是指在多线程环境下,当 A线程 正在使用 对象状态 而 B线程 同时修改该对象状态,而B线程修改的 对象状态 对 A线程 不可见。

        要理解这个问题,我们首先要知道CPU缓存的相关知识。今天的CPU主要采用三层缓存:L1、L2是本地核心内缓存,即一个核一个。如果机器是4核,那就有4个L1和4个L2。L3缓存是所有核共享的,无论你的CPU是几核,这个CPU中只有一个L3。

        由于每个线程执行的时候操作的都是同一个CPU的缓存,这就可能导致某个线程修改了对象的状态,但是在其它线程的缓存中,这个对象的值还没有被更新,这就是内存可见性问题。

         内存可见性问题主要是针对多核CPU架构设计的。对于单核CPU,由于同一时间只有一个线程在执行,不存在多线程竞争导致的数据不一致问题,因此单核CPU不会出现内存可见性问题。

        内存可见性问题的出现,部分源自编译器/JVM在多线程环境下的优化。编译器优化的本质是对代码进行智能分析判断,以提高程序运行效率。然而,这种优化有时可能会产生误判,导致多线程环境下的数据不一致问题,也就是内存可见性问题。

        举个例子:

        在这种情况下,我们需要手动干预,防止编译器做出错误的优化。一个常见的解决方法就是在变量前加上volatile关键字,这可以确保修饰的变量在各个线程中的可见性。

         volatile关键字。(volatile意为 :易变的、易失的)  意思就是告诉编译器,这个变量是会改变的,你一定要每次都重新读取这个变量的内存内容。指不定什么时候就改变了,不能随便优化

        此外,内存屏障指令也可以用来强制刷新工作内存中的值,使得所有线程都能看到最新的值。

        值得注意的是,禁用缓存和编译优化虽然可以解决可见性和有序性的问题,但这样会降低程序的运行效率。因此,在实际编程中,我们需要在保证程序运行效率和数据一致性之间找到一个平衡点。

       


       wait()、notify()

        wait() 和 notify() 可以更好的控制多线程之间的执行顺序。 

        多线程最大的问题是,抢占式执行,随机调度。而随机调度 对程序员来说,非常的不友好。所以就想了一些办法,来控制线程之间的执行顺序。虽然线程在内核里的调度是随机的,但是可以通过一些 API 让线程主动阻塞。中东放弃CPU(给别的线程让路)

        举个例子:t1、t2 俩线程,希望 t1 先干活,干的差不多了,再让 t2 来干,就可以让 t2 先wait(阻塞),等着 t1 干的差不多了,通过 notify 通知 t2, 把 t2 唤醒,让 t2 接着干。

        有wait notify , 那为什么还要有 join 呢?

                答:从功能上说,wait 和 notify 比 join 功能更强,覆盖了 join 的功能。 但是呢,前者使用起来要比 join 麻烦不少。

        这里这个异常:

        这里的这个异常要注意一下,多线程中,很多带有阻塞功能的方法都带这个异常,这些方法都是可以被 interrupt 方法通过这个异常给唤醒

        若wait不加任何参数,那就是一个“死等”,一直等待,直到有其他线程唤醒。

wait执行的操作具体如下:

        1,先释放锁;

        2,进行阻塞等待;

        3,收到通知后,重新尝试获取锁,并且在获取锁之后,继续往下执行。

        如果在执行wait的时候,线程并没有锁,那么会报一个 非法的锁状态异常  ;

        例如:上图代码执行结果 如下:

下图代码中就很好的执行了wait命令:

         这里的wait是阻塞了,阻塞在synchronized代码块里,实际上是释放了锁,此时其他的线程是可以获取到Object这个对象的锁的。

notify:notify这个方法必须在获取到锁之后才能生效。 也就是说在java中notify 是必须和 synchronized 来进行搭配使用,

        此处的 notify 通知得和 wait 配对,

        如果wait 使用的对象和notify 使用的对象不同,则此时notify 不会有效果。(notify 只能唤醒在同一个对象上等待的线程。)

        还有一个问题:

这里如果直接这么写,由于线程调度的不确定性,有可能是t2线程的notify 先执行,这样的话 也没有起到任何作用。

        要时刻牢记,线程是抢占式执行的!!!      

        wait 无参数 就是死等

        wait 带参数,就是指定了最大等待时间。

        wait看起来和sleep 有点像:虽然都是能指定等待时间,虽然也都能被提前唤醒(wait使用notify唤醒,sleep使用interrupt唤醒)。但是表示的含义截然不同。notify唤醒wait 是不会有任何异常的。(正常的业务逻辑)   interrupt唤醒sleep则是出异常了(表示一个出问题了的逻辑

        

        如果有多个线程在等待object对象,此时有一个线程在执行 object.notify(),这时是随机唤醒一个等待的线程,(不知道具体是哪个)    notifyAll是唤醒所有wait object的线程。


        我是专注学习的章鱼哥~

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

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

相关文章

华为ensp:静态默认路由

静态路由 到r2 上的系统视图模式 下一跳为1.1.1.2 ip route-static 192.168.2.0 255.255.255.0 1.1.1.2 如果找2网段下一跳为1.1.1.2接口 默认路由 到r3上做的是默认路由 ip route-static 0.0.0.0 0 1.1.1.1 所有的流量去找1.1.1.1 查看效果 只要做完完整的路由就可…

HIKVISION流媒体管理服务器后台任意文件读取漏洞

默认账号密码为 admin/12345 构造payload /systemLog/downFile.php?fileName../../../../../../../../../../../../../../../windows/system.ini漏洞证明 文笔生疏&#xff0c;措辞浅薄&#xff0c;望各位大佬不吝赐教&#xff0c;万分感谢。 免责声明&#xff1a;由于传播…

无需标注海量数据,目标检测新范式OVD

当前大火的多模态GPT-4在视觉能力上只具备目标识别的能力&#xff0c;还无法完成更高难度的目标检测任务。而识别出图像或视频中物体的类别、位置和大小信息&#xff0c;是现实生产中众多人工智能应用的关键&#xff0c;例如自动驾驶中的行人车辆识别、安防监控应用中的人脸锁定…

若依如何进行页面路由跳转,路由跳转时如何携带参数(超详细图文教程)

我们经常会有这样需求&#xff0c;当我们在一个页面时&#xff0c;想要跳转到另一个页面&#xff0c;但是跳转的同时还需要携带参数。那么这种情况在若依系统中该如何做呢&#xff0c;下面我们来说一下。 文章目录 问题提出&#xff1a;一、创建目标页面的路由(也就是图2的路由…

C语言--有3个候选人,每个选民只能投票选一人,要求编一个统计选票的程序,先后输入被选人的名字,最后输出各人得票结果。

一.解体思路 设一个结构体数组&#xff0c;数组中包含3个元素; 每个元素中的信息应包括候选人的姓名和得票数;输入被选人的姓名&#xff0c;然后与数组元素中的“姓名”成员比较&#xff0c;如果相同&#xff0c;就给这个元素中的“得票数”成 员的值加1;输出所有元素的信息。 …

win下安卓打包指南

win下安卓打包指南 0、缘起 换了台电脑竟然忘了怎么打包&#xff0c;还好有笔记&#xff0c;用软件打包也挺好&#xff0c;但是我感觉用 命令行 更有操作感&#xff0c;分享下。 1、下载并配置apktool&#xff08;放在C://Windows无需配置环境变量&#xff0c;需要java环境&…

【算法练习Day45】最长公共子序列不相交的线最大子数组和

​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;练题 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 文章目录 最长公共子序列不相交的线最…

Linux输入与输出设备的管理

计算机系统中CPU 并不直接和设备打交道&#xff0c;它们中间有一个叫作设备控制器&#xff08;Device Control Unit&#xff09;的组件&#xff0c;例如硬盘有磁盘控制器、USB 有 USB 控制器、显示器有视频控制器等。这些控制器就像代理商一样&#xff0c;它们知道如何应对硬盘…

如何在 Windows 11 上恢复丢失的文件?(4种方法)

在 Windows 11 设备上丢失重要文件感觉就像一场噩梦。这是您希望时光倒流并撤消意外删除或避免那些意外的系统故障的时刻之一。这种情况带来的挫败感和焦虑感简直难以承受。但是&#xff0c;嘿&#xff0c;不要绝望&#xff01;我们随时为您提供帮助。 在这本真诚的指南中&…

CA 陪你看 Ignite | 聚焦 Microsoft Ignite 2023

点击蓝字 关注我们 编辑&#xff1a;Alan Wang 排版&#xff1a;Rani Sun 微软 Reactor 为帮助广开发者&#xff0c;技术爱好者&#xff0c;更好的学习 .NET Core, C#, Python&#xff0c;数据科学&#xff0c;机器学习&#xff0c;AI&#xff0c;区块链, IoT 等技术&#xff0…

android studio 修改图标

Android Studio 修改图标 简介 Android Studio 是一款由谷歌推出的用于开发 Android 应用程序的集成开发环境&#xff08;IDE&#xff09;。在开发过程中&#xff0c;我们可以根据自己的需求修改 Android Studio 的图标&#xff0c;以个性化我们的开发环境。 本文将介绍如何在…

XC1010非隔离型、低成本的PWM功率开关、AC-DC 220V转5V 200mA小电流芯片

XC1010是一款非隔离型、高集成度且低成本的PWM功率开关&#xff0c;适用于降压型和升降压型电路。 XC1010采用高压单晶圆工艺&#xff0c;在同一片晶圆上集成有 500V 高压 MOSFET 和采用开关式峰值电流模式控制的控制器。在全电压输入的范围内可以保证高精度的 5V 默认…

LoRAShear:微软在LLM修剪和知识恢复方面的最新研究

LoRAShear是微软为优化语言模型模型(llm)和保存知识而开发的一种新方法。它可以进行结构性修剪&#xff0c;减少计算需求并提高效率。 LHSPG技术&#xff08; Lora Half-Space Projected Gradient&#xff09;支持渐进式结构化剪枝和动态知识恢复。可以通过依赖图分析和稀疏度…

【3】Gradle-快速入门使用【Gradle概念】

目录 【3】Gradle-快速入门使用【Gradle概念】Gradle任务查看可用任务了解任务探索任务依赖性 依赖关系了解传递依赖关系查看项目依赖项添加版本目录 【可选】 插件使用插件查看插件提供的任务配置插件 增量构建启用缓存使用构建缓存步骤总结 个人主页: 【⭐️个人主页】 需要您…

【tg】 5 :线程切换

manager 可以切到 其他类的其他线程去执行。线程切换 先通过 networkmgr 线程 执行 ,但是传递了Manager 自己的线程 进去。在networkmgr 的network线程中,获取到stats数据,然后扔给 manager的线程thread ,去posttask 还行这个task里调用了mediamanager 的perform ,在media…

研究方法——案例研究设计与方法

作者&#xff1a;罗伯特K.殷 &#xff08;一&#xff09;计划&#xff1a;如何把握何处、何时用案例研究方法 1.问题&#xff1a; 按照作者的观点&#xff0c;案例研究1984年之后才逐渐得到重视&#xff0c;可是在数据信息有效收集的时代&#xff0c;几乎所有的经典都是以案例…

[西湖论剑 2022]real_ez_node

文章目录 前置知识EJS模板注入&#xff08;CVE-2022-29078&#xff09;原型链污染漏洞 &#xff08;CVE-2021-25928&#xff09;HTTP响应拆分攻击&#xff08;CRLF&#xff09; 解题过程代码审计构造payload 前置知识 EJS模板注入&#xff08;CVE-2022-29078&#xff09; EJS…

LeetCode18-四数之和

注意!其中nums数值的范围,四个加一起会导致INT溢出,long类型则是64位的整数,因此不会导致溢出,这也是本题难点之一! 大佬解法(拿捏offer的解法) 经过反复的代码比对和Debug,发现大佬解法的速度之快体现在足足7个if语句的剪枝,其中包括了2个关键性的去重的if语句以及2个关键性…

openpnp - 74路西门子飞达控制板(主控板STM32_NUCLEO-144) - 验证

文章目录 openpnp - 74路西门子飞达控制板(主控板STM32_NUCLEO-144) - 验证概述笔记重复数字IO的问题想法手工实现程序实现确定要摘掉的数字重合线自动化测试的问题测试程序的场景测试程序的运行效果测试程序实现备注END openpnp - 74路西门子飞达控制板(主控板STM32_NUCLEO-14…

推荐这款机器学习的特征筛选神器!

大家好&#xff0c;特征选择是机器学习建模流程中最重要的步骤之一&#xff0c;特征选择的好坏直接决定着模型效果的上限&#xff0c;好的特征组合甚至比模型算法更重要。除了模型效果外&#xff0c;特征选择还有以下几点好处&#xff1a; 提高模型性能并降低复杂性&#xff08…