单例模式是23种设计模式之一,也是学习设计模式遇到的第一个设计模式。
意义:在实际开发中,会有很多通用类,比如获取数据源的类,假如在使用这些类的时候,每用一次就创建一个新的实例,这样就会创建很多实例,实际上这些实例做的作用都是一样的。要想避免这种无用的创建实例,这里就需要用到单例模式。
类型:创建型模式
实现方式:1.构造器私有化 2.在类内部创建好一个实例 3.提供一个方法来获取实例
使用方式:获取该类的实例时,使用提供的方法来获取
接下来从三个方面来介绍单例模式
单例模式创建
1.饿汉式
含义:饿汉式就是犹如它的名字一样,很饿,所以提前准备好食物(实例)。
/** * 1.饿汉式 */ public class Singleton1 { //1.构造器私有化 private Singleton1() { } //2.本类内部创建对象实例 private final static Singleton1 singleton1 = new Singleton1(); //3.提供一个公有的静态方法,返回实例对象 public static Singleton1 getInstance() { return singleton1; } }
优点:这种写法非常的简练,并且线程安全。
缺点:假如这个类不会被使用到,但是在类加载的时候就会创建一个实例,造成了内存浪费。
2.懒汉式
含义:很懒,一开始不会去创建,等你到使用的时候再去创建。
/** * 2.懒汉式(线程不安全) */ public class Singleton2 { //1.构造器私有化 private Singleton2() { } //2.本类内部创建对象实例 private static Singleton2 singleton2; //3.提供一个获取实例的方法,但是在第一次使用的时候再去实例化(存在线程安全问题) public static Singleton2 getInstance() { if (singleton2 == null) { singleton2 = new Singleton2(); } return singleton2; } }
但是这种存在线程安全问题,当多个线程调用 getInstance()方法时,有可能出现线程a执行到 singleton2 = new Singleton2(); 这行时,这时候失去cpu执行权,b线程开始执行getInstance() 方法,成功创建了一个实例,这时候a线程获取到cpu执行权,继续执行 singleton2 = new Singleton2(); 就会造成获取到两个不同的实例。
那么怎么规避这种情况那,答案就是加锁,但是仅仅的加锁还是不完美的
/** * 3.懒汉式(双重检查,线程安全) */ public class Singleton3 { //1.构造器私有化 private Singleton3() { } //2.本类内部创建对象实例 private static volatile Singleton3 singleton3; //3.提供一个获取实例的方法,但是在第一次使用的时候再去实例化 public static Singleton3 getInstance() { if (singleton3 == null) { synchronized (Singleton3.class) { if (singleton3 == null) { singleton3 = new Singleton3(); } } } return singleton3; } }
这里提供了一种双重检查的方法来使懒汉式变成线程安全的方式。
为什么是双重检查,而不是下面这种写法呢?
这是因为如果是下面这种写法,再每一次获取实例对象的时候,所有线程都会去竞争锁,导致额外的开销。实际上线程安全问题只存在第一次创建对象的时候。在synchronized外层套一层判断就可以规避对象创建完成之后,线程竞争锁的问题,提高了效率。
public static Singleton3 getInstance() { synchronized (Singleton3.class) { if (singleton3 == null) { singleton3 = new Singleton3(); } } return singleton3; }
为什么要使用volatile关键字?
这是因为 singleton3 = new Singleton3(); 这一步并不是原子操作,别看这是一行代码,在底层它被分成了三步操作:
1.分配一部分空间用于创建对象
2.在分配好的空间创建对象
3.将创建好的对象指向变量,在这里变量是singleton3
cpu在操作时为了提高效率会出现指令重排的操作,也就是把这三步进行乱序。假如说步骤变成了 1、3、2。那么就会导致 当a线程执行到第2步(执行顺序是132)的时候失去了操作权,b线程开始执行(由于a线程没有执行第2步就把空间指向了变量),发现singleton3指向的空间不是null,但是实际上此空间并没有实例,那么b线程使用该对象时候就会报错。
使用了volatile关键字就会禁止指令重排。
优点:进行了懒加载
缺点:编写复杂,要考虑线程安全
3.静态内部类
/** * 4.静态内部类 */ public class Singleton4 { //1.创建静态内部类来声名实例对象 private static class InnerSingleton { private static final Singleton4 SINGLETON_4 = new Singleton4(); } //2.构造器私有化 private Singleton4() { } //3.提供一个获取实例的方法 public static Singleton4 getInstance() { return InnerSingleton.SINGLETON_4; } }
优点:也是懒加载
4.枚举
/** * 5.枚举 */ public enum Singleton5 { INSTANCE; }
优点:无法使用序列化和反射破坏单例
破坏单例模式
在知道怎么编写单例模式之后,实际上单例模式是可以进行破坏的,这里介绍三种方式:
1.反射
这里通过反射来破坏单例模式,虽然有很多方式在构造器中预防单例模式被破坏,但是道高一尺魔高一丈,总有办法进行破坏。
/** * 反射破坏单例模式 */ public class ReflectDestroy { public static void main(String[] args) throws Exception { //1。获取Singleton1的无参构造方法 Constructor<Singleton1> constructor = Singleton1.class.getDeclaredConstructor(); //2.接触private的封装 constructor.setAccessible(true); //3.通过反射和单例模式创建两个对象,进行比较 Singleton1 instance1 = constructor.newInstance(); Singleton1 instance2 = Singleton1.getInstance(); //4.结果为false,说明两个对象不是一个对象,单例模式被破坏了 System.out.println(instance1 == instance2); } }
2.序列化
首先实现Serializable接口才可以进行序列化
/** * 1.饿汉式 */ public class Singleton1 implements Serializable { //1.构造器私有化 private Singleton1() { } //2.本类内部创建对象实例 private final static Singleton1 singleton1 = new Singleton1(); //3.提供一个公有的静态方法,返回实例对象 public static Singleton1 getInstance() { return singleton1; } }
然后进行测试
public class SerializableDestroy { public static void main(String[] args) throws Exception { //1.获取实例 Singleton1 instance1 = Singleton1.getInstance(); //2.使用输出流保存该对象 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("instance")); out.writeObject(instance1); //3.使用输入流读取对象 ObjectInputStream in = new ObjectInputStream(new FileInputStream("instance")); Singleton1 instance2 = (Singleton1) in.readObject(); //4.发现结果为false,单例被破坏 System.out.println(instance1 == instance2); } }
这里发现单例被破坏,但是可以预防。
3.克隆
首先要实现Cloneable接口
public class Singleton6 implements Cloneable{ private Singleton6() { } private final static Singleton6 singleton6 = new Singleton6(); public static Singleton6 getInstance() { return singleton6; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
测试
public class CloneDestroy { public static void main(String[] args) throws CloneNotSupportedException { Singleton6 instance1 = Singleton6.getInstance(); Singleton6 instance2 = (Singleton6) instance1.clone(); System.out.println(instance1 == instance2); } }
发现单例被破坏,那么如何防止单例被破坏那,请看下一个章节。
如何防止单例模式被破坏
这里我们从破坏单例模式的两个方面进行讲解
1.反射
如果是枚举类,那么就不可以使用反射进行破坏
public class ReflectEnumDestroy { public static void main(String[] args) throws Exception { //1.获取枚举类的构造方法 Constructor<Singleton5> constructor = Singleton5.class.getDeclaredConstructor(String.class, int.class); //2.解除封装 constructor.setAccessible(true); //3.创建对象,此步骤会报错:Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects Singleton5 instance = constructor.newInstance("INSTANCE", 0); } }
首先说一下第一步,为什么获取构造函数要两个参数。枚举类实际上就是一个类实现Enum接口,在Enum接口中可以看到该构造方法
那第三步为什么会报错那,可以点进 newInstance(); 这个方法
这里可以看到在jdk中就定义了不准枚举类进行反射,自然而然枚举类不可以反射创建对象。
2.序列化
又是如何在序列化中保证单例那?
其实在单例类中加一个方法即可
/** * 1.饿汉式 */ public class Singleton1 implements Serializable { //1.构造器私有化 private Singleton1() { } //2.本类内部创建对象实例 private final static Singleton1 singleton1 = new Singleton1(); //3.提供一个公有的静态方法,返回实例对象 public static Singleton1 getInstance() { return singleton1; } //4.新加入的方法,防止序列化破坏单例 private Object readResolve() { return singleton1; } }
然后进行测试
public class SerializableDestroy { public static void main(String[] args) throws Exception { //1.获取实例 Singleton1 instance1 = Singleton1.getInstance(); //2.使用输出流保存该对象 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("instance")); out.writeObject(instance1); //3.使用输入流读取对象 ObjectInputStream in = new ObjectInputStream(new FileInputStream("instance")); Singleton1 instance2 = (Singleton1) in.readObject(); //4.发现结果为true,是单例的 System.out.println(instance1 == instance2); } }
为什么这个方法加入就可以保证单例那?
在ObjectInputStream类中可以找到答案
可以从 Singleton1 instance2 = (Singleton1) in.readObject(); 这一行的readObject()方法进入查看
如果单例类中有readResolve()方法,那么就会调用这个方法获取对象,保持单例。
枚举类本身就实现了序列化接口,直接进行序列化就可以
public class SerializableEnumDestroy { public static void main(String[] args) throws Exception { Singleton5 instance1 = Singleton5.INSTANCE; ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("instance")); out.writeObject(instance1); ObjectInputStream in = new ObjectInputStream(new FileInputStream("instance")); Singleton5 instance2 = (Singleton5) in.readObject(); //结果为true,是单例的 System.out.println(instance1 == instance2); } }
枚举类没有readResolve()方法但是也可以保持单例,这是因为枚举的实例会保存在一个map中,实际读取的时候会从map中读取保证单例。
可以说枚举的特性用来做单例最为合适,后期整理到框架,也会结合框架来丰富单例模式。