java单例模式的7种实现方式

1.懒汉式加载,最简单的单例模式,只需要2两步,

   a.把自己的构造方法设置为私有的,不让别人访问你的实例,b.提供一个static方法给别人获取你的实例

public class Singleton{
    private static Singleton singleton;

    private Singleton(){

    }

    public static Singleton getInstance(){
        if(singleton==null){
            singleton = new Singleton();
        }        
        return singleton;
    }
}

该方式,在单线程情况下是没问题的,但是在多线程情况下还是会创建多个实例,测试代码如下:

public class SingletonTest {
    public static void main(String[] args) throws InterruptedException {
        Set<Singleton> set = new HashSet<>();
        Set<Singleton> singletonSet = Collections.synchronizedSet(set);
        for (int i = 0; i <1000 ; i++) {
            new Thread(()->{
                singletonSet.add(Singleton.getInstance());
            }).start();
        }
        Thread.sleep(10000);
        for (Singleton singleton : singletonSet) {
            System.out.println(singleton);
        }
    }
}

为什么会这样呢?我们假设第一个线程进入getInstance方法,判断实例为null,准备进入if块内执行实例化,这时线程突然让出时间片,第二个线程也进入方法,判断实例也为null,并且进入if块执行实例化,第一个线程唤醒也进入if块进行实例化。这时就会出现2个实例。所以出现了bug

2.饿汉式加载,即在jvm加载这个类时,就进行实例的初始化,当调用getInstance()时直接返回;

public class Singleton{
    private static final Singleton singleton = new Singleton();

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

 这种方式是能保证单例,但缺点是,当你没有用到这个实例时,该实例已经被加载了,如果该单例很大的话,将浪费很多的内存。

3.synchronized 同步式,同步静态getInstance()方法;

public class Singleton{
    private static Singleton singleton;

    private Singleton(){

    }
       
    //同步静态方法
    public synchronized static Singleton getInstance(){
        if(singleton==null){
            singleton = new Singleton();
        }        
        return singleton;
    }
}

 这种方式是能够保证线程安全和单例,但是在多线程情况下,每个线程进入该方法都要阻塞排队等待,当实例已经初始化之后,还需要做同步控制吗?显然这种方式,对性能的影响是很大的;

4.双重锁验证

public class Singleton{
    private static volatile Singleton singleton;

    private Singleton(){

    }

    public static Singleton getInstance(){
        //多线程直接访问,不做控制,不影响性能,
        if(singleton==null){
        //此时,如果有多个线程进入,则进入同步块,其余线程等待
            synchronized(this){
                //此时,第一个线程进来判断为null,但是第二个线程进来已经不是null了
                if(singleton==null){
                    singleton = new Singleton();
                }
            }
        }        
        return singleton;
    }
}

首先看getInstance方法,我们在方法声明上去除了synchronized关键字,多线程进入方法内部,判断是否为null,如果为null,多个线程同时进入if块内,此时,我们是用Singleton Class对象同步一段方法。保证只有一个线程进入该方法。并且判断是否为null,如果为null,就进行初始化。我们想象一下,如果第一个线程进入进入同步块,发现该实例为null,于是进入if块实例化,第二个线程进入同步内则发现实例已经不是null,直接就返回 了,从而保证了并发安全。那么这个和第三种方式又什么区别呢?第三种方式的缺陷是:每个线程每次进入该方法都需要被同步,成本巨大。而第四种方式呢?每个线程最多只有在第一次的时候才会进入同步块,也就是说,只要实例被初始化了,那么之后进入该方法的线程就不必进入同步块了。就解决并发下线程安全和性能的平衡。虽然第一次还是会被阻塞。但相比较于第三种,已经好多了;为什么要使用volatile关键字呢?

首先我们看,Java虚拟机初始化一个对象都干了些什么?总的来说,3件事情:

  1. 在堆空间分配内存

  2. 执行构造方法进行初始化

  3. 将对象指向内存中分配的内存空间,也就是地址

但是由于当我们编译的时候,编译器在生成汇编代码的时候会对流程进行优化(这里涉及到happen-before原则和Java内存模型和CPU流水线执行的知识,就不展开讲了),优化的结果式有可能式123顺序执行,也有可能式132执行,但是,如果是按照132的顺序执行,走到第三步(还没到第二步)的时候,这时突然另一个线程来访问,走到if(Singleton == null)块,会发现Singleton 已经不是null了,就直接返回了,但是此时对象还没有完成初始化,如果另一个线程对实例的某些需要初始化的参数进行操作,就有可能报错。使用volatile关键字,能够告诉编译器不要对代码进行重排序的优化。就不会出现这种问题了。

5.静态内部类实现,即是懒汉式加载,也能够保证线程安全

public class Singleton {
    private  static  Singleton instance = null;
    private Singleton(){

    }
    public static Singleton getInstance3(){
        return  SingletonFactory.singleton;
    }

     //使用静态内部类    
    private static class SingletonFactory{
        private static  Singleton singleton = new Singleton();
    }
}

引入了内部类的方式,虚拟机的机制是,如果你没有访问一个类,那么是不会载入该类进入虚拟机的。当我们使用外部类的时候其他属性的时候,是不会浪费内存载入内部类中的单例的,而也就保证了并发安全和防止内存浪费。

6.由于序列化和反序列化会破坏单例,因此对于实现Serializable或Externalizable的单例,需要定义一个readResolve方法

public class Singleton implements Serializable{
    private  static  Singleton instance = null;
    private Singleton(){

    }

    public static Singleton getInstance3(){
        return  SingletonFactory.singleton;
    }
    
    private static class SingletonFactory{
        private static  Singleton singleton = new Singleton();
    }
   
    public Object readResolve(){
         return  SingletonFactory.singleton;
    }
}

重写readResolve() 方法,防止反序列化破坏单例机制,这是因为:反序列化的机制在反序列化的时候,会判断如果实现了serializable或者externalizable接口的类中包含readResolve方法的话,会直接调用readResolve方法来获取实例。这样我们就制止了反序列化破坏我们的单例模式。

7.使用枚举

public enmu Singleton{
    
    SINGLETON;

    //获取实例
    public Singleton getInstance(){
        return SINGLETON;
    }
}

为什么使用枚举可以呢?枚举类型反编译之后可以看到实际上是一个继承自Enum的类。所以本质还是一个类。 因为枚举的特点,你只会有一个实例。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值