java单例模式详解

目录

         一、饿汉式

二、懒汉式

三、静态内部类式

四、枚举式


单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类等,其中没一种实现方式都有其对应的特点和不同的应用场景,下面分别就每种方式做详细的说明。

一、饿汉式

饿汉式就是在类加载的时候就会将该单实例对象创建出来。

public class HungrySingle {
    private HungrySingle(){}

    private final static HungrySingle HUNGRY_SINGLE = new HungrySingle();

    public static HungrySingle getInstance(){
        return HUNGRY_SINGLE;
    }
}

这种方式就是我不管你用到用不到这个实例对象我先给你创建出来,用的时候直接拿过来用就可以了。给人很饥渴的感觉,所以俗称饿汉式。它的缺点就是有浪费内存的可能,假如我一直用不到这个实例对象你却给我创建了出来占用了一定的内存空间,虽然占用空间很小但也是占用了。优点就是它不存在线程安全问题,在多线程高并发的环境下可以安全执行,也不存在会创建出多个实例的可能。

针对饿汉式的单例模式我们思考一下如下的几个问题:

1.如果这个单例实现了序列化接口Serializable,我们需要做什么来防止反序列化的时候破坏单例?

我们知道对象的创建不单单是通过new可以来创建,如果你一个类实现了序列化接口,那么我们通过反序列化也可以生成一个类的对应对象。如此,我们通过反序列化创建出来的对象和原本通过单例模式构建出来的对象就不是同一个对象了,这样就有了两个不同的对象,违反了单例模式的初衷。那怎么解决这个问题呢?我们只需在单例类中加一个public Object readResovle()方法并把单例对象返回即可,注意这个方法的名字和返回值都是规定死的,必须这么写。那为什么加了这个方法以后就可以防止反序列化破坏单例呢?这是因为在反序列化的过程中一旦发现类中有一个readResovle返回了一个对象,它就会采用已有的对象进行返回(这个对象HUNGRY_SINGLE就是HungrySingle类在加载时就创建好的对象),而不会反序列化一个新的对象。如下:

public class HungrySingle implements Serializable {
    private HungrySingle(){}

    private final static HungrySingle HUNGRY_SINGLE = new HungrySingle();

    public static HungrySingle getInstance(){
        return HUNGRY_SINGLE;
    }
    
    //防止反序列化破坏单例规则
    public Object readResovle(){
        return HUNGRY_SINGLE;
    }
}

2.懒汉模式是怎么保证线程安全的?

我们看上面的代码可以知道HUNGRY_SINGLE是一个成员变量,并且它被static关键字修饰,静态成员变量的初始化操作是在类加载的时候完成的,而类加载阶段是由JVM来执行的,所以在这个阶段其实是由JVM在底层为我们保证了线程安全。

3.是否能反之通过反射机制来创建新的单例对象?

我们知道反射功能很强大,有流氓的称号,所以很不幸的是饿汉模式也不可避免通过反射来创建出多个实例的命运。具体的演示下文会详细说明。

以上三个问题是对饿汉式单例的扩展了解,当然你在面试的时候面试官要你手写饿汉单例,你只需要把最上面的那种写出来就可以了。

二、懒汉式

与饿汉式相比,懒汉式的单例在类加载时不会导致该单实例对象被创建,而是首次使用该对象时才会创建。

第一版:

public class LazySingle {
    private LazySingle(){}

    private static LazySingle lazySingle;

    public static LazySingle getInstance(){
        if(lazySingle == null){
            lazySingle = new LazySingle();
        }
        return lazySingle;
    }
}

上面这种形式的懒汉式单例在单线程环境下没有任何问题,但在多线程环境它就会出现创建出多个对象的问题,这就违背了单例模式的初衷,我要一个,你却给我创建出多个来。比如现在有两个线程同时调用了getInstance()方法,同时走到了if(lazySingle == null)这行代码,两个线程都判断出lazySingle == null,那么这时两个线程就会都进入到if语句块中,这样就会new出两个对象,

如下图:

                          

第二版:

public class LazySingle {
    private LazySingle(){}

    private static LazySingle lazySingle;
    //给方法加上synchronized关键字
    public static synchronized LazySingle getInstance(){
        if(lazySingle == null){
            lazySingle = new LazySingle();
        }
        return lazySingle;
    }
}

上面这一版就可以避免在多线程环境下创建出多个实例的问题和其他一些线程安全的问题,所以它是线程安全的。但这种方式会存在性能上的一些问题,以上单例,加锁的粒度太大,它加在了整个方法上,这就导致每个线程来了都会执行加锁操作,而我们知道加锁操作是会额外的消耗系统资源的,所以有了下面的双重检查模式(double check locking 简称DCL),这种方式只有在第一次创建对象的时候需要加锁,之后其他其他线程执行到此方法通过判断对象是否创建(第一次已经创建了实例),因为第一次创建过了,就不需要继续往下执行加锁操作了,而是直接返回对象。如下:

版本三:

public class LazySingle {
    private LazySingle(){}

    private static LazySingle lazySingle;

    public  static LazySingle getInstance(){
        if(lazySingle == null){
            synchronized (LazySingle.class){
                if (lazySingle == null){
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }
}

对以上代码你可能会疑惑为什么要做两次非空判断? 进入Synchronized 临界区之前做了一次判断了,怎么进入Synchronized 临界区之后又做了一次?我们假设这样一个场景:当线程1和线程2都已经走到了 if(lazySingle == null)这行代码,它俩都判断为空,那么它两会接着走下面的加锁代码,这时假设线程1拿到了锁对象,而线程2阻塞在了这里,那么线程1就开始执行临界区的代码并new出了一个对象,临界区代码执行完以后把锁释放掉。这时线程2拿到锁对象去执行临界区的代码,如果没有第二层临界区的判空操作,那么线程2也会去new一个对象出来,这样就会导致有两个实例对象,这当然是违背单例模式的初衷的。而加上这一层判空操作就可以避免这种情况的发生。如下:

                     

但是以上方式虽然较版本二不用每个线程来都去加锁,性能得到了提升,但它却存在线程安全的问题。假设这样的场景,当两个线程一先一后访问getInstance方法的时候,当2线程正在构建对象,1线程刚刚进入方法:

                          

这里需要了解一个概念就是“指令重排序”:CPU为了提高执行效率,有时候会将某些语句(非原子操作)拆分成原子操作,然后对这些原子操作做进一步排序,以提高执行效率。而我们上面代码中的lazySingle = new LazySingle();其实就是一个非原子操作,它可以进一步拆分成如下三个步骤:

  1. VM会分配内存地址空间
  2. 使用构造方法实例化对象
  3. 将lazySingle指向第一步中分配好的内存地址

以上3个动作在真正执行时可能123,也可能是132,因为这样排序后在单线程下不会对结果造成任何影响,所以是可以进行重排序的。但是如果在多线程环境下,使用132可能出现如下问题:假设线程2刚刚执行完以下步骤(即刚执行1、3,但还没有执行2)

    1.JVM会分配内存地址和空间0x123

    3.nstance = 第一步中分配好的内存地址:lazySingle=0x123

此时线程1进入单例程序的if判断,直接会得到lazySingle对象(注意,此时的lazySingle是刚才线程2并没有new出来的对象),就去使用该对象,例如调用对象的方法lazySingle.xxx()必然报错,因为此时虽然给对象分配了内存地址,但是却没有把new出来的对象放到这个地址上。那么怎么解决这个问题呢?解决方案就是禁止此程序使用132的重排序,那么在lazySingle前面加上volatile关键字即可实现。

版本四:

public class LazySingle {
    private LazySingle(){}
    //和版本三比没有其他变化,只是多加了一个volatile关键字
    private volatile static LazySingle lazySingle;

    public  static LazySingle getInstance(){
        if(lazySingle == null){
            synchronized (LazySingle.class){
                if (lazySingle == null){
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }
}

三、静态内部类式

public class HolderSingle {
    private HolderSingle(){

    }
    
    public static class InnerClass{
        private static final HolderSingle HOLDER_SINGLE = new HolderSingle();
    }

    public static HolderSingle getInstance(){
        return InnerClass.HOLDER_SINGLE;
    }
}

静态内部类形式属于懒汉式,因为在类加载得时候,JVM只会加载外部的HolderSingle类而不会去加载内部的InnerClass类,只有在你用到这个内部类的时候才会触发它的加载,所以当我们调用getInstance()方法的时候才会用到InnerClass类,这个时候才会触发内部类的加载进而创建出单例对象HolderSingle,因为在JVM层面对类的加载是线程安全的,所以这种方式创建的单例也是不会存在线程安全问题的。因此这也是一种比较推介使用的单例模式的实现形式。

以上所说的几种方式,不管是懒汉、饥汉、DCL还是静态内部类都无法避免一个问题:就是通过反射来重新构建对象。虽然你是单例模式,但是我通过反射机制还是可以new出n个不同的对象。下面我们看看怎样通过反射来打破单例模式只能构建一个对象实例的约束:

利用反射打破单例:

//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));

最后输出的结果为false,可见 singleton1和 singleton2已经不是同一个对象了,这就打破了单例模式只能建造一个对象的约束。那怎么才能阻止反射去构建对各对象实例呢?这就要用到我们下面说的利用枚举方式构建的单例了。

四、枚举式

public enum SingletonEnum {
    INSTANCE;
}

对,枚举单例模式的代码就这么简单,如果对枚举的语法不熟悉就先补补基础。我们都知道反射机制是通过获取一个类的构造方法来创建对象的,而JVM会阻止反射来获取枚举类的私有构造方法。使用枚举实现的单例不仅可以防止反射破坏单例约束,也能保证线程安全,而且还可以防止反序列化破坏单例。不过这种方式属于饿汉式的,其单例对象是在枚举类被加载的时候就进行了初始化操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值