快捷搜索:

java单例模式 实例_详解Java单例模式5种实现方法

单例模式是最简单的设计模式之一,也是Java面试的高频题,有的甚至要求手写单例模式,下面我们就来详细讲解Java单例模式的5种实现方法。

首先,我们来了解下创建单例的四大原则:

构造方法必需是私有的;

以静态方法或枚举返回实例;

确保实例只有一个,尤其在多线程环境下

确保反序列化时不会重新构建对象

常用的单例创建方式有:饿汉式、懒汉式、双重锁检测式(DCL)、静态内部类、枚举模式,下面就来依次讲解一下:

饿汉式

public class Singleton {

private static Singleton instance = new Singleton(); // 直接初始化一个实例对象

// private类型的构造函数,确保其它类对象不能new一个本类的实例

private Singleton() {

}

public static Singleton getInstance() {

return instance;

}

}

饿汉式在类被加载的时候就创建好了实例,此时有可能实例还没有用到。

优点:实现简单,比较直观

缺点:当系统中这样的类比较多时,会使启动速度变慢。

懒汉式

public class Singleton {

private static Singleton instance;

private Singleton() {

}

public static synchronized Singleton getInstance() {

if(instance == null) {

instance = new Singleton();

}

return instance;

}

}

懒汉式只有在实际需要的时候才会创建实例,实现起来也很简单,由一个静态方法返回实例,同时对这个静态方法加了一把锁,所有来获取实例的方法都会去竞争这把锁,不管实例有没有创建。

但这也带来了一个问题,就是锁的粒度太粗。实际上,只有首次创建实例时,为了防止多线程同时创建多个实例才需要锁,当实例已经创建完成后,直接返回实例就好,不再需要什么锁了。但在当前示例里,不管实例有没有创建,只要想获取实例,就要去竞争锁,显然是扩大了锁的竞争范围,效率肯定会降低。

双重锁检测模式(Double Check Lock)

上一节讲到了懒汉式,并且分析了懒汉式的缺点就是锁竞争导致的效率低下,那么有没有效率更高的方法呢,当然有啦,那就是本例的DCL模式,也就是双重锁检测,下面来看一下代码实现。

public class Singleton {

private static volatile Singleton instance;

private Singleton() {

}

public static Singleton getInstance() {

if(instance == null) {

synchronized (Singleton.class) {

if(instance == null) {

instance = new Singleton();

}

}

}

return instance;

}

}

从代码中可以看出,DCL模式在方法内部,对实例是否已创建进行了两次检测,一次加锁,所以叫双重锁检测。

另外DCL模式也是在使用时才会创建,且只在首次创建实例时有锁竞争,实例创建完成后,每次获取实例时,在第一次检测时就已经知道实例创建好了,不会再执行synchronized里的代码段,也就没有锁竞争了,效率明显比懒汉式提高了不少。

解释下为什么获取到锁之后还要再次检测:因为第一次检测时没有加锁,那么获取到锁之后,有可能别的线程已经创建好了,如果不判断直接创建,就可能实际上创建了多个实例,达不到单例的目的了。

同时,请注意,这里还用到了volatile关键字来修饰instance,其最关键的作用是防止指令重排序,具体分析过程如下:

当在Java中new一个对象时,如instance = new Singleton();,其实在JVM里面的步骤分为三步:

在堆内存开辟内存空间

在堆内存中使用参数对Singleton进行实例化

把变量instance指向堆内空间地址

由于JVM存在乱序执行功能,有可能第3步骤先于第2步执行。假如线程A执行完第3步后,线程B来获取实例,此时instance已经不为null了,线程B获取到了实例,但此时线程A第2步还没执行,实例还没完成初始化,线程B获取到的是不完整的实例,那么在使用时就会出问题。

volatile的可以禁止指令重排序,从而避免了上述问题的出现。

静态内部类模式

DCL模式的效率已经非常不错了,但还是有完美主义者提出,有没有一种既不加锁,又能实现懒加载的方法呢,还真有,那就是静态内部类模式。

public class Singleton {

private Singleton() {

}

private static class Inner {

private static Singleton instance = new Singleton();

}

public static Singleton getInstance() {

return Inner.instance;

}

}

静态内部类模式的实现原理:外部类加载时,并不会立刻加载内部类,内部类不被加载,就不会去创建实例,只有当getInstance方法被调用时,才会加载内部类去创建实例,所以实现了延迟加载功能。

一个类在什么情况下会被加载呢?

类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。

1.遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时。

对应的Java代码场景为: new一个关键字或者一个实例对象时、读取或者设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时;

使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,需要先调用其初始化方法进行初始化。

当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。

当虚拟机启动时,用户需要制定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个类。

当使用JDK1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

那么内部静态类是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:

虚拟机会保证一个类的()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其它线程都需要阻塞等待,直到活动线程执行()方法完毕。如果一个类的()方法有耗时很长的操作,就可能造成多个线程阻塞(需要注意的是,其它线程虽然会被阻塞,但如果执行()方法后,其它线程被唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会被初始化一次),在实际应用中,这种阻塞可能是很隐蔽的。

综合分析,通过静态内部类的方式实现单例模式是线程安全的,同时静态内部类不会在Singleton类加载时就加载,而是在调用getInstance()方法时才进行加载,达到了懒加载的效果。

似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。且看如下代码:

public static void main(String[] args) throws Exception {

Singleton singleton = Singleton.getInstance();

Constructor constructor = Singleton.class.getDeclaredConstructor();

constructor.setAccessible(true);

Singleton newSingleton = constructor.newInstance();

System.out.println(singleton == newSingleton);

}

通过结果看,这两个实例不是同一个,这就违背了单例模式的原则了。

除了反射攻击之外,还可能存在反序列化攻击的情况。如下:

引入依赖:

org.apache.commons

commons-lang3

3.8.1

这个依赖提供了序列化和反序列化工具类。

Singleton类实现java.io.Serializable接口。

如下:

public class Singleton implements Serializable {

private Singleton() {

}

private static class Inner {

private static Singleton instance = new Singleton();

}

public static Singleton getInstance() {

return Inner.instance;

}

public static void main(String[] args) {

Singleton instance = Singleton.getInstance();

byte[] serialize = SerializationUtils.serialize(instance);

Singleton newInstance = SerializationUtils.deserialize(serialize);

System.out.println(instance == newInstance);

}

}

运行结果:

通过枚举实现单例

在《effective java》中说到,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。

public enum Singleton {

INSTANCE;

public void doSomething() {

System.out.println("do something!");

}

}

调用方法:

public class Main {

public static void main(String[] args) {

Singleton.INSTANCE.doSomething();

}

}

参考:

单例模式是最简单的设计模式之一,也是Java面试的高频题,有的甚至要求手写单例模式,下面我们就来详细讲解Java单例模式的5种实现方法。 首先,我们来了解下创建单例的四大原则: 构造方法必需是私有的; 以静态方法或枚举返回实例; 确保实例只有一个,尤其在多线程环境下 确保反序列化时不会重新构建对象 常用的单例创建方式有:饿汉式、懒汉式、双重锁检测式(DCL)、静态内部类、枚举模式,下面就来依次讲解一下: 饿汉式 public class Singleton { private static Singleton instance = new Singleton(); // 直接初始化一个实例对象 // private类型的构造函数,确保其它类对象不能new一个本类的实例 private Singleton() { } public static Singleton getInstance() { return instance; } } 饿汉式在类被加载的时候就创建好了实例,此时有可能实例还没有用到。 优点:实现简单,比较直观 缺点:当系统中这样的类比较多时,会使启动速度变慢。 懒汉式 public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } } 懒汉式只有在实际需要的时候才会创建实例,实现起来也很简单,由一个静态方法返回实例,同时对这个静态方法加了一把锁,所有来获取实例的方法都会去竞争这把锁,不管实例有没有创建。 但这也带来了一个问题,就是锁的粒度太粗。实际上,只有首次创建实例时,为了防止多线程同时创建多个实例才需要锁,当实例已经创建完成后,直接返回实例就好,不再需要什么锁了。但在当前示例里,不管实例有没有创建,只要想获取实例,就要去竞争锁,显然是扩大了锁的竞争范围,效率肯定会降低。 双重锁检测模式(Double Check Lock) 上一节讲到了懒汉式,并且分析了懒汉式的缺点就是锁竞争导致的效率低下,那么有没有效率更高的方法呢,当然有啦,那就是本例的DCL模式,也就是双重锁检测,下面来看一下代码实现。 public class Singleton { private static volatile Singleton instance; private Singleton() { } public static Singleton getInstance() { if(instance == null) { synchronized (Singleton.class) { if(instance == null) { instance = new Singleton(); } } } return instance; } } 从代码中可以看出,DCL模式在方法内部,对实例是否已创建进行了两次检测,一次加锁,所以叫双重锁检测。 另外DCL模式也是在使用时才会创建,且只在首次创建实例时有锁竞争,实例创建完成后,每次获取实例时,在第一次检测时就已经知道实例创建好了,不会再执行synchronized里的代码段,也就没有锁竞争了,效率明显比懒汉式提高了不少。 解释下为什么获取到锁之后还要再次检测:因为第一次检测时没有加锁,那么获取到锁之后,有可能别的线程已经创建好了,如果不判断直接创建,就可能实际上创建了多个实例,达不到单例的目的了。 同时,请注意,这里还用到了volatile关键字来修饰instance,其最关键的作用是防止指令重排序,具体分析过程如下: 当在Java中new一个对象时,如instance = new Singleton();,其实在JVM里面的步骤分为三步: 在堆内存开辟内存空间 在堆内存中使用参数对Singleton进行实例化 把变量instance指向堆内空间地址 由于JVM存在乱序执行功能,有可能第3步骤先于第2步执行。假如线程A执行完第3步后,线程B来获取实例,此时instance已经不为null了,线程B获取到了实例,但此时线程A第2步还没执行,实例还没完成初始化,线程B获取到的是不完整的实例,那么在使用时就会出问题。 volatile的可以禁止指令重排序,从而避免了上述问题的出现。 静态内部类模式 DCL模式的效率已经非常不错了,但还是有完美主义者提出,有没有一种既不加锁,又能实现懒加载的方法呢,还真有,那就是静态内部类模式。 public class Singleton { private Singleton() { } private static class Inner { private static Singleton instance = new Singleton(); } public static Singleton getInstance() { return Inner.instance; } } 静态内部类模式的实现原理:外部类加载时,并不会立刻加载内部类,内部类不被加载,就不会去创建实例,只有当getInstance方法被调用时,才会加载内部类去创建实例,所以实现了延迟加载功能。 一个类在什么情况下会被加载呢? 类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。 1.遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时。 对应的Java代码场景为: new一个关键字或者一个实例对象时、读取或者设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时; 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,需要先调用其初始化方法进行初始化。 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。 当虚拟机启动时,用户需要制定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个类。 当使用JDK1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。 这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。 那么内部静态类是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话: 虚拟机会保证一个类的()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其它线程都需要阻塞等待,直到活动线程执行()方法完毕。如果一个类的()方法有耗时很长的操作,就可能造成多个线程阻塞(需要注意的是,其它线程虽然会被阻塞,但如果执行()方法后,其它线程被唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会被初始化一次),在实际应用中,这种阻塞可能是很隐蔽的。 综合分析,通过静态内部类的方式实现单例模式是线程安全的,同时静态内部类不会在Singleton类加载时就加载,而是在调用getInstance()方法时才进行加载,达到了懒加载的效果。 似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。且看如下代码: public static void main(String[] args) throws Exception { Singleton singleton = Singleton.getInstance(); Constructor constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton newSingleton = constructor.newInstance(); System.out.println(singleton == newSingleton); } 通过结果看,这两个实例不是同一个,这就违背了单例模式的原则了。 除了反射攻击之外,还可能存在反序列化攻击的情况。如下: 引入依赖: org.apache.commons commons-lang3 3.8.1 这个依赖提供了序列化和反序列化工具类。 Singleton类实现java.io.Serializable接口。 如下: public class Singleton implements Serializable { private Singleton() { } private static class Inner { private static Singleton instance = new Singleton(); } public static Singleton getInstance() { return Inner.instance; } public static void main(String[] args) { Singleton instance = Singleton.getInstance(); byte[] serialize = SerializationUtils.serialize(instance); Singleton newInstance = SerializationUtils.deserialize(serialize); System.out.println(instance == newInstance); } } 运行结果: 通过枚举实现单例 在《effective java》中说到,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。 public enum Singleton { INSTANCE; public void doSomething() { System.out.println("do something!"); } } 调用方法: public class Main { public static void main(String[] args) { Singleton.INSTANCE.doSomething(); } } 参考:
经验分享 程序员 微信小程序 职场和发展