JavaEE初阶第九期:解锁多线程,从 “单车道” 到 “高速公路” 的编程升级(七)

专栏:JavaEE初阶起飞计划

个人主页:手握风云

目录

一、wait和notify

[1.1. wait和sleep的对比](#1.1. wait和sleep的对比)

二、多线程案例

[2.1. 单例模式](#2.1. 单例模式)


一、wait和notify

1.1. wait和sleep的对比

  • wait的设计是提前唤醒,超时的时间属于是"留后手",而sleep是到时间才唤醒,虽然也可以通过Interrupt()提前唤醒,但会出现异常。
  • wait需要搭配锁来使用,wait执行时需要先释放锁,当sleep放在synchronized内部时,不会释放锁。

二、多线程案例

2.1. 单例模式

单例模式是一种设计模式,属于校招中最常考的设计模式之一。设计模式可以看作在下棋中的研究出来的一些固定套路。为了使一些代码能力比较差的人下限能够得到提升,大佬们研究出一些固定套路来应对一些场景的题。

在多线程编程中,单例模式是控制资源访问的核心设计模式之一。它确保一个类仅有一个实例,并提供全局访问点,避免多线程竞争导致的资源重复创建或状态不一致问题。

单例模式实现的方式有很多,最常见的是饿汉模式和懒汉模式。

  • 饿汉模式
java 复制代码
// 饿汉模式
class Singleton {
    // 创建唯一实例
    // 此处的instance属于static成员,创建时机,就是在类加载的时候
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {
    }
}

public class Demo1 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);// s1和s2指向同一对象

        Singleton s3 = new Singleton();
    }
}

如果所有的代码都是一个人写的,那么有没有单例无所谓,就能人为保证只有一个实例,这些规则自己都能遵守,但在实际工作中,多人一起分工协作,某人对某个方法进行封装,别人无法调用,强制去使用某种规则。

  • 懒汉模式
java 复制代码
// 懒汉模式
class SingletonLazy {
    public static SingletonLazy instance = null;

    // 第一次使用实例的时候才会创建
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

public class Demo2 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

如同洗碗一样,吃完饭就洗就是饿汉模式,吃完饭之后等下次吃饭之前再洗就是懒汉模式。如果第一次吃饭用了4个碗,等下次吃饭需要2个碗,按照懒汉模式,下次吃饭前只需洗两个碗,所以懒汉模式效率更高。假设有一个几个G的大文件,当我们想打开文件查看内容时,如果把整个文件加载显示出来显示给用户,就会有明显的卡顿,但按照懒汉模式,只加载一小部分就显示给用户,效率就会大大提高。

从线程安全的角度来说,饿汉模式只是针对同一变量进行读取,因此线程是安全的。但懒汉模式下,当多个线程同时检测到instance == null,就可能会有多个线程创建出多个实例。为了解决线程安全的问题,还是利用加锁来解决。

java 复制代码
class SingletonLazy {
    private static SingletonLazy instance = null;
    private static Object locker = new Object();
    public static SingletonLazy getInstance() {
        // 注意要把哪些操作打包成原子
        synchronized (locker) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
}

public class Demo3 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
    }
}

刚才的单例模式只是在第一次调用的时候,才会涉及到线程安全问题,只要对象创建完毕,后序都是直接返回的操作,就不涉及修改了。但上述代码每次调用都会加锁,线程已经安全了,却还要进行加锁,显然是不合理的,因为加锁就可能触发阻塞。

java 复制代码
class SingletonLazy {
    private static SingletonLazy instance = null;
    private static Object locker = new Object();
    public static SingletonLazy getInstance() {
        if (instance == null) {
            // 注意要把哪些操作打包成原子
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

public class Demo3 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
    }
}

看到上面的代码,可能会打破某些人的认知。以前都是一个线程,代码执行下来,一次判定和二次判定逻辑是一样的。但在多线程编程中,一次判定和二次判定的结论可能不同。

代码写到这里,还是会有一些问题,就是之前提到的指令重排序引起的线程安全问题。指令重排序是编译器的一种优化手段,确保逻辑一致的前提下,通过调整代码的顺序,从而提升效率。上面创建对象的过程可以简化成3个步骤:1.申请内存空间;2.在内存空间上通过构造方法进行初始化;3.将内存地址保存到引用变量中。编译器就可能优化成"132"的顺序,就可能出现问题。

当先执行步骤3时,instance确实非空了,t2就会提前拿到一个未初始化的对象,当去调用s的属性和方法时,就会讲错就错。此时我们就可以通过volatile关键字避免重排序。

java 复制代码
class SingletonLazy {
    private static volatile SingletonLazy instance = null;
    private static Object locker = new Object();
    public static SingletonLazy getInstance() {
        if (instance == null) {
            // 注意要把哪些操作打包成原子
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

public class Demo3 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
    }
}
相关推荐
IsSh9nj6q21 分钟前
Python全栈应用搭建神器magic-dash .新版本介绍
开发语言·python·dash
知彼解己30 分钟前
Eclipse Temurin:企业级 Java JDK 发行版的最佳实践
java·ide·eclipse
朱容zr33313331 分钟前
请解释“回表”的概念。
java·前端·数据库
不吃辣49032 分钟前
vibe coding | 如何做一个 AI 音乐生成工具?
java·人工智能·后端·ai·ai编程
Cachel wood42 分钟前
hands-on-modern-rl:动手学强化学习策略梯度reinforce
开发语言·python
NWU_LK44 分钟前
【WebFlux】第八篇 —— 自定义调度器
java
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第210题】【10_网络协议篇】第1题:说说 TCP/IP 网络五层模型
java·网络协议·tcp/ip·计算机网络·网络五层模型
wuminyu1 小时前
JUC组件逐层剥离与深度剖析
java·linux·c语言·jvm·c++·算法
大黄说说1 小时前
EF Core 避坑指南:查询慢、循环查询、并发更新问题如何解决
java·服务器·数据库
蓝斯4971 小时前
一碰即传,重构跨设备文件分享体验
开发语言·python·重构