Java多线程案例——单例模式

简介: 设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.

1. 单例模式概述


啥是设计模式?


设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有

一些固定的套路. 按照套路来走局势就不会吃亏.


软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照

这个套路来实现代码, 也不会吃亏.


单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.


这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个.


单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.


饿汉模式:在类加载阶段就把实例创建出来

懒汉模式:通过getInstance方法来获取到实例,首次调用该方法的时候,才真正创建实例。

2.单例模式实现


2.1 饿汉模式


在类加载阶段就把实例创建出来


static class Singleton {
    //把构造方法设为private,防止在类外面调用构造方法,也就禁止了调用者在其他地方创建实例的机会
     private Singleton() {
     }
     private static Singleton instance=new Singleton();
     public static Singleton getInstance() {
          return instance;
     }
}


如果多个线程调用getInstance,是否出现问题呢?


此处认为是线程安全的,因为是多个线程读(只有return instance),不涉及修改。


2.2 懒汉模式-单线程版


类加载的时候不创建实例. 第一次使用的时候才创建实例


static class Singleton {
        private Singleton() {}
        private static Singleton instance=null;
        public static Singleton getInstance() {
            if(instance==null) {
                instance=new Singleton();
            }
            return instance;
        }
}


2.3 懒汉模式-多线程版


在上述2.2的代码示例中,如果实例已经创建完毕,后续再调用getInstance,此时不涉及修改操作,是线程安全的。


但是如果实例尚未创建,此时就可能会涉及到修改(分为两步 LOAD和SAVE),如果确实存在多个线程同时修改,就会涉及到线程安全问题。


如何解决这里的问题呢?

加锁


微信图片_20230110210426.png

注意!!!

这是一种典型的错误写法,此处线程不安全主要是因为if操作和=操作不是原子的,我们加锁的目的是让代码具有原子性,就应该使用synchronized把这两个操作包裹上。


微信图片_20230110210423.png

注意!!!


如果这样加锁,确实解决了线程安全问题,但是也引入了新问题,getInstance是首次调用的时候才涉及线程安全问题,在后续调用时就不涉及了(后续不存在修改操作了),所以如果我们按照刚才这个加锁方法,不仅仅时首次调用,包括后续的调用也会涉及到加锁操作


后续本来没有线程安全问题,不需要加锁,如果加锁便是多此一举,

加锁操作本身是一个开销比较大的操作,在不该加锁的地方加锁了,可能会让这个代码的速度降低很多倍。


2.4 懒汉模式-多线程版(改进)

为了解决2.3中提出的问题,以下代码在加锁的基础上, 做出了进一步改动:


使用双重 if 判定, 降低锁竞争的频率.

给 instance 加上了 volatile.

class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
           if (instance == null) {
               instance = new Singleton();
               }
           }
       }
        return instance;
   }
}


理解双重 if 判定


加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候.


因此后续使用的时候, 不必再进行加锁了. 外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了,如果已经创建出来了实例,就不必再加锁了,节省了开销。而里层的if判断是否要创建实例。


看起来两个if之隔了一行代码,但中间隔得是加锁操作,就很有可能隔得是“沧海桑田”(加锁有可能产生竞争,竞争就会导致阻塞,阻塞什么时候截止就不确定了)


理解volatile

同时为了避免 “内存可见性” 导致读取的 instance 出现偏差(后续批次的线程通过第一层if的时候,也需要判定instance的值,但是这个判定不一定是从内存读的数据,也可能是从寄存器读的数据), 于是补充上 volatile .


下边我们通过一个一个例子来更好的理解:

当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁,

其中竞争成功的线程, 再完成创建实例的操作.

当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.


1.有三个线程, 开始执行 getInstance , 通过外层的 if (instance == null) 知道了实例还没有创建的消息. 于是开始竞争同一把锁


微信图片_20230110210416.png

2.其中线程1 率先获取到锁, 此时线程1 通过里层的 if (instance == null) 进一步确认实例是否已经创建. 如果没创建, 就把这个实例创建出来.


微信图片_20230110210413.png

3.当线程1 释放锁之后, 线程2 和 线程3 也拿到锁, 也通过里层的 if (instance == null) 来确认实例是否已经创建, 发现实例已经创建出来了, 就不再创建了.


微信图片_20230110210410.png

4.后续的线程, 不必加锁, 直接就通过外层 if (instance == null) 就知道实例已经创建了, 从而不再尝试获取锁了. 降低了开销.

微信图片_20230110210406.png

相关文章
|
4天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
6天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
6天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
6天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
22 3
|
4天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
6天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
21 1
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
60 1
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
35 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
25 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
41 2