单例模式的多种实现

单例模式

应用场景:

  保证一个类仅有一个实例, 并提供一个访问它的全局访问点。
  Spring 中的单例模式完成了后半句话, 即提供了全局的访问点 BeanFactory。 但没有从构造器级别去控制单例, 这是因为 Spring 管理的是任意的 Java 对象。 Spring 下默认的 Bean 均为单例。

实现:

饿汉式单例

  它是在类加载的时候就立即初始化,并且创建单例对象。

  • 优点
    • 没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
    • 绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题。
  • 缺点
    • 类加载的时候就初始化,不管你用还是不用,我都占着空间。浪费了内存,有可能占着茅坑不拉屎。
public class Hungry {

    private Hungry(){}

    private static final Hungry hungry = new Hungry();

    public static Hungry getInstance(){
        return  hungry;
    }

}

懒汉式单例

在外部需要使用的时候才进行实例化。

public class LazyOne {
    private LazyOne(){}


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

    public static LazyOne getInstance(){

        //调用方法之前,先判断
        //如果没有初始化,将其进行初始化,并且赋值
        //将该实例缓存好
        if(lazy == null){
            //两个线程都会进入这个if里面
            lazy = new LazyOne();
        }
        //如果已经初始化,直接返回之前已经保存好的结果

        return lazy;
    }
}

  上述代码中,并发访问时会出现问题,如果两个线程同时进入if中,则会创建出两个实例,不符合单例要求。测试代码如下(纯属娱乐):

public class ThreadSafeTest {


    public static void main(String[] args) {
        int count = 200;

        //发令枪,我就能想到运动员
        CountDownLatch latch = new CountDownLatch(count);

        long start = System.currentTimeMillis();
        for (int i = 0; i < count;i ++) {
            new Thread(){
                @Override
                public void run() {
                    try{
                        try {
                            // 阻塞
                            // count = 0 就会释放所有的共享锁
                            // 万箭齐发
                            latch.await();
                        }catch(Exception e){
                            e.printStackTrace();
                        }

                        //必然会调用,可能会有很多线程同时去访问getInstance()
                        Object obj = LazyOne.getInstance();
                        System.out.println(System.currentTimeMillis() + ":" + obj);

                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }.start(); //每循环一次,就启动一个线程,具有一定的随机性

            //每次启动一个线程,count --
            latch.countDown();

        }
        long end = System.currentTimeMillis();
        System.out.println("总耗时:" + (end - start));


//        CountDownLatch 并不是这样子用,实际应用场景中不要学老师这样投机取巧
    }
}

输出结果如下:
在这里插入图片描述

懒汉式单例+同步锁

public class LazyTwo {

    private LazyTwo(){}

    private static LazyTwo lazy = null;

    public static synchronized LazyTwo getInstance(){

        if(lazy == null){
            lazy = new LazyTwo();
        }
        return lazy;

    }

}

测试发现该写法效率很低,测试代码如下:

public class LazyTest {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 200000000;i ++) {
            Object obj = LazyOne.getInstance();
        }
        long end = System.currentTimeMillis();
        System.out.println("总耗时:" + (end - start));
    }

}

加锁前后耗时如下:
在这里插入图片描述
在这里插入图片描述

有没有即不浪费资源,又可以保证并发时不出现问题,效率还高的单例写法呢?
答案是有的。

懒汉模式——内部类写法

内部类特点
  在外部类被调用的时候内部类才会被加载;内部类一定是要在方法调用之前初始化。这样就巧妙地避免了线程安全问题。

public class LazyThree {

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

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

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


}

  这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题,完美地屏蔽了这两个缺点。堪称史上最牛B的单例模式的实现方式。

  那么问题又来了,如果恶意通过反射来创建对象,不就可以破坏这种单例模式了么?
  代码就是用来解决问题,请品鉴:

public class LazyThree {

    private static boolean initialized = false;

    private LazyThree(){

        synchronized (LazyThree.class){
            if(initialized == false){
                initialized = !initialized;
            }else{
                throw new RuntimeException("单例已被侵犯");
            }
        }

    }

    public static final LazyThree getInstance(){
        //在返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }
    
    private static class LazyHolder{
        private static final LazyThree LAZY = new LazyThree();
    }
    
}

  通过上述代码可知,我们可以重写私有构造方法解决反射可能引发的非单例情况。

  问题又来了系列,假如通过反射事先改变initialized静态成员变量的值,那不又可以破坏单例模式了么?
  这个问题先留在这里,大家可以在评论区留言讨论哦(*_

注册登记式

  每使用一次,都往一个固定的容器中注册并且将使用过的对象进行缓存,下次去取对象的时候,就直接从缓存中取值,以保证每次获取的都是同一个对象。
  IOC中的单例模式,就是典型的注册登记式单例。

//Spring中的做法,就是用这种注册式单例
public class BeanFactory {

    private BeanFactory(){}

    //线程安全
    private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();

    public static synchronized Object getBean(String className){
        if(!ioc.containsKey(className)){
            Object obj = null;
            try {
                obj = Class.forName(className).newInstance();
                ioc.put(className,obj);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return obj;
        }else{
            return ioc.get(className);
        }
    }
}

  这里也抛给大家一个思考:去掉synchronized关键字可不可以?
  答案是不行的,因为去掉后,在高并发的情况下,可能同时有两个或以上线程来调用getBean()方法,导致单例模式遭到破坏。
  而synchronized关键字又很影响效率,有没有什么两者兼顾的解决方法呢?

序列化与反序列化保证单例

  序列化就是说把内存中的状态通过转换成字节码的形式,从而转换一个IO流,写入到其他地方(可以是磁盘、网络IO),内存中状态给永久保存下来了;反序列化将已经持久化的字节码内容,转换为IO流。通过IO流的读取,进而将读取的内容转换为Java对象,在转换过程中会重新创建对象new。
  而如何保证反序列化创建的对象在内存中也保证是单例的呢? 就是我们接下来要解决的问题。

//反序列化时导致单例破坏
public class  Seriable implements Serializable {

    public  final static Seriable INSTANCE = new Seriable();
    private Seriable(){}

    public static  Seriable getInstance(){
        return INSTANCE;
    }

    private  Object readResolve(){
        return  INSTANCE;
    }
}

  答案就是重写readResolve()方法,具体的原因大家可以通过学习jvm了解。

一图流在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值