多线程编程的简单案例——单例模式[多线程编程篇(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个字

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

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

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

相关推荐
丶小鱼丶5 分钟前
并发编程之【优雅地结束线程的执行】
java
市场部需要一个软件开发岗位9 分钟前
JAVA开发常见安全问题:Cookie 中明文存储用户名、密码
android·java·安全
忆~遂愿13 分钟前
GE 引擎进阶:依赖图的原子性管理与异构算子协作调度
java·开发语言·人工智能
MZ_ZXD00118 分钟前
springboot旅游信息管理系统-计算机毕业设计源码21675
java·c++·vue.js·spring boot·python·django·php
PP东20 分钟前
Flowable学习(二)——Flowable概念学习
java·后端·学习·flowable
ManThink Technology26 分钟前
如何使用EBHelper 简化EdgeBus的代码编写?
java·前端·网络
invicinble30 分钟前
springboot的核心实现机制原理
java·spring boot·后端
人道领域38 分钟前
SSM框架从入门到入土(AOP面向切面编程)
java·开发语言
大模型玩家七七1 小时前
梯度累积真的省显存吗?它换走的是什么成本
java·javascript·数据库·人工智能·深度学习
CodeToGym1 小时前
【Java 办公自动化】Apache POI 入门:手把手教你实现 Excel 导入与导出
java·apache·excel