多线程编程的简单案例——单例模式[多线程编程篇(3)]

目录

前言

[1.wati() 和 notify()](#1.wati() 和 notify())

[wait() 和 notify() 的产生原因](#wait() 和 notify() 的产生原因)

如何使用wait()和notify()?

案例一:单例模式

饿汉式写法:

懒汉式写法

对于它的优化

再次优化

结尾

前言

如何简单的去使用jconsloe 查看线程 (多线程编程篇1)_eclipse查看线程-CSDN博客

浅谈Thread类及常见方法与线程的状态(多线程编程篇2)_thread.join() 和thread.get()-CSDN博客

这是系列的第三篇博客,这篇博客笔者想结合自己的学习经历,分享几个多线程编程的简单案例,帮助读者们更快的理解多线程编程,也非常感激能耐心阅读本系列博客的读者们!

本篇博客的内容如下,您可以通过目录导航直接传送过去

1.介绍wait()和notify()这两个方法

2.介绍单例模式

废话不多说,让我们开始吧,希望我们在知识的道路上越走越远!

博客中出现的参考图都是笔者手画或者截图的的

代码示例也是笔者手敲的!

影响虽小,但请勿抄袭

1.wati() 和 notify()

wait() 和 notify() 的产生原因

在多线程编程中,多个线程同时读写共享资源非常常见。假设两个线程要交替操作一个数据,比如:

  • 线程A:负责生产数据;

  • 线程B:负责消费数据。

如果没有协调机制,线程A和线程B的执行顺序完全由CPU调度,极有可能出现这种情况:

  • 线程B执行时,发现A还没生产好;

  • 线程A刚生产好,B却还没来消费。

这样会出现资源使用错误,甚至死循环。

所以,Java提供了 wait()notify(),解决线程之间通信的问题,帮助程序做到:

一个线程在条件不满足时,自动等待。
另一个线程操作完后,主动唤醒等待的线程。

这种机制,叫做等待-通知机制"。

具体来说:

wait()方法:让指定的程序进入阻塞状态
wait 结束等待的条件 :

1.其他线程调用该对象的 notify 方法 .
2.wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本 , 来指定等待时间 ).
3.其他线程调用该等待线程的 interrupted 方法 , 导致 wait 抛出 InterruptedException 异常 .

notify()方法:唤醒对应的处在阻塞状态的线程.

举个生活中的例子:

假设你去银行取号排队:

  • 你取号后坐在椅子上等待(相当于调用 wait() 进入等待状态)。

  • 银行的叫号系统喊你的号码时,你再去窗口办理业务(相当于 notify() 唤醒你)。

如果没有这个等待机制,你可能得不停地站在窗口问"轮到我了吗?什么时候才能到我啊?前面的人能不能tm快点啊!"(浪费CPU资源)

有了 wait()notify(),就能让线程"高效地等待"而不是死循环轮询

如何使用wait()和notify()?

OK了解了他们的概念和作用,接下来,笔者将介绍如何使用wait()和notify()

首先,读者们需要了解一些前置知识

第一:根据源码文档,wait() 方法在调用时,必须处理 InterruptedException

因此使用时要么用 try-catch 捕获 ,要么在方法上声明 throws,否则代码无法通过编译。

第二:wait() 和 notify() 方法并不是定义在 Thread 类中,而是属于 Object 类的方法。

所以在实际使用中,我们通常需要先创建一个 Object 对象 ,通过这个对象来调用 wait()和 notify(),并且配合 synchronized 关键字一起使用,确保线程安全。

请看一组示例代码:

java 复制代码
public class Demo
{
    public static void main(String[] args) {
         Object  ob = new Object();
         Object  lock = new Object();
         Thread thread1 = new Thread(() ->
         {
            synchronized (ob)
            {
                System.out.println("wait 之前");
                try {
                    ob.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
             System.out.println("进入了");
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
             System.out.println("wait 之后");

         });
wait 做的事情:
使当前执行代码的线程进行等待. (把线程放到等待队列中)
释放当前的锁
满足一定条件时被唤醒, 重新尝试获取这个锁.
        Thread thread2 = new Thread(()->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (ob)
            {
                System.out.println("通知了");
                ob.notify();
            }
        });
         thread1.start();
         thread2.start();
    }
}

在使用 wait()notify() 这两个方法时,有一个非常重要的前提条件:

调用它们时,必须先持有调用对像的锁,而且必须时同一个对像,否则会抛出异常

我们一定要保证,哪个对像调用了wati(),哪个对像就要调用notify(),或者也要设置好阻塞时间.

java 复制代码
synchronized (ob) {
    ob.wait();  //  正确,线程1的锁对象是 ob
}

synchronized (lock) {
    ob.notify();  //  错误,线程2的锁对象是 lock,调用 notify 却针对 ob
}
错误写法
java 复制代码
synchronized (ob) {
    ob.wait();  //  正确,线程1的锁对象是 ob
}

synchronized (ob) {
    ob.notify();  
正确写法

案例一:单例模式

单例模式是一种设计模式

它保证了一个类在内存中永远只会有一个对象实例.并且提供全局访问点。

举个例子:

假设你要开发一个系统中的配置文件读取器 ,配置文件只需要加载一次,所有模块都要读取相同的配置信息。如果每次调用都重新 new 一个对象,不仅浪费内存,而且可能导致配置不一致。

通过单例模式,你可以保证这个读取器在整个程序运行期间只创建一次,并且全局唯一!

又或者 比如 JDBC 中的 DataSource 实例就只需要一个!!!

单例模式也有两种写法 :

1.懒汉式: 只要在需要被实例化的时候,才会被实例化.

2.饿汉式:顾名思义,在类内部创建唯一实例,并且用 private static final 修饰,保证类一旦被加载了,就开始实例化了

饿汉式写法:

java 复制代码
public class Singleton {
    // 饿汉单例,类一旦被加载,就开始实例化了
    // 1️⃣ 在类内部创建唯一实例,并用 `private static final` 修饰
    private static final Singleton demo = new Singleton();
    // 2️⃣ 私有构造方法,防止外部创建实例
    // 静态代码块
   private Singleton() {
        System.out.println("Singleton 实例被创建");
    }
    // 3️⃣ 提供公共方法获取实例
    public static Singleton getInstance() {
        return demo;
    }
}

在饿汉式单例中,我们会直接在类内部创建好对象实例,当类加载进内存时,实例就已经完成了初始化。

这是因为我们使用了 static 关键字来修饰这个实例,static 属于类本身 ,随着类的加载而初始化。

所以,只要 JVM 加载这个类,单例对象就会被创建,并且保证全局只有一个。

在 Java 中,static 修饰的属性或方法属于类本身 ,而不是某个具体对象。

类被加载到内存 时,所有 static 修饰的成员(属性、方法、代码块)会随类一起初始化,而且只会初始化一次。

也就是说:

  • 类加载时,static 属性会被分配内存并初始化。

  • static 方法属于类本身,不依赖对象,可以通过类名.方法名()调用。

我们简单测试一下:

java 复制代码
class  MyTest
{
    public static void main(String[] args) {

       Singleton s1 =  Singleton.getInstance();

    }
}

调用 Singleton.getInstance()的时候,类被加载,demo被初始化,并且 Singleton() 构造方法被执行,打印"Singleton 实例被创建".

懒汉式写法

类加载的时候不创建实例 . 第一次使用的时候才创建实例 . 我们依据这个思路,写出来懒汉式单例

java 复制代码
public class SingletonLazy {

    // 1️⃣ 声明一个静态变量用来存储实例
    private static  SingletonLazy instance;

    // 2️⃣ 私有构造方法,防止外部创建实例
    private SingletonLazy() {
        System.out.println("SingletonLazy 构造方法执行:对象创建成功!");
    }

    // 3️⃣ 提供公共的静态方法来获取实例,第一次调用时实例化
    public static SingletonLazy getInstance() {

        instance = new SingletonLazy();
        return instance;
    }

为了测试懒汉和饿汉的不同,我们再写两个辅助的静态方法测试:

java 复制代码
public class SingletonLazy {

    // 1️⃣ 声明一个静态变量用来存储实例
    private static  SingletonLazy instance;

    // 2️⃣ 私有构造方法,防止外部创建实例
    private SingletonLazy() {
        System.out.println("SingletonLazy 构造方法执行:对象创建成功!");
    }

    // 3️⃣ 提供公共的静态方法来获取实例,第一次调用时实例化
    public static SingletonLazy getInstance() {

        instance = new SingletonLazy();
        return instance;
    }

    static {
        System.out.println("SingletonLazy 类已加载!");
    }

    public static void printf() {
        System.out.println("调用了静态方法 printf()");
    }

}

测试一下:

java 复制代码
class Test {
    public static void main(String[] args) {
        // 不调用 getInstance 只调用静态方法
        SingletonLazy.printf();  // 会触发类加载,但不会创建对象!

        System.out.println("---------------");

        // 真正调用 getInstance,才会创建对象
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
    }
}

结果如下:

调用静态方法后,类会被加载 ,但此时并不会执行构造方法,也就是说对象还没有被创建。只有当调用 getInstance() 方法时,程序才会真正实例化对象,执行构造方法,完成对象的创建!

我们还可以做一点优化,我们都知道这是单例模式, 只允许有一个对象实例,那么,只有第一次访问时才需要被创建,后续就不用再次创建了,因此可以写成:

java 复制代码
public class SingletonLazy {

    // 1️⃣ 声明一个静态变量用来存储实例
    private static volatile SingletonLazy instance;

    // 2️⃣ 私有构造方法,防止外部创建实例
    private SingletonLazy() {
        System.out.println("SingletonLazy 构造方法执行:对象创建成功!");
    }

    // 3️⃣ 提供公共的静态方法来获取实例,第一次调用时实例化
    public static SingletonLazy getInstance() {
        if(instance == null)
        {
         instance = new SingletonLazy();           
        }
        return instance;
    }
}

如果在单线程编程下,这样就挑不出毛病了!

对于它的优化

但是,假设在多线程环境下,有复数个线程同时调用 getInstance() ,那么就会创建出多个实例

举一个具体的例子

一旦程序进入多线程环境 ,比如存在A、B、C 三个线程,它们几乎在同一时刻调用 getInstance()方法

在这一瞬间,instance 的确是 null,三个线程会同时通过 if 判断 ,然后同时执行 new SingletonLazy(),最终结果就是:

创建了多个实例,破坏了单例模式!!!

因此,我们希望判断是否为空,以及创建实例,这两个动作"原子化"------即不会也不能被打断

怎么办?聪明的你肯定想到了,加锁!

java 复制代码
    public static SingletonLazy getInstance() {     
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        return instance;
    }

加完锁以后,刚刚的情况就会变为:

1.假设程序运行在多线程环境下,A、B、C 三个线程几乎在同一时间,调用了 getInstance() 方法。
2.在这一瞬间,instance 的确是 null,于是三个人一起冲进来,准备创建对象。但是!因为这里加了 synchronized,所以三个线程必须抢锁,只有一个幸运儿能抢到,比如A线程。
3.然后A线程释放锁,B、C线程后面排队进来,发现 instance 已经不再是 null,所以它们就啥也不干,直接返回已有的实例。

4.这样一来,就保证了全局唯一实例,不会被多线程同时创建多个,单例模式真正实现了!

再次优化

不过啊,虽然上面这种"方法加锁"确实解决了多线程下的安全问题------只要一个线程进来了,其他线程就乖乖排队,等着用同一个实例,表面上看没毛病。

但是!问题又来了:

每次调用 getInstance(),都要加锁。

不管 instance 有没有被创建,线程都得卡着 synchronized 排队。

想一想------如果我已经拿到实例了,后面无数次调用其实都只是想用一下这个对象,根本不需要再创建,可还是得老老实实抢锁,这效率能不低吗? 毕竟,加锁的开销也不小了.

所以,聪明的程序员又想了个办法,叫:

双重检查锁(Double-Check Locking),简称 DCL。

核心思路就一句话:

先检查,不满足再加锁,锁住后再检查,确认安全后再创建。

也就是说,外面先检查一次,里面再检查一次,这样只有在 instance 真正等于 null 的时候,才会走到创建对象的逻辑,其他时候,直接跳过锁,快速返回。

java 复制代码
public class SingletonLazy {

    // 加上 volatile,防止指令重排序
    private static volatile SingletonLazy instance;

    private SingletonLazy() {
        System.out.println("SingletonLazy 构造方法执行:对象创建成功!");
    }

    public static SingletonLazy getInstance() {
        if (instance == null) {  // 第一次检查
            synchronized (SingletonLazy.class) {
                if (instance == null) {  // 第二次检查
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

而且还有个小细节,volatile 关键字也别忘了加上!

因为 Java 内存模型中,new 操作可能会被"重排序"

那么,还是刚刚ABC三线程竞争的例子:

A、B、C 三个线程同时调用 getInstance() ,一起执行第一次 if (instance == null)
2. 假设 instance 真的为 null,于是三个线程都准备往下走。
3.

A、B、C 到达 synchronized 这里,开始抢锁。假设A赢了,进入同步代码块。

A 再次执行第二次 if (instance == null),发现确实为空,于是创建 new SingletonLazy()

A 创建完成后,释放锁。
4.

B、C 排队进来,再次检查 if (instance == null),发现已经不为空了,直接跳过创建,返回已存在的实例。
这样对比普通加锁的好处是,实例化以后,先判断一下是否是空,而不是多个线程直接去竞争锁导致资源浪费

总结一句话:
DCL的好处就是,实例化之后,线程们先看一眼:
"对象在不在?"
在,就立刻用!
不在,才排队抢锁。

相比"每次都抢锁"的方式,DCL大幅减少了资源浪费,尤其适合多线程访问频繁的场景。

完整代码:

java 复制代码
public class SingletonLazy {

    // 1️⃣ 声明一个静态变量用来存储实例
    private static volatile SingletonLazy instance;

    // 2️⃣ 私有构造方法,防止外部创建实例
    private SingletonLazy() {
        System.out.println("SingletonLazy 构造方法执行:对象创建成功!");
    }

    // 3️⃣ 提供公共的静态方法来获取实例,第一次调用时实例化
    public static SingletonLazy getInstance() {
        if(instance == null)
        {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
// 外层 if 的作用:
// 避免已经实例化对象的情况下,仍然加锁。因为加锁是一种消耗性能的操作,
// 所以外层先判断,能直接返回就直接返回,提高效率。

// 内层 if 的作用:
// 防止多个线程在 instance == null 的情况下,同时进入同步代码块,
// 抢锁后,重复创建实例。内层 if 可以保证只有第一个抢到锁的线程会创建实例。

// 假设 instance 初始为 null,两个线程 A 和 B 几乎同时调用 getInstance():
// 【第一阶段:外层 if 判断(无锁)】
// - 线程A发现 instance == null,进入同步块等待抢锁。
// - 线程B也发现 instance == null,也准备进入同步块等待抢锁。

// 【第二阶段:尝试获取锁】
// - 线程A抢到 synchronized(SingletonLazy.class) 的锁,进入同步块,开始执行内层代码。
// - 线程B未抢到锁,必须等待线程A释放锁,挂起等待。

// 【第三阶段:内层 if 判断】
// - 线程A在内层再次检查 instance 是否为 null,
//   如果确实是 null,就创建 SingletonLazy 实例。
// - 线程A释放锁,线程B接着抢到锁。

// 【第四阶段:线程B再次检查】
// - 线程B进入同步块,内层 if 判断时,发现 instance 已经不是 null,
//   所以不会再创建新对象,直接返回已存在的实例。

// 【总结】
// 这样写的双重检查机制,既保证了线程安全,
// 又避免每次都去加锁,提升了性能!


    // 辅助方法,观察类是否加载
    static {
        System.out.println("SingletonLazy 类已加载!");
    }

    public static void printf() {
        System.out.println("调用了静态方法 printf()");
    }
}

class Test {
    public static void main(String[] args) {
        // 不调用 getInstance 只调用静态方法
        SingletonLazy.printf();  // 会触发类加载,但不会创建对象!

        System.out.println("---------------");

        // 真正调用 getInstance,才会创建对象
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
    }
}

结尾

写到这里的时候,大约花费了笔者120分钟,写了8145个字

本来笔者想接着介绍阻塞队列的,看来只能留到下次了!

笔者的风格是每一步都会写的很详细,因为笔者觉得自己天赋不佳,需要在学会的时候记录的越详细越好,方便读者查阅和调用

希望笔者如此之高质量的博客能帮助到你我他!

相关推荐
天天扭码10 分钟前
一分钟解决 | 高频面试算法题——滑动窗口最大值(单调队列)
前端·算法·面试
hope_wisdom16 分钟前
实战设计模式之备忘录模式
设计模式·系统架构·软件工程·备忘录模式·架构设计
xxy!19 分钟前
Spring 框架中用到的设计模式
java·spring·设计模式
东阳马生架构22 分钟前
Sentinel源码—7.参数限流和注解的实现二
java·sentinel
Klong.k25 分钟前
什么是 Stream
java·stream
曾经的三心草26 分钟前
博客系统-邮件发送-nginx-服务部署
java·数据库·nginx·邮件发送·服务部署
Java中文社群33 分钟前
聊聊SpringAI流式输出的底层实现?
java·人工智能·后端
心走36 分钟前
八股文中TCP三次握手怎么具象理解?
前端·面试
Leaf吧44 分钟前
java 设计模式 原型模式
java·设计模式·原型模式
super凹凸曼1 小时前
分享一个把你的API快速升级为MCP规范的方案,可在线体验
java·后端·开源