初始JavaEE篇——多线程(4):wait、notify,饿汉模式,懒汉模式,指令重排序

news2024/11/23 4:05:52

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-CSDN博客

所属专栏:JavaEE

目录

wait、notify 方法

多线程练习

单例模式

饿汉模式

懒汉模式

指令重排序 


wait、notify 方法

wait 和 我们前面学习的sleep、join方法一样,也是让线程阻塞,但是其可以被notify方法唤醒,但是sleep是被Interrupt给提前唤醒或者指定时间过了之后自动被唤醒,并且会抛出异常。且 join 是一个线程等待另一个线程,并且要 被等待的线程彻底执行完成之后,等待的线程才会从阻塞的中被唤醒重新执行。

wait方法在使用时,要和synchronized一起搭配使用,因为其是先对调用它的对象进行解锁,阻塞,在被唤醒之后,在进行加锁操作。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Thread t = new Thread(()->{
            System.out.println("wait之前");
            synchronized (locker1) { // 加锁
                try {
                    locker1.wait(); // 进入wait方法解锁,出wait方法加锁
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("wait之后");
        });
        t.start();
        // 为了让t线程先wait阻塞等待,得先休眠主线程一会:
        // 可以使用sleep方法,也可以使用IO的方法阻塞
        Thread.sleep(1000);
        System.out.println("输入任意内容,唤醒t线程");
        Scanner scanner = new Scanner(System.in);
        scanner.next();
        // 要出wait方法就需要notify进行唤醒操作
        synchronized (locker1) {
            locker1.notify();
        }
    }
}

注意:

1、在Java中,wait 和 notify 方法一定是和 synchronized 一起使用的。

2、在1的基础上,四者的进行加锁解锁的操作一定是针对同一个锁对象。

3、notify 的唤醒操作一定是在 wait 之前才能有效的唤醒。如果先执行了 notify 的唤醒操作,但是 还没有执行wait的阻塞操作的话,那么线程就一直会阻塞,但是 notify 的唤醒操作对线程本身是不会有影响的。

4、wait 和 notify 方法是 Object 对象的方法,即所有对象都可以使用这两个方法。

5、如果有多个线程处于 wait 的阻塞状态,那么 notify 一次只能随机唤醒一个线程。如果想要全部唤醒的话,得使用 notifyAll 方法。当然,也可以使用 notifyAll 去唤醒一个线程。

6、wait 和 join一样,也提供了最大等待时间。当超出这个最大等待时间时,被 wait 方法阻塞的线程将不会在处于阻塞状态。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t = new Thread(()->{
            System.out.println("wait之前");
            synchronized (locker) {
                try {
                    locker.wait(5000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("wait之后");
            System.out.println("t线程结束");
        });

        t.start();
        Thread.sleep(1000);
        System.out.println("输入任意内容,唤醒t线程");
        Scanner scanner = new Scanner(System.in);
        scanner.next();
        synchronized (locker) {
            locker.notify();
        }
    }
}

当我们迟迟没有去输入值时,如果已经超过了 wait 的最大阻塞时间的话, wait 便不会去阻塞 t 线程了,而是会让其继续执行下去,即使我们后续再次输入值来执行 notify 的唤醒操作,也不再有用了。

7、当一个线程执行到 wait 之后,这个锁被释放了,也就意味着有别的线程可以使用这把锁了。 

多线程练习

到此为止,我们已经学习了不少的多线程知识,现在我们就来练习一下。

题目:

有三个线程:t1、t2、t3,三者分别打印A、B、C,现在我们需要打印10次ABC。 

思路:

1、既然打印有先后顺序,那么我们肯定是可以通过手动控制sleep 的休眠时间来决定的。

2、刚刚我们学习了 wait 和 notify ,应该是可以想到这个应用场景的,完全对上了。一个 打印完A之后,唤醒另一个线程,打印B,接着唤醒另一个线程打印C,最后 t3线程唤醒 t1线程,就这样相互唤醒打印,而 main 线程用来唤醒 t1 线程开始最初的打印即可。

代码实现:

1、暴力-sleep:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.print("A");
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.print("B");
            }
        });

        Thread t3 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("C");
            }
        });

        t1.start();
        Thread.sleep(10); // 确保 t1是最先执行的
        t2.start();
        Thread.sleep(10); // 确保 t2比t1后执行,比t3先执行
        t3.start();
    }
}

注意:这里使三个线程的执行顺序的确定,其休眠的时间不能过长,否则不好衔接。 

2、wait-notify版本:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Object locker3 = new Object();

        Thread t1 = new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    synchronized (locker1) {
                        locker1.wait();
                    }
                    System.out.print("A");
                    synchronized (locker2) { // 要清楚唤醒的是哪个线程
                        locker2.notify();
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        Thread t2 = new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    synchronized (locker2) {
                        locker2.wait();
                    }
                    System.out.print("B");
                    synchronized (locker3) { // 要清楚唤醒的是哪个线程
                        locker3.notify();
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        Thread t3 = new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    synchronized (locker3) {
                        locker3.wait();
                    }
                    System.out.println("C");
                    synchronized (locker1) { // 要清楚唤醒的是哪个线程
                        locker1.notify();
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        t1.start();
        t2.start();
        t3.start();
        // 确保t1先执行到了wait
        Thread.sleep(1000);
        synchronized (locker1) { // notify一定要和synchronized配合使用
            locker1.notify();
        }
    }
}

单例模式

单例模式属于设计模式的一种,是指一个进程中,一个类只能实例化一个对象,即单个实例。那怎么去实现一个进程中只能有一个对象呢?直接把构造方法改为private即可,这样在外部就不能创建实例了。 

单例模式中,最常见的就是饿汉模式与懒汉模式。 

饿汉模式

饿汉模式,主要体现在"饿"字上,因为其是迫不及待的去创建类的实例。

代码演示:

// 饿汉模式
class SingleTon {
    // 迫不及待的创建实例
    private static SingleTon singleTon = new SingleTon();

    public static SingleTon getInstance() {
        return singleTon;
    }

    // 单例模式的构造方法一定是private修饰的
    private SingleTon() {

    }
}

这里创建类的实例是通过创建一个静态的成员变量来实现的,而静态的成员变量是类在加载时,就会被创建,即JVM中有这个类存在的痕迹的话,那么这个实例就会存在。 因此,以"饿"得名。

我们也可以去检查这个饿汉模式是否创建成功,主要检查是否是单例模式。

public class Test {
    public static void main(String[] args) {
        // SingleTon s = new SingleTon(); // error

        SingleTon s1 = SingleTon.getInstance();
        SingleTon s2 = SingleTon.getInstance();
        System.out.println(s1 == s2); // true
    }
}

从上面的程序运行的结果,可以得知:一个进程中不能实例化多个对象,符合单例模式的特征。

懒汉模式

懒汉模式,主要体现在"懒"字上,只有当迫不得已时,才去创建实例。

代码演示:

// 懒汉模式
class SingleTonLazy {
    // 迫不得已才创建实例
    private static SingleTonLazy singleTonLazy = null;

    public static SingleTonLazy getInstance() {
        if (singleTonLazy == null) {
            singleTonLazy = new SingleTonLazy(); // 一定要把对象保留下来
        }
        return singleTonLazy;
    }

    // 单例模式的构造方法一定是私有的
    private SingleTonLazy() {

    }
}

懒汉模式只有当外部调用getInstance方法时,才会去创建实例,否则就不会创建实例。 

同样也可以去测试这个懒汉模式是否创建成功。

public class Test {
    public static void main(String[] args) {
        // SingleTonLazy s = new SingleTonLazy(); // error

        SingleTonLazy s1 = SingleTonLazy.getInstance();
        SingleTonLazy s2 = SingleTonLazy.getInstance();
        System.out.println(s1 == s2); // true
    }
}

上面的懒汉模式在单线程下使用没问题,但是在多线程下使用,便会出现线程安全问题。(饿汉模式之所没有线程安全问题,是因为饿汉模式只是进行return的"读"操作,而不是和懒汉模式一样,有"写"操作)

因为懒汉模式的创建线程虽然只是一个赋值代码,也就是对应一条CPU指令,但是有了 if 语句之后,两者就不算是原子的了。

例如,当线程1去实例化一个对象时,执行到 if 语句,但偏偏此时操作系统将其从CPU上踢下去了,然后线程2就也去CPU上执行了实例化对象的操作,和线程1一样只是执行到 if 语句,也被赶下去了,接着 线程1执行了赋值语句成功的创建了一个对象,然后线程2又被调度到CPU上了,也执行了创建对象的赋值语句。

上面就会导致两个问题:

1、 这里new了两次,即创建了两次对象破坏了单例模式的初衷。

2、后一次new的对象会覆盖前面的对象,可以会对程序的数据造成影响,最终导致程序崩溃。

这里有小伙伴可能会疑惑:为什么线程1创建了对象之后,线程2还会去创建对象呢?因为线程1创建完成之后,线程2已经执行到了 if 语句之中,其认为还没有创建对象。

因此,我们得对上述代码进行加锁操作。

// 懒汉模式
class SingleTonLazy {
    // 迫不得已才创建实例
    private static SingleTonLazy singleTonLazy = null;
    private static Object locker = new Object();

    public static SingleTonLazy getInstance() {
        synchronized (locker) {
            if (singleTonLazy == null) {
                singleTonLazy = new SingleTonLazy(); // 一定要把对象保留下来
            }
        }
        return singleTonLazy;
    }

    // 单例模式的构造方法一定是私有的
    private SingleTonLazy() {

    }
}

加锁操作确实可以实现线程安全,但是它也会造成程序的性能下降,因为当对象的实例被创建出来后,别的线程再去调用这个方法时,就会进行加锁操作,而加锁对于最终的结果来说没影响,也就是加锁加了个寂寞,这就是在浪费时间了。因此,也就导致了性能下降了。

我们的解决方法是在锁的最外层再加上一个 if 语句去判断,这样即使有了实例之后,别的线程再尝试去创建实例时,就会直接return,而不会再去进行加锁操作了,这样性能就提升了不少。

    public static SingleTonLazy getInstance() {
        if (singleTonLazy == null) {
            synchronized (locker) {
                if (singleTonLazy == null) {
                    singleTonLazy = new SingleTonLazy(); // 一定要把对象保留下来
                }
            }
        }
        return singleTonLazy;
    }

指令重排序 

上面的懒汉模式代码,还是有点问题,这个问题和指令重排序有关。

概念:指令重排序是指在不影响代码的执行逻辑的基础上,编译器对要执行的代码其底层对应的计算机指令进行了优化处理,会使其与原来的执行顺序不一致。

懒汉模式的指令重排序体现在 赋值语句。我们先来学习一下,这个赋值语句,其底层对应的逻辑:1、向内存申请了一块空间;2、在这块空间内构造对象(初始化成员变量等)3、将这块空间的首地址给到引用变量。如果将上述三个操作类比到我们日常生活的话,那就是1、买房子;2、装修;3、拿到钥匙。

指令重排序可能会使这个1、2、3的顺序打乱,变成1、3、2。虽然这个在日常生活中,即使打乱之后,我们也是不会直接入住的,因为还没有装修,但是计算机可不一样,它是一个铁憨憨,他只知道执行工作,因此当它执行了1、3之后,也就是拿到了这个对象的引用之后,如果此时操作系统将其从CPU上踢下去了,让别的线程来执行相关方法的话,这个操作就不亚于在毛坯房中直接拎包入住的行为了。这可能直接就把程序给搞崩溃了。因此,我们不能让指令重排序的行为发生,这里就需要用到 volatile 关键字了。这个关键字既可以避免 内存可见性的问题,也可以避免指令重排序的问题。

private static volatile SingleTonLazy singleTonLazy = null;

好啦!本期 初始JavaEE篇——多线程(4):wait、notify,饿汉模式,懒汉模式,指令重排序 的学习之旅 就到此结束啦!我们下一期再一起学习吧!

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

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

相关文章

MySQL-基础汇总

MySQL-基础汇总 数据库对于任何一个从事后台开发的人说都是永远躲不掉的&#xff0c;任何系统或程序离开了数据的支持都变的毫无意义。而管理数据的工具——数据库就显得尤为重要。本章节我们的核心就是 MySQL&#xff0c;相信很多小伙伴跟我一样&#xff0c;也沉浸在增、删、…

【AD】1-2 AD24软件的中英文版本切换

1.如图设置软件后&#xff0c;关闭软件重新打开。如果想要切换回英文&#xff0c;将③勾选去掉&#xff0c;关闭软件重新在打开即可。

CSS、Less、Scss

CSS、Less和SCSS都是用于描述网页外观的样式表语言&#xff0c;但它们各自具有不同的特点和功能。以下是对这三者的详细阐述及区别对比&#xff1a; 详细阐述 CSS&#xff08;Cascading Style Sheets&#xff09; 定义&#xff1a;CSS是一种用来表现HTML或XML等文件样式的计算机…

【Python项目管理】“无法创建虚拟环境”报错原因及解决方法

一、问题说明 笔者最近在做一个python项目&#xff08;使用pycharm IDE&#xff09;&#xff0c;在添加python解释器时&#xff0c;提示无法创建虚拟环境&#xff08;Unable to create virtual environment&#xff09;&#xff0c;如下2图所示&#xff1a; 【添加python解释…

【实践】某央企研究院如何打造IT监控告警平台?

01客户简介&#xff1a; 案例客户为某央企下属研究院。 02痛点分析&#xff1a; 随着信创国产化持续推进&#xff0c;案例客户已完成部分IT核心系统的替代&#xff0c;部署了一系列国产软硬件设施&#xff0c;如Kylinv10操作系统、融智通网络设备等。由于信创生态不够成熟&a…

qt QBrush详解

1、概述 QBrush是Qt框架中的一个基本图形对象类&#xff0c;它主要用于定义图形的填充模式。QBrush可以用于填充如矩形、椭圆形、多边形等形状&#xff0c;也可以用于绘制背景等。通过QBrush&#xff0c;可以设置填充的颜色、样式&#xff08;如实心、渐变、纹理等&#xff09…

0-1规划的求解

实验类型&#xff1a;◆验证性实验 ◇综合性实验 ◇设计性实验 实验目的&#xff1a;学会使用Matlab编程实现求解0-1规划。 实验内容&#xff1a;1.学习使用Matlab定义子函数的命令function&#xff1b; 2.编程求解0-1型整数规划的枚举法或隐枚举法。 例1&#xff1a;求…

禾川HCQ1控制器程序编译报错如何解决

1、第一次打开用户程序 2、提示库未安装 3、安装库文件 4、脉冲轴库未安装 5、没有错误 去禾川自动化官网,把可以安装的包和库都安装下,程序编译就没有错误了。 6、下载相关包文件

C++进阶-->AVL树的实现

1. AVL树的介绍 1、AVL树的名字来源于他的发明者G. M. Adelson-Velsky和E. M. Landis两个前苏联的科学家&#xff0c;他们名字首元素组成。 2、AVL树就是我们前面二叉搜索树实现的时候提到的平衡二叉搜索树即二叉搜索树的左右孩子都是AVL树&#xff0c;即左右子树的高度差的绝…

【网络安全】|nessus使用

1、扫描结果分析&#xff1a; Sev&#xff1a;漏洞的严重性级别 CVSS&#xff1a;量化漏洞严重性的标准&#xff0c;通过计算得出一个分数&#xff0c;分数越高表示漏洞越严重。 VPR&#xff1a;基于风险的评分系统&#xff0c;帮助组织优先处理风险最高的漏洞。 EPSS&#xf…

P2-5【C语言基本数据类型、运算符和表达式】第五节-知识要点:格式输出函数printf()

讲解视频&#xff1a; P2-5【C语言基本数据类型、运算符和表达式】第五节-知识要点&#xff1a;格式输出函数printf() 知识要点&#xff1a;格式输出函数printf()。 一、任务分析 已知三角形三边a&#xff0c;b&#xff0c;c的值&#xff0c;求三角形的面积。要求输出a&#…

RFID资产管理

随着物联网和智能制造的发展&#xff0c;RFID资产管理逐渐成为企业提升运营效率的重要工具。利用RFID技术&#xff0c;企业能够实时跟踪和管理各种固定资产&#xff0c;从而提高资产利用率&#xff0c;降低运营成本。在现代化的管理体系中&#xff0c;RFID资产管理不仅限于资产…

Vue2——单页应用程序路由的使用

一.单页应用程序与多页应用程序之间的比较 二.单页的应用场景 系统类网站 / 内部网站 / 文档类网站 / 移动端网站 三.路由的介绍 1. 什么是路由 路由是一种映射关系 2. Vue中的路由是什么 路径和组件的映射关系 四.VueRouter的使用 5个基础步骤&#xff08;固定&#xff09; …

苹果ipa上架apple store 遇到的问题汇总已经解决方案!

大家伙&#xff0c;我是小黄。 最近在将ipa上架到apple store的时候遇到了一些问题&#xff0c;经过很长时间的摸索和修改终于成功上架了&#xff0c;下面是我遇到的问题和解决过程&#xff0c;希望可以帮助到大家。 一&#xff1a; Guideline 1.3 - Safety - Kids Category …

数据库三范式(1NF、2NF、3NF)

1NF&#xff08;第一范式&#xff09; 定义&#xff1a;确保每一列都是原子值&#xff0c;即是不可分割的基础数据项。 所谓第一范式&#xff08;1NF&#xff09;是指在关系模型中&#xff0c;对于添加列的一个规范要求&#xff0c;所有的列都 应该是原子性的&#xff0c;即数…

亚马逊CEO安迪·贾西(Andy Jassy)近日透露,Alexa助手即将迎来一次重大升级,具备“代理性”功能

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

ts:函数的重载

ts&#xff1a;函数的重载 1 主要内容说明2 例子2.1 函数的重载2.1.1 源码1 &#xff08;函数的重载&#xff09;2.1.2 源码1运行效果 3.结语4.定位日期 1 主要内容说明 重载函数可以提高类型安全性&#xff0c;内容灵活性和可读性。重载允许同一个函数定义多个参数类型的数量…

qt QTabWidget详解

1、概述 QTabWidget是Qt框架中的一个控件&#xff0c;它提供了一个标签页式的界面&#xff0c;允许用户在不同的页面&#xff08;或称为标签&#xff09;之间切换。每个页面都可以包含不同的内容&#xff0c;如文本、图像、按钮或其他小部件。QTabWidget非常适合用于创建具有多…

telnet 密码模式 访问路由器

telnet 密码访问华为路由器 模拟被访问路由 sy [Huawei]int g0/0/0 //选中 g0/0/0端口 [Huawei-GigabitEthernet0/0/0]ip add 192.168.1.1 24 //设置端口ip [Huawei]user-interface vty 0 4 //配置vty [Huawei-ui-vty0-4]set authentication password cipher huawei123 //设置…

项目模块1~12总结:服务器大模块梳理

一、思维导图 二、设计思路 1、各种回调函数梳理 服务器里面包含了监听套接字和监听到的通信套接字&#xff08;新连接&#xff09;&#xff0c;我们要对这两种套接字进行设置回调函数&#xff0c;其中监听套接字里面只要设置读回调&#xff0c;通信套接字要设置5种回调&…