超详细单例模式讲解(建议收藏)

概念

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。并提供一个全局访问点供其他类获取并使用这个对象

单例模式的类型

单例模式有两种类型:

  • 懒汉式:在真正需要使用对象时才去创建该单例类对象
  • 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用

单例模式有技术三要素:

  • 单例模式类内部维护一个私有的、静态的、volitate、fianl的实例对象的实例或引用。如何是饿汉式就持有一个单实例的对象,如果是懒汉模式就是持有它的引用,但最终实例化单例对象的过程依然是在本类中
  • 构造器是私有的,确保无法在其它类中被实例化
  • 提供一个get方法将这个对象对外暴漏,但要注意确保这个方法是线程安全且高性能的

单例模式的应用

存在需要全局唯一实例的场景,例如

  • 数据库连接池,数据库连接池被设计为预先创建并维护一定数量的数据库连接,以便可以快速地重用这些连接,将数据库连接池设计为单例可以确保整个应用程序中只有一个全局的连接池实例,这样可以高效地管理和复用数据库连接。
  • 线程池管理着一组工作线程,通常会实现为单例模式,以便在应用程序中统一调度和管理线程资源。

案例代码

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolSingleton {
    // 使用volatile关键字来确保多线程环境下的线程安全
    private static volatile ThreadPoolSingleton instance = null;
    private ExecutorService executorService;

    // 私有构造方法,防止外部直接创建实例
    private ThreadPoolSingleton() {
        // 创建具有固定线程数的线程池
        executorService = Executors.newFixedThreadPool(10);
    }

    // 提供全局访问点
    public static ThreadPoolSingleton getInstance() {
        if (instance == null) {
            synchronized (ThreadPoolSingleton.class) {
                // 双重检查锁定,以避免多线程下的问题
                if (instance == null) {
                    instance = new ThreadPoolSingleton();
                }
            }
        }
        return instance;
    }

    // 提供一个方法来执行任务
    public void execute(Runnable task) {
        executorService.execute(task);
    }

    // 关闭线程池的方法
    public void shutdown() {
        if (executorService != null && !executorService.isShutdown()) {
            executorService.shutdown();
        }
    }
}

// 使用线程池的例子
public class ThreadPoolExample {
    public static void main(String[] args) {
        // 获取线程池的单例实例
        ThreadPoolSingleton threadPool = ThreadPoolSingleton.getInstance();

        // 提交任务给线程池
        for (int i = 0; i < 20; i++) {
            int taskId = i;
            threadPool.execute(() -> {
                System.out.println("Executing task " + taskId + " in thread: " + Thread.currentThread().getName());
                // 模拟任务执行时间
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 执行完任务后关闭线程池
        threadPool.shutdown();
    }
}

单例模式

饿汉一模式

/*
饿汉式
优点:①写法简单②线程安全的
缺点:还没被调用就对象就被加载了,不够节约
 */
public class Singleton {
    public final static Singleton dataSources = new Singleton();
    //此处采取一些措施只能增加使用反射进行破坏的难度而无法根除反射的安全性问题
    private Singleton(){

    }
    public static Singleton getInstance(){
        return dataSources;
    }


    /*
    测试方法用于证明饿汉单例模式下fianl修饰单例对象的作用
    注意必须是饿汉式,懒汉式不考虑使用fianl
    以下的测试值是在没有使用final的情况下的值
     */
    public static void main(String[] args) throws Exception {
        //反射情况一:以下种情况(通过反射获取了单例模式的构造器并授予其操作权限)很难处理,单例模式必然被破坏
        System.out.println("第一次拿到单例模式的对象"+Singleton.getInstance());//com.wzh.atcrowdfunding.entity.project.Singleton@266474c2
        Class clazz = Singleton.class;
        Constructor constructor = clazz.getDeclaredConstructor(); //私有构造器获取需要用这个方法
        //授予constructor可以操作私有构造器用于创建对象的权限
        constructor.setAccessible(true);
        Object unIllegalSingletonInstance = constructor.newInstance();

        System.out.println("第二次通过反射拿到的非法单例模式的对象"+unIllegalSingletonInstance);//com.wzh.atcrowdfunding.entity.project.Singleton@6f94fa3e

        //反射情况二:在情况一的基础上继续破坏单例结构,就是在获取了额外实例的同时将单例类原有的单例属性替换为上面通过反射new出来的不合法对象
        //为防止情况二的进一步破坏,可以通过为属性加fianl来解决
        Field[] declaredField = clazz.getDeclaredFields();
        for (Field field : declaredField) {
            System.out.println("拿到原有的单例类的单例对象属性" +field.get(Singleton.getInstance()));//com.wzh.atcrowdfunding.entity.project.Singleton@266474c2
            //修改原有的单例类的单例对象属性为上面通过反射获取的非法单例对象值
            field.set(dataSources,unIllegalSingletonInstance);
            System.out.println("被彻底破坏的单例类的单例对象属性" +field.get(Singleton.getInstance()));//om.wzh.atcrowdfunding.entity.project.Singleton@6f94fa3e
        }
    }
}

懒汉模式(线程不安全)

//标准的懒汉式,也是不安全的(包含测试方法,可以证明线程不安全)
public class Singleton {
    public static Singleton dataSources;
    //此处采取一些措施只能增加使用反射进行破坏的难度而无法根除反射的安全性问题
    private Singleton(){

    }
    public static final Singleton getInstance(){
        if(dataSources == null){
            try {
            //休眠1000毫秒模拟线程问题,模拟创建比较耗时的大型资源,那么在下面main方法中100%复现线程安全问题
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            dataSources = new Singleton();
            return dataSources;
        }
        return dataSources;
    }

    //线程安全测试
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Singleton> singletonCallable = new Callable<Singleton>() {
            @Override
            public Singleton call() throws Exception {
                return Singleton.getInstance();
            }
        };

        ExecutorService service = Executors.newFixedThreadPool(2);
        Future<Singleton> f1 = service.submit(singletonCallable);
        Future<Singleton> f2 = service.submit(singletonCallable);

        Singleton singleton1 = f1.get();
        Singleton singleton2 = f2.get();

        System.out.println(singleton1);
        System.out.println(singleton2);
        System.out.println(singleton1 == singleton2);
    }

}

DCL线程安全的懒汉模式(重点)

如果把synchronized加到getDateaSource方法中,那么多线程并发调用n次该方法只有,一次null的情况,而只对这一种情况使得n次调用线程全部要同步,程序运行效率自然会很低,所以我们可以通过只对创建单实例对象的代码加锁,即使用synchronized代码块的形式,在单例资源singleton不为null时再调用同步代码块,但高并发环境下,可能一瞬间 多个线程涌入了if(singleton == null)代码块中,然后这些线程按序去执行singleton = new Singleton(); 此时会创建多个singleton单实例对象,singleton引用最终只会指向最新创建的singleton单实例对象,其它的对象在下次GC过程中会被回收,这种情况破坏了单例原则,且会影响系统业务逻辑,出现并发问题

public class Singleton {
    public static volatile Singleton singleton;


    private Singleton() {
        System.out.println("只能内部构建大型此类资源对象");
    }


    private static final Singleton getInstance() {
        //这里假设一瞬间又涌入了if块10个线程就好理解了
        if (singleton == null) {
            //休眠1000毫秒模拟线程问题,模拟创建比较耗时的大型资源,那么在下面main方法中100%复现线程安全问题
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                    return singleton;
                }
                return singleton;
            }
        }
        return singleton;
    }

    //线程安全测试
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Singleton> singletonCallable = new Callable<Singleton>() {
            @Override
            public Singleton call() throws Exception {
                return Singleton.getInstance();
            }
        };

        ExecutorService service = Executors.newFixedThreadPool(2);
        Future<Singleton> f1 = service.submit(singletonCallable);
        Future<Singleton> f2 = service.submit(singletonCallable);

       Singleton singleton1 = f1.get();
       Singleton singleton2 = f2.get();

        System.out.println(singleton1);
        System.out.println(singleton2);
        System.out.println(singleton1 == singleton2);

    }
}

懒汉模式 - 静态类实现

//标准懒汉式单例模式(静态内部类完成单例模式)
//原理:类装载时内部类(静态内部类)不受影响(这是JVM的内部类的加载延迟机制)但是当调用静态内部类的属性或方法时才会
//加载且只加载一次,这样的机制是无视于线程的(类加载期间不考虑线程),且还能实现懒加载;
class InnerDataSources {
    public static volatile InnerDataSources dataSources;
   
    //此处采取一些措施只能增加使用反射进行破坏的难度而无法根除反射的安全性问题
    private InnerDataSources(){
    }
    //写一个内部具有InnerDataSources实例的对象
    private static class InnerDSInstance{
        private static final InnerDataSources IDS = new InnerDataSources();
    }
    public static InnerDataSources getDateSource(){
        return InnerDSInstance.IDS;
    }
}

懒汉模式 - 枚举实现

//枚举实现单例模式
enum SingleDemo{
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

DCL方案存在的线程安全问题

标准的DCL代码

public class Singleton {
    
    private static Singleton singleton;

    private long id = 24082100184461750l;
    
    private Singleton(){}
    
    public static Singleton getInstance() {
        if (singleton == null) { 
            synchronized(Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
}

以上代码从代码层面来看确实已经看不出问题了,但从字节码层面来看

  public static com.example.democommon.linklog.test.Singleton getInstance();
    Code:
       0: getstatic     #7                  // Field singleton:Lcom/example/democommon/linklog/test/Singleton;
       3: ifnonnull     37
       6: ldc           #8                  // class com/example/democommon/linklog/test/Singleton
       8: dup
       9: astore_0
      10: monitorenter
      11: getstatic     #7                  // Field singleton:Lcom/example/democommon/linklog/test/Singleton;
      14: ifnonnull     27
      17: new           #8                  // class com/example/democommon/linklog/test/Singleton
      20: dup
      21: invokespecial #13                 // Method "<init>":()V
      24: putstatic     #7                  // Field singleton:Lcom/example/democommon/linklog/test/Singleton;
      27: aload_0
      28: monitorexit
      29: goto          37
      32: astore_1
      33: aload_0
      34: monitorexit
      35: aload_1
      36: athrow
      37: getstatic     #7                  // Field singleton:Lcom/example/democommon/linklog/test/Singleton;
      40: areturn

问题的根源在于singleton = new Singleton();这行代码并非原子性的,从字节码层面,这一行代码内容为

      17: new           #8                  // class com/example/democommon/linklog/test/Singleton
      20: dup
      21: invokespecial #13                 // Method "<init>":()V
      24: putstatic     #7                  // Field 

如果小伙伴们想继续往下学习,且存在知识盲区的话,需要话参考我这篇文章哈:java对象创建的过程

new命令是判断目标类是否已加载、为对象分配内存空间、为对象赋默认值、初始化对象头,invokespecial的作用是显示初始化对象,执行对象的目标构造器,这里的引用singleton默认是null,正常情况下是对象创建且初始化完毕后,才会被引用所指向,引用指向对象后,那么引用的值就是对象的地址值,但这里如果是先执行的putstatic,此时还未执行的invokespecial,对象没有被初始化,如果这时候有一个线程通过调用getInstance方法,此时会直接获取到未初始化的单实例对象,比如这里的一个属性id = 24082100184461750l,只有在执行invokespecial做
显性赋值的时候才会被辅助24082100184461750l,而此时这个线程获取到的是未被显性赋值的对象,那么这个单实例所对应的属性id就是0l, 造成线程安全问题

解决方案 - 禁止指令重排

使用关键字volitate加在singleton上

 private static volitate Singleton singleton;
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值