安全的发布对象:单例模式

  作为最常用的设计模式——单例模式,是平常最常用的,也是面试最常问的。在上一节中讲到了对象的发布与逃逸,如何安全的发布对象?单例模式给出了答案。单例模式的应用非常广,在连接池,线程池,以及Spring框架中的所有类,都是单例模式的体现

  

饿汉式

  单例模式也就是说类只有一个实例,那么构造方法肯定是私有的,提供一个工厂方法来创建对象。而饿汉式则是无论是否需要该实例,在类创建的时候就初始化该实例,当需要的时候返回即可

直接初始化
public class Singleton {
    private static Singleton singleton = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return singleton;
    }
}

  在一开始就创建了实例singleton,当该实例的时候,直接通过getinstance方法来获得即可。那么如何保证多线程下,该类只有一个实例?在第一次使用类的时候,会进行加载,验证,准备,解析,初始化五个阶段,在准备阶段,会为静态变量分配空间,在初始化阶段,会为静态变量赋值,该过程由jvm完成,并且保证了该过程只会执行一次,所以在多线程下,该写法是安全的

静态代码块

  和上面的思想类似,在初始化阶段不但为静态变量赋值,还会执行静态代码块,所以,我们也可也将初始化的过程挪到静态代码块中

class Singleton {
    private static Singleton singleton = null;

    private Singleton() {
    }

    static {
        singleton = new Singleton();
    }

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

  这两种写法都是饿汉式的写法,思考一下不难发现,这种写法不论是否调用getInstance方法,初始化的工作都会完成,这种写法与懒加载的写法比起来,有可能就会出现一定的浪费,下面看一下懒汉式的写法

懒汉式

  懒汉式顾名思义,就是只有当第一次外界需要拿到实例的时候,才会去初始化实例,并返回给调用对象,那么很多人会说,这很简单啊, 这样写即可:

class Singleton {
    private static Singleton singleton = null;
    private Singleton() {}
    public static Singleton getInstance() {
    	//如果单例没有初始化,那么直接初始化,再进行返回
    	//如果初始化过了,那么直接返回
    	//位置1
        if(singleton == null) {
        	//位置2
            singleton = new Singleton();
        }
        return singleton;
    }
}

  这种写法看似没有问题,但是如果在多线程的情况下,线程1走到了位置2,但是这个时候还没有执行初始化语句,线程2已经到位置1进行了判断,然后线程2也走到了位置2,那么两个线程会执行两次初始化语句,调用两次构造函数,这里的构造虽然为空,只是作为演示,实际的过程中构造函数会做很多的事情如资源的初始化等等,那么这种写法肯定是线程不安全的

  既然是线程不安全的,在Java中可以使用synchronized来保证线程安全,于是就想到了这种写法:

class Singleton {
    private static Singleton singleton = null;
    private Singleton() {}
    public synchronized static Singleton getInstance() {
        if(singleton == null) {
            singleton = new Singleton3();
        }
        return singleton;
    }
}

  通过锁住静态方法也就是锁住整个类,来保证线程安全,现在安全解决了,但是却带来了很大的效率问题,不管是第几次访问该方法,都会进行加锁,解锁的过程,这样的话初始化完成后,后面每次去获取实例的时候都是串行执行,效率很低,这就出现了双重校验锁的方法

class Singleton {
    private static Singleton singleton = null;
    private Singleton() {}
    public static Singleton getInstance() {
    	//先判断有没有必要加锁
        if(singleton == null) {
            synchronized (Singleton.class) {
                if(singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

但是这种写法是线程不安全的?为什么呢?在进行初始化对象的时候分为三步:

(1)memory = allocate() 分配对象的内存空间,对所有的实例变量赋默认值
(2)ctorInstance() 初始化对象    
(3)instance = memory 设置instance指向刚分配的内存

  在jvm底层,会进行指令的重排序,(2)和(3)会进行重排序,实际的执行顺序是(1)(3)(2),那么如果线程1已经执行到了(3),但是并没有初始化对象,这时实例并不为null,线程2过来进行第一重判断后,直接就将还没有初始化的对象进行了返回,造成了不安全的情况。

  可以通过volatile关键字来保证,volatile的作用由两个:1.保证可见性,2.防止执行重排序。为singleton加上volatile即可:

 private volatile static Singleton singleton = null;
通过枚举实现
public class SingletonExample {

    private SingletonExample() {
    }

    public static SingletonExample getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;
        private SingletonExample singleton;

        Singleton() {
            singleton = new SingletonExample();
        }

        public SingletonExample getInstance() {
            return singleton;
        }
    }
}

  由于枚举类的特殊性,枚举类的构造函数Singleton方法只会被实例化一次,且是这个类被调用之前。这个是JVM保证的。对比懒汉与饿汉模式,它的优势很明显。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值