Java单例模式

单例模式

Java多线程程序中,有时候需要延迟对象的初始化,降低初始化类和创建对象的开销。

而单例模式就提供了这种解决方法。在单例模式中,只允许一个类创建一个对象。所以不能让外部提供构造方法创建它,于是就

private SingleDemo(){}

且只提供一个对象,则用一个公共方法来获取这一个对象。

单例模式有饿汉式,和懒汉式。

饿汉式,缺点是提前就进行了对象初始化,资源的浪费,等到需要的时候再初始化,要延迟初始化。但是这也不是一个缺点,如果这个实例初始化耗时很长,我们可以再系统上线前就知道,不用等到系统允许一段时间才出现问题,类似fail-fast。

public class SingletonDemo2 {
    private SingletonDemo2(){}
    private static SingletonDemo2 sd = new SingletonDemo2();
    public static SingletonDemo2 getInstance(){
        return sd;
    }
}

懒汉式,延迟加载。但是很明显,这里完全没有进行同步,所以会并发问题。

public class SingletonDemo2 {
    private SingletonDemo2(){}
    private static SingletonDemo2 sd;
    public static SingletonDemo2 getInstance(){
        if (sd == null) {//1
            sd = new SingletonDemo2();//2
        }
        return sd;
    }
}

线程A 执行到注释2代码,此时还需创建对象,并不是立即就创建好。那么线程B也可以进入if。

我们可以加锁,对方法加个锁

public class SingletonDemo2 {
    private SingletonDemo2(){}
    private static SingletonDemo2 sd;
    public synchronized static SingletonDemo2 getInstance(){
        if (sd == null) {//1
            sd = new SingletonDemo2();//2
        }
        return sd;
    }
}

经过加锁之后,确保只有一个线程可以进行对象的初始化。这在竞争不激烈的时候,实现了我们的需求。但是一旦getInstance()方法被频繁调用,那么将会阻塞全部线程在外面然后进入,无论是有没有初始化完毕。太耗费性能。于是,就有了双重检查锁定

public class SingletonDemo2 {
    private SingletonDemo2(){}
    private static SingletonDemo2 sd;
    public  static SingletonDemo2 getInstance(){
        if (sd == null) {//1
            synchronized(SingletonDemo2.class){
                if(sd == null){
                    sd = new SingletonDemo2();//2
                }
            }            
        }
        return sd;
    }
}

这样我们从表面上看代码似乎完成了我们的需求

  1. 多个线程试图同一时刻初始化对象,确保只会有一个对象去初始化。
  2. 在对象创建好之后,直接判断,不会阻塞线程。

但是,当你了解了对象的初始化,就会发现有问题。

对象的初始化分为三步

1. memory = allocate() //为对象分配内存
2. ctorInstance(memory) //初始化对象
3. sd = memory //将sd引用指向刚分配的内存地址

2和3之间可能会发生重排序,于是

1. memory = allocate() //为对象分配内存 1
3. sd = memory //将sd引用指向刚分配的内存地址 2
2. ctorInstance(memory) //初始化对象 3

假设此时线程A执行到了 现在的第二步,也就是sd = new SingletonDemo2(); 这意味着线程A执行完了,会退出锁。但是此时还未及时执行第三步 初始化对象的时候,线程B进入了同步代码块。之前一直在阻塞等待锁。进入了之后,即线程B访问到了一个未初始化的对象sd。

发现了问题,怎么解决?

  1. 禁止2,3重排序,使用volatile

    private volatile static AtomicInteger sd;
    public static AtomicInteger getInstance(){
        if (sd == null) {
            synchronized (SingletonDemo2.class){
                if(sd == null){
                    sd = new AtomicInteger(0);
                }
            }
        }
        return sd;
    }
    
  2. 让2,3重排序对其它线程不可见,使用静态内部类

    private static class Inner{
         private static final SingletonDemo2 sd = new SingletonDemo2();
     }
    public static SingletonDemo2 getInstance(){
        return Inner.sd;
    }
    

    JVM的类在初始化阶段(即在Class被加载,且未被线程使用之前),会执行类的初始化。在执行期间,JVM会获取一个锁,这保证多个线程对类初始化是线程安全的。

    而有如下代码就会引起类T的初始化

    1. T是一个类,而且T类型的实例被创建
    2. T是一个类,T中声明的一个方法被调用
    3. T声明的一个静态变量被赋值
    4. T声明的一个静态字段被使用,且不是一个final修饰的字段
    

    则静态内部类的调用就,是属于4的,静态字段被使用。会引起类的初始化。也是此时,JVM保证只有一个线程会获取锁然后初始化类。

学习自:并发编程的艺术,第三章

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值