多线程应用——单例模式

简介: 多线程应用——单例模式

单例模式

文章目录

一.什么是单例模式

单例模式(Singleton Pattern)顾名思义,在程序中一个类只有一个对象实例。例如我们在JDBC编程中,我们创建了一个简单类DataSource,只要从DataSource中获取数据库连接即可,不用创建多个DataSource对象。

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。

二.如何实现

1.口头实现

2.利用语法特性

  • 本质上就是利用编程语言自身的特性,强行限制某个类不能创建多个实例
  • static修饰一个变量后,这个变量就从一个普通的成员变量属性变成了类对象的成员变量
  • 在JVM中一个类只要一个类对象,从而保证了static变量的唯一性

三.实现方式(饿汉式+懒汉式)

1.饿汉式

public class SingletonHungry{
    //类的成员变量
    private static Singleton instance=new Singleton();
    //私有化构造方法
    private SingletonHungry(){ }
    /**
     * 对外获取类成员方法
     * @return
     */
    public static SingletonHungry getInstance(){
        return instance;
    }
}

饿汉式:需要急迫的创建这个实例,类在加载的过程中就创建出来了

描述:这种方式比较常见,但容易产生垃圾对象

  • 优点:没有加锁,执行效率高
  • 缺点:类加载时就初始化,浪费内存

2.懒汉式

public class SingletonLazy{
    //类的成员变量
    private static Singleton instance=null;
    //私有化构造方法
    private Singleton(){ }
    /**
     * 对外获取类成员方法
     * @return
     */
    public static Singleton getInstance(){
        //判断一个需要返回的对象是否为空
        if (instance==null){
            //创建对象
            instance=new SingletonLazy();
        }
        //返回单例对象
        return instance;
    }
}

懒汉式:什么时候用什么时候才去创建,不要程序启动的时候创建,从而节省了程序启动时的开销

3.线程安全的单例模式

在多线程中,饿汉式只是获取变量而不是修改变量;而懒汉式是修改共享变量,因此存在线程安全问题。

我们用上面的代码做一测试

public class Demo_SingletonLazy {
    public static void main(String[] args) {
        //多个线程获取单例对象
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                SingletonLazy instance = SingletonLazy.getInstance();
                System.out.println(instance);
            });
            thread.start();
        }
    }
}

3fbccea7190af98b1400b70eb05a60bd.png

我们知道造成线程安全问题的原因有 原子性、内存可见性、有序性

7f6a0075cbe11d80b77843a2607b50a2.png

通过上图分析得出问题:不满足原子性,那该如何解决呢,当然是加锁。

public class SingletonLazy{
    //类的成员变量
    private static Singleton instance=null;
    //私有化构造方法
    private Singleton(){ }
    /**
     * 对外获取类成员方法
     * @return
     */
    public static Singleton getInstance(){
        synchronized(SingletonLazy.class){
            //判断一个需要返回的对象是否为空
          if (instance==null){
              //创建对象
              instance=new SingletonLazy();
          }
        }
        //返回单例对象
        return instance;
    }
}

536de8347f48fc41781264f8c8842820.png加锁之后,我们看到问题也解决了,但此时还有一个非常严重的问题:效率问题


  1. 当变量没有初始化时,第一次创建可能会出现线程问题,因为多个线程可能创建实例
  2. 当实例变量被创建后,new操作将永远不会执行了,因为获取到的实例不为null了
  3. 那么synchronized的锁就没有必要加了,因为实例已经创建好了,之后线程拿到锁之后只是判断一下实例是否为空,不会去new了,如果不为null就什么也不干就把锁释放了,这样一来锁白加了,资源也白白浪费了
  4. synchronizeed看上去是一个关键字,可能会涉及到用户态–>内核态之间的切换,这个成本是比较高的,我们为了保证程序正确执行的基础可以承担这个成本,但是没有必要做无用的消耗

4.双重检查锁

既然在第一次创建完实例后加锁是为了判断实例是否为空,那么不如将判断为空放到加锁之前,避免因为上述原因而造成资源浪费

public class SingletonDCL {
    //定义一个类的成员变量
    private static SingletonDCL instance=null;
    private SingletonDCL(){}
    public static SingletonDCL getInstance(){
        //第一层判断是否需要加锁
        if (instance==null){
            synchronized (SingletonDCL.class){
                //第二层加锁判断是否需要创建对象
                if (instance==null){
                    //创建对象
                    instance=new SingletonDCL();
                }
            }
        }
        //返回单例对象
        return instance;
    }
}

5.禁止指令重排序

上述代码还存在一个严重问题,那就是指令重排序问题

假设一个线程在调用getInstande()方法时,拿到了锁,进入了第二层开始new对象:

new对象本质分为三步:

  1. 申请内存空间
  2. 调用构造方法,初始化实例
  3. 把内存首地址赋给对象的引用

可以看出1和3有逻辑关系,2是在这个内存空间里填充数据

如果这里指令重排序,造成执行顺序为1 3 2 那么这个时候又有一个线程执行到第一层的判断,这里的instance就不为空了,返回一个没有完成初始化的对象。这种情况也是很危险的

为了防止指令重排序,给变量加入关键字volatile

public class SingletonDCL {
    //定义一个类的成员变量
    private static volatile SingletonDCL instance=null;//禁止指令重排序,也保证了在对共享变量修改时的内存可见性
    private SingletonDCL(){}
    public static SingletonDCL getInstance(){
        //第一层判断是否需要加锁
        if (instance==null){
            synchronized (SingletonDCL.class){
                //第二层加锁判断是否需要创建对象
                if (instance==null){
                    //创建对象
                    instance=new SingletonDCL();
                }
            }
        }
        //返回单例对象
        return instance;
    }
}


目录
相关文章
|
26天前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
1月前
|
监控 Java
在实际应用中选择线程异常捕获方法的考量
【10月更文挑战第15天】选择最适合的线程异常捕获方法需要综合考虑多种因素。没有一种方法是绝对最优的,需要根据具体情况进行权衡和选择。在实际应用中,还需要不断地实践和总结经验,以提高异常处理的效果和程序的稳定性。
21 3
|
1月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
47 4
|
1月前
|
数据采集 存储 Java
Crawler4j在多线程网页抓取中的应用
Crawler4j在多线程网页抓取中的应用
|
1月前
|
数据挖掘 程序员 调度
探索Python的并发编程:线程与进程的实战应用
【10月更文挑战第4天】 本文深入探讨了Python中实现并发编程的两种主要方式——线程和进程,通过对比分析它们的特点、适用场景以及在实际编程中的应用,为读者提供清晰的指导。同时,文章还介绍了一些高级并发模型如协程,并给出了性能优化的建议。
31 3
|
1月前
|
Java 数据处理 数据库
Java多线程的理解和应用场景
Java多线程的理解和应用场景
52 1
|
23天前
|
Java 开发者
Java中的多线程基础与应用
【10月更文挑战第24天】在Java的世界中,多线程是提高效率和实现并发处理的关键。本文将深入浅出地介绍如何在Java中创建和管理多线程,以及如何通过同步机制确保数据的安全性。我们将一起探索线程生命周期的奥秘,并通过实例学习如何优化多线程的性能。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开一扇通往高效编程的大门。
17 0
|
2月前
|
安全 Java 关系型数据库
单例模式下引发的线程安全问题
单例模式确保类在进程中仅有一个实例,适用于如数据库连接等场景。分为饿汉式与懒汉式:饿汉式在类加载时创建实例,简单但可能浪费资源;懒汉式延迟创建实例,需注意线程安全问题,常采用双重检查锁定(Double-Checked Locking)模式,并使用 `volatile` 关键字避免指令重排序导致的问题。
61 2
单例模式下引发的线程安全问题
|
2月前
|
负载均衡 Java 调度
探索Python的并发编程:线程与进程的比较与应用
本文旨在深入探讨Python中的并发编程,重点比较线程与进程的异同、适用场景及实现方法。通过分析GIL对线程并发的影响,以及进程间通信的成本,我们将揭示何时选择线程或进程更为合理。同时,文章将提供实用的代码示例,帮助读者更好地理解并运用这些概念,以提升多任务处理的效率和性能。
60 3
|
2月前
|
Java 开发者
Java中的多线程基础与应用
【9月更文挑战第22天】在Java的世界中,多线程是一块基石,它支撑着现代并发编程的大厦。本文将深入浅出地介绍Java中多线程的基本概念、创建方法以及常见的应用场景,帮助读者理解并掌握这一核心技术。