atwood-pan

 

设计模式-创建型-单例模式

单例模式

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。

单例模式是创建型模式,Spring框架中的ApplicationContext、J2EE中的ServletContext和ServletContextConfig、数据库的连接池都是单例模式

1、饿汉单例模式

饿汉单例模式在类加载的时候就立即初始化,并且创建单例对象。

绝对的线程安全,在线程还没有出现以前就实例化了,不可能存在访问安全问题。

  • 优点:没有任何加锁操作、执行效率比较高,用户体验比懒汉单例模式更好

  • 缺点:类加载的时候就初始化,不管用不用都占空间,浪费内存

1.1、饿汉单例案例

经典案例:

/**
 * TODO 懒汉单例模式
 *  饿汉单例模式在类加载的时候就立即初始化,并且创建单利对象
 *  绝对线程安全
 * @author ss_419
 * @version 1.0
 * @date 2023/9/1 09:10
 */
public class HungrySingleton {
    // 先静态,后动态
    // 先属性,后方法
    // 先上后下

    private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

    private HungrySingleton() {}
    // 通过getInstance返回对象实例
    public static HungrySingleton getInstance(){
        return HUNGRY_SINGLETON;
    }
}

改进写法,使用静态代码块机制:

/**
 * TODO 静态饿汉单例模式
 * 利用静态代码块的机制
 * 饿汉单例模式适用于单例对象较少的情况
 * @author ss_419
 * @version 1.0
 * @date 2023/9/1 09:16
 */
public class HungryStaticSingleton {
    private static final HungryStaticSingleton INSTANCE ;

    /**
     * 通过静态代码块对类进行实例化
     */
    static {
        // 静态代码块,在初始化之前的连接阶段中的准备阶段就对静态变量分配内存、设置初始值等操作
        INSTANCE = new HungryStaticSingleton();
    }

    /**
     * 不能通过构造器对类进行实例化操作
     */
    private HungryStaticSingleton() {
    }
    // 提供一个全局访问点,以供实例化对象
    public static HungryStaticSingleton getInstance() {
        return INSTANCE;
    }
}

2、懒汉单例模式

特点:被外部类调用的时候内部类才会加载

2.1、懒汉单例案例

懒汉单例模式在外部需要使用的时候才进行实例化:

/**
 * TODO 懒汉单例模式
 * 懒汉单例模式:在外部需要使用的时候才进行实例化
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/9/1 09:20
 */
public class LazySimpleSingleton {
    private LazySimpleSingleton() {

    }

    // 静态块,公共内存区域
    private static LazySimpleSingleton lazy = null;

    // 提供全局访问点,实例化单例对象
    public static LazySimpleSingleton getInstance() {
        if (lazy == null){
            // 对象没有创建的时候才new,否则就返回之前所存在的对象
            return new LazySimpleSingleton();
        }
        return lazy;
    }
}

创建一个线程类ExectorThread:

public class ExectorThread implements Runnable{
    @Override
    public void run() {
        // 通过懒汉单例中提供的全局访问点创建一个单例对象
        LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
        // 查看线程中存在的对象
        System.out.println(Thread.currentThread().getName() + ": " + singleton);
    }
}

测试代码:

    @Test
    public void test1(){
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        // 这里测试,出现两个类不同的情况,单例存在线程安全 问题
        t1.start();
        t2.start();

        System.out.println("End");
    }

通过测试,上面的代码存在线程安全问题,怎么样才能使线程安全呢?

第一个方法:通过对获得实例方法进行加锁操作,保证当前线程获得实例时,其他线程都处于阻塞状态

 package org.pp.my.design_pattern.create.singleton2.lazy;

/**
 * TODO 懒汉单例模式
 * 懒汉单例模式:在外部需要使用的时候才进行实例化
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/9/1 09:20
 */
public class LazySimpleSingleton {
    private LazySimpleSingleton() {

    }

    // 静态块,公共内存区域
    private static LazySimpleSingleton lazy = null;

    /**
     * 为了保证懒汉单例模式在多线程环境下线程安全,通过synchronized加锁实现
     * @return
     */
    public synchronized static LazySimpleSingleton getInstance() {
        if (lazy == null){
            // 对象没有创建的时候才new,否则就返回之前所存在的对象
            return new LazySimpleSingleton();
        }
        return lazy;
    }
}

但是通过synchronized加锁的方式,会使性能降低

使用双重锁既能兼顾线程安全又能提升程序性能:

/**
 * TODO 懒汉模式双重检查锁
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/9/1 09:34
 */
public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazy = null;

    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance() {
        if (lazy == null){
            synchronized (LazyDoubleCheckSingleton.class){
                lazy = new  LazyDoubleCheckSingleton();
                // 1、分配内存给这个对象
                // 2、初始化对象
                // 3、设置lazy指向刚分配的内存地址
            }
        }
        return lazy;
    }

}

采用静态内部类方式,避免加锁操作

/**
 * TODO 懒汉模式-静态内部类
 * 通过静态内部类来避免使用synchronized加锁的情况
 * @author ss_419
 * @version 1.0
 * @date 2023/9/1 09:39
 */
public class LazyInnerClassSingleton {
    // 使用LazyInnerClassSingleton,默认会先初始化内部类
    // 如果没使用,则内部类是不加载的
    private LazyInnerClassSingleton(){

    }
    // 每一个关键字都不是多余的,static是为了使单例的空间共享,保证这个方法不会被重写、重载
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.LAZY;
    }

    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

2.2、反射破坏单例

通过反射机制,获取类的私有构造方法,强制访问

    /**
     * 通过反射来破坏单例
     */
    @Test
    public void test2(){
        try {
            // 反射获取单例类
            Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
            // 通过反射获取私有的构造方法
            Constructor<LazyInnerClassSingleton> c = clazz.getDeclaredConstructor(null);
            // 强制访问
            c.setAccessible(true);
            // 暴力初始化
            Object o1 = c.newInstance();
            // 调用了两次构造方法,相当于“new”了两次,犯了原则性错误
            Object o2 = c.newInstance();
            System.out.println("o1 = " + o1);
            System.out.println("o2 = " + o2);
            System.out.println(o1 == o2);
        }catch (Exception e){

        }
    }

为了避免上述情况发生,在构造器中做一些操作,一旦出现多次重复创建,则直接抛出异常

/**
 * TODO 史上最强的懒汉单例模式
 * 一旦出现多次重复创建,则直接抛出异常
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/9/1 09:51
 */
public class LazyInnerClassNoNullSingleton {
    // 使用LazyInnerClassGeneral的时候,默认会先初始化内部类
    // 如果没有使用,则内部类是不加载的
    private LazyInnerClassNoNullSingleton(){
        if (LazyHolder.LAZY != null){
            // 不为null,说明对象已经实例化过了
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    /**
     * 每一个关键字都不是多余的
     * static是为了单例的空间共享,保证这个方法不会被重写、重载
     * @return
     */
    public static LazyInnerClassNoNullSingleton getInstance(){
        // 在返回之前,一定会先加载内部类
        return LazyHolder.LAZY;
    }

    /**
     * 默认不加载
     */
    private static class LazyHolder {
        private static final LazyInnerClassNoNullSingleton LAZY = new LazyInnerClassNoNullSingleton();
    }
}

posted on 2023-09-01 13:39  JavaCoderPan  阅读(8)  评论(0编辑  收藏  举报

导航