【设计模式】周末在家肝了一天,终于写完这篇单例模式,看完还不会你打我~

饿汉式

单例模式的通用写法,一般均指饿汉式。该写法在类加载的时候会立即初始化,并且创建单例对象。之所以说它线程安全,是因为在线程还未run起来之前就实例化了,不存在访问安全问题。

//饿汉式静态代码块单例模式
public class HungryStaticSingleton {
    private static final HungryStaticSingleton instance = new HungryStaticSingleton();

    private HungryStaticSingleton(){}

    public static HungryStaticSingleton getInstance(){
        return  instance;
    }
}

饿汉式静态代码块写法:

//饿汉式静态代码块单例模式
public class HungryStaticSingleton {
    private static final HungryStaticSingleton instance;
    
    static {
        instance = new HungryStaticSingleton();
    }

    private HungryStaticSingleton(){}

    public static HungryStaticSingleton getInstance(){
        return  instance;
    }
}

这种写法使用了静态代码块机制。饿汉式适用于单例对象比较少的情况,可以保证绝对的线程安全,执行效率比较高。

其缺点也很明显,所有对象类在加载时候就会实例化。如果系统中有大批量的单例对象存在,而且单例对象的数量不确定的情况下,当系统初始化时会有大量的内存浪费。无论对象会不会使用到,都占用了一定的内存空间,造成系统资源的浪费。

懒汉式

懒汉式是为了解决饿汉式会带来的内存浪费问题。这种写法在对象被使用时才会被初始化。

//懒汉式单例模式在外部需要使用的时候才进行实例化
public class LazySimpleSingletion {
    //静态块,公共内存区域
    private static LazySimpleSingletion instance;

    private LazySimpleSingletion() {
    }

    public static LazySimpleSingletion getInstance() {
        // 判断instance是否为空
        if (instance == null) {
            instance = new LazySimpleSingletion();
        }
        return instance;
    }
}

这种写法在多线程环境中会产生线程安全的问题吗?


可以看出,会存在一定的概率出现两种不同的结果,有可能两个线程获取到的对象是一致的,也有可能是不一致的。

因此,上面的单例会存在线程安全问题。

假设两个线程在同一时间同时进入getInstance() 方法,那么就会同时满足 if (null == instance) 条件,创建两个对象。

如果两个线程都继续往下执行,有可能后执行的线程的结果会覆盖先执行的线程的结果。

在这里插入图片描述

解决以上问题,仅需在getInstance() 方法前加把同步锁 synchronize,使这个方法变为同步方法。

//懒汉式单例模式在外部需要使用的时候才进行实例化
public class LazySimpleSingletion {
    //静态块,公共内存区域
    private static LazySimpleSingletion instance;

    private LazySimpleSingletion() {
    }

    // 加上synchronize关键字,变为同步方法
    public synchronized static LazySimpleSingletion getInstance() {
        // 判断instance是否为空
        if (instance == null) {
            instance = new LazySimpleSingletion();
        }
        return instance;
    }
}

如果线程数量骤增,懒汉式是否还适用?为什么?

当线程数量在短时间内剧增,使用synchronize加锁会导致线程阻塞,使程序性能下降。

举例:

如图所示,餐厅有5个分餐口,但是入口只有1条通道,这样的话会造成大量的堵塞,降低了用户体验。

在这里插入图片描述

是否有比上面更好的方案呢?

双重检查锁单例

在这里插入图片描述

如上图所示,将人群分开排队,进入餐厅后仍保持分流,如此一来效率会提升许多。

LazySimpleSingletion 进行改造,得到 LazyDoubleCheckSingleton1 ,代码如下:

public class LazyDoubleCheckSingleton1 {
    private volatile static LazyDoubleCheckSingleton1 instance;

    private LazyDoubleCheckSingleton1() {
    }

    // 参照 LazySimpleSingletion
    public static LazyDoubleCheckSingleton1 getInstance() {
        synchronized (LazyDoubleCheckSingleton1.class) {
            if (instance == null) {
                instance = new LazyDoubleCheckSingleton1();
            }
        }
        return instance;
    }
}

那么这种写法和没有加锁的 LazySimpleSingletion 并无差异,因此将 if 判断向上提一级,得到LazyDoubleCheckSingleton2

public class LazyDoubleCheckSingleton2 {
    private volatile static LazyDoubleCheckSingleton2 instance;

    private LazyDoubleCheckSingleton2() {
    }

    // 参照 LazySimpleSingletion
    public static LazyDoubleCheckSingleton2 getInstance() {
        if (instance == null) {
            synchronized (LazyDoubleCheckSingleton2.class) {
                instance = new LazyDoubleCheckSingleton2();
            }
        }
        return instance;
    }
}

LazyDoubleCheckSingleton2 进行调试发现,仍存在线程不安全问题


造成这种情况的原因在于,如果两个线程在同一时间都满足 if(null == instance) 条件,那么两个线程还是会执行 synchronize 中的代码,继续优化:

public class LazyDoubleCheckSingleton3 {
    private volatile static LazyDoubleCheckSingleton3 instance;

    private LazyDoubleCheckSingleton3() {
    }

    // 参照 LazySimpleSingletion
    public static LazyDoubleCheckSingleton3 getInstance() {
        if (null == instance) {
            // 检查是否要阻塞
            synchronized (LazyDoubleCheckSingleton3.class) {
                // 检查是否要重新创建实例
                if (null == instance) {
                    instance = new LazyDoubleCheckSingleton3();
                }
            }
        }
        return instance;
    }
}

调试如下:

在这里插入图片描述

虽然双重检查锁单例解决了线程的安全与性能问题。当使用到 synchronize 关键字时还需上锁,对程序的性能还是存在影响。

静态内部类单例

public class LazyStaticInnerClassSingleton1 {
    //使用 LazyStaticInnerClassSingleton1 的时候,默认会先初始化内部类
    //如果没使用,则内部类是不加载的
    private LazyStaticInnerClassSingleton1() {

    }

    // static是为了使单例的空间共享,保证这个方法不会被重写、重载
    private static LazyStaticInnerClassSingleton1 getInstance() {
        //在返回结果之前,一定会先加载内部类
        return LazyHolder.INSTANCE;
    }

    // 利用Java内部类的语法特点,默认不加载内部类
    private static class LazyHolder {
        private static final LazyStaticInnerClassSingleton1 INSTANCE = new LazyStaticInnerClassSingleton1();
    }
}

该方式即解决了饿汉式的内存资源浪费及 synchronize 加锁的性能问题,内部类一定是在方法调用之前初始化,避免了线程安全问题。

那么,是否可以使用反射来调用构造方法,再调用 getInstance() 方法实现上面的单例模式?

public static void main(String[] args) {
    try {
        // 使用反射进行破坏
        Class<?> clazz = LazyStaticInnerClassSingleton1.class;

        // 通过反射获取私有构造方法
        Constructor<?> c = clazz.getDeclaredConstructor(null);

        Object o1 = c.newInstance();

        Object o2 = c.newInstance();

        System.out.println(o1 == o2);

    } catch (Exception e) {
        e.printStackTrace();
    }
}

调用结果如下:

很显然,这种方式在内存中创建了两个不同的实例

public class LazyStaticInnerClassSingleton2 {
    //使用LazyStaticInnerClassSingleton2的时候,默认会先初始化内部类
    //如果没使用,则内部类是不加载的
    private LazyStaticInnerClassSingleton2() {
        if (LazyHolder.INSTANCE != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    // static是为了使单例的空间共享,保证这个方法不会被重写、重载
    private static LazyStaticInnerClassSingleton2 getInstance() {
        //在返回结果之前,一定会先加载内部类
        return LazyHolder.INSTANCE;
    }

    // 利用Java内部类的语法特点,默认不加载内部类
    private static class LazyHolder {
        private static final LazyStaticInnerClassSingleton2 INSTANCE = new LazyStaticInnerClassSingleton2();
    }
}

彩蛋:ThreadLocal单例模式

public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton =
            ThreadLocal.withInitial(ThreadLocalSingleton::new);

    // 等价于上面的写法
    /*private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton =
            new ThreadLocal<ThreadLocalSingleton>() {
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };*/

    private ThreadLocalSingleton() {
    }

    public static ThreadLocalSingleton getInstance() {
        return threadLocalSingleton.get();
    }

    public static void main(String[] args) {
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());

        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());

        t1.start();
        t2.start();

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

调用结果:

可以看到,无论在主线程中调用多少次,获取到的实例都是同一个,但在两个子线程中获取到了不同的实例。

其实,ThreadLocal是将所有的对象全部放在ThreadLocalMap中,为每一个线程提供一个对象。

大功告成 ~

喜欢就一键三连 b( ̄▽ ̄)d
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@是小白吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值