> 文章列表 > 设计模式之单例模式~

设计模式之单例模式~

设计模式之单例模式~

设计模式包含很多,但与面试相关的设计模式是单例模式,单例模式的写法有好几种,我们主要学习这三种—饿汉式单例,懒汉式单例、登记式单例,这篇文章我们主要学习饿汉式单例

单例模式:

满足要点:

私有构造
提供静态全局变量
提供专门访问静态全局变量的方法

饿汉式实现:

实现类:

import java.io.Serializable;//1:饿汉式
public class Singleton1 implements Serializable {//私有构造private Singleton1(){System.out.println("private Singleton1");}//提供静态的全局变量private  static final Singleton1 INSTANCE= new Singleton1();//相对于懒汉式是提前创建的//提供专有的方法去获得静态变量public static Singleton1 getInstance(){return  INSTANCE;}public static void otherMethod(){System.out.println("otherMethod()");}
}

测试类:

public class Test_Singleton1 {public static void main(String[] args) {//触发当前类的加载链接--导致该类的初始化操作被创建,当我们调用otherMethod()方法时,该类已经被创建,因为在输出的内容中,包含该类私有构造中的内容Singleton1.otherMethod();System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");//下面我们调用getInstance()方法时,得到的对象为上述已经创建好的对象System.out.println(Singleton1.getInstance());System.out.println(Singleton1.getInstance());}
}

输出:

private Singleton1
otherMethod()
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Singleton1@7ef20235
Singleton1@7ef20235

由于上述的单例我们实现的是Serializable接口,因此很容易被破坏,常见的破坏方式有以下几种:

反射破坏单例:

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;public class Test_Singleton1 {Singleton1.otherMethod();System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");System.out.println(Singleton1.getInstance());System.out.println(Singleton1.getInstance());//反射破坏单例reflection(Singleton1.class);}private static void reflection(Class<?> classz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {//通过getDeclaredConstructor获得私有构造Constructor<?> constructor=classz.getDeclaredConstructor();//通过setAccessible使得私有的构造方法也可以被使用constructor.setAccessible(true);//使用newInstance创建新的对象System.out.println("反射创建实例:"+constructor.newInstance());}
}

输出:

private Singleton1
otherMethod()
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Singleton1@7ef20235
Singleton1@7ef20235
private Singleton1
反射创建实例:Singleton1@15aeb7ab

经过反射操作的Singleton1已经不是单例了,由于getInstance和反射创建的对象并不是同一个,一个类有两个对象,并不符合单例

newInstance()和new()区别:

创建对象的方式不同,newInstance()是实用类的加载机制,new()则是直接创建一个类newInstance创建类是这个类必须已经加载过且已经连接(Class.forName(“A”)这个过程)new创建类是则不需要这个类加载过

newInstance实际上是把new这个方式分解为两步,首先调用class的加载方法加载某个类,然后实例化,这样做的好处体现在我们可以在调用class的静态加载方法forName时获得更好的灵活性

newInstance 是弱类型(GC是回收对象的限制条件很低,容易被回收)、低效率、只能调用无参构造

new是强类型(GC不会自动回收,只有所有的指向对象的引用被移除是才会被回收,若对象生命周期已经结束,但引用没有被移除,经常会出现内存溢出)

预防反射破坏单例:

解决方法:

在私有构造中加入如下代码,当单例对象已经被创建过时,则直接抛出异常

if(INSTANCE!=null) {throw new RuntimeException("单例对象不能重复创建");
}

测试结果如下:

对象只被创建了一次,第二次通过反射创建对象时,直接抛出异常

设计模式之单例模式~

反序列化破坏单例:

import java.io.*;
import java.lang.reflect.InvocationTargetException;public class Test_Singleton1 {public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {Singleton1.otherMethod();System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");System.out.println(Singleton1.getInstance());System.out.println(Singleton1.getInstance());//反序列化破坏单例serializable(Singleton1.getInstance());}private static void  serializable(Object instance) throws IOException, ClassNotFoundException {ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();//创建输出流对象ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);//通过writeObject将获取到的对象转变为字节流objectOutputStream.writeObject(instance);//创建文件输入流对象//toByteArray()方法是将一个ByteArrayOutputStream对象转换为byte字符数组返回ObjectInputStream objectInputStream=new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));//通过readObject将转变后的字节流还原成对象System.out.println("反序列化创建实例:"+objectInputStream.readObject());}
}

输出:

这种反序列化的过程不仅可以产生新的对象,且并不实现构造方法

private Singleton1
otherMethod()
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Singleton1@7ef20235
Singleton1@7ef20235
反序列化创建实例:Singleton1@1a93a7ca

预防反序列化破坏单例:

解决办法为在Singleton1类中重写readResolve方法,使得它的返回值为初始创建的instance对象,而不是通过反序列化再创建新的对象

public Object readResolve(){return INSTANCE;}

Unsafe破坏单例:

 unsafe(Singleton1.class);private static void unsafe(Class<?> clazz) throws InstantiationException {Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);System.out.println("Unsafe 创建实例:" + o);
}

Unsafe是JDK内置的一个类,它并不是Java标准类,一般的开发者不会涉及此类的开发,它不需要调用构造函数,且这种破坏方式目前没有解决的办法

枚举类实现饿汉式单例:

enum_Sington类:

public enum enum_Sington {INSTANCE;private enum_Sington(){System.out.println("private enum_Sington()");}public static enum_Sington getInstance(){return INSTANCE;}@Overridepublic String toString() {return getClass().getName()+"@"+Integer.toHexString(hashCode());}public static void otherMethod(){System.out.println("otherMethod()");}
}

测试类:

import java.io.*;
import java.lang.reflect.InvocationTargetException;
public class Test_Sington2 {//单例模式-饿汉式public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException,IOException, ClassNotFoundException {enum_Sington.otherMethod();System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");System.out.println(enum_Sington.getInstance());System.out.println(enum_Sington.getInstance());}
}

输出:

private enum_Sington()
otherMethod()
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
enum_Sington@7ef20235
enum_Sington@7ef20235

然而对于枚举类,反序列化并不能将其破坏掉,验证如下:

在上述的测试类中加入反序列的代码,如下所示:

 serializable(enum_Sington.getInstance());private static void  serializable(Object instance) throws IOException, ClassNotFoundException {ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);//通过writeObject将获取到的对象转变为字节流objectOutputStream.writeObject(instance);ObjectInputStream objectInputStream=new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));//通过readObject将转变后的字节流还原成对象System.out.println("反序列化创建实例:"+objectInputStream.readObject());}

输出:

private enum_Sington()
otherMethod()
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
enum_Sington@7ef20235
enum_Sington@7ef20235
反序列化创建实例:enum_Sington@7ef20235

通过输出结果,我们会发现,反序列化创建的对象和上述枚举类创建的对象是同一个

那么反射可以破坏吗?

验证如下:

reflection(enum_Sington.class);
private static void reflection(Class<?> classz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {//通过getDeclaredConstructor获得私有构造Constructor<?> constructor=classz.getDeclaredConstructor();//通过setAccessible使得私有的构造方法也可以被使用constructor.setAccessible(true);System.out.println("反射创建实例:"+constructor.newInstance());}

设计模式之单例模式~

newInstance实例化对象只能调用无参构造方法(如果重写了一个带参构造方法,想要使用newInstance,则必须指定一个无参构造方法,否则会报初始化错误)

设计模式之单例模式~

懒汉式实现:

import java.io.Serializable;public class Singleton3 implements Serializable {private Singleton3(){System.out.println("private Singleton3()");}private static Singleton3 INSTACE =null;//开始将其置为null,当要使用它时,才创建该对象public static Singleton3 getInstance() {if (INSTACE == null) {INSTACE = new Singleton3();}return INSTACE;}public static void otherMethod() {System.out.println("otherMethod()");}
}
class Test_Singleton1 {public static void main(String[] args) {//INSTACE对象此时还不会被创建Singleton3.otherMethod();System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");//当调用getISTACE方法时,INSTACE对象才会被创建System.out.println(Singleton3.getInstance());System.out.println(Singleton3.getInstance());}
}
otherMethod()
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
private Singleton3()
Singleton3@7ef20235
Singleton3@7ef20235

但懒汉式会出现一个问题,也就是不加线程的保护下,多个线程都可以调用getINSTACE方法,那么当多线程调用这个方法时,就会出现下述问题:

线程1首次调用getINSATCE方法,发现INSTACE是null没有被创建,因此进行创建,当线程1执行到if语句中的代码时,线程二也调用该方法,假设此时线程1还没有完成将刚创建好的对象赋值给INSTACE的这个操作,就会出现当线程2调用该方法时,INSTACE显示还是为null,它也会创建新的INSTACE对象,此时就不能被称为是单例了

设计模式之单例模式~

这就是我们在多线程环境下执行单例存在的问题,那么该如何解决呢?

给getInstance方法加安全保护,也就是用sychronized进行修饰,其原理就是给该方法加锁

当加锁后的运行过程如下所示:

设计模式之单例模式~
设计模式之单例模式~

当线程2获得使用权后,进入该方法,发现INSTCE不为null,则不进行该对象的创建,直接返回前面线程1已经创建好的INSTCE对象,这样就能有效的避免多线程下单例模式的正确运行

但这样做的性能并不是最好的,原因是:我们加锁的目的就是为了防止INSTACE被多次创建,这样虽然能够解决首次仅由一个线程创建该对象,但当下次调用该方法的时候,加锁似乎没什么意义,因为对象已经被创建出来了

我们理想的目标是首次调用该方法加锁,而后续调用不加锁

优化方法——使用双检锁:

设计模式之单例模式~

注意:

设计模式之单例模式~

使用双检锁的INSTACE对象必须用volatile关键字修饰:主要利用内存屏障,保证有序性,在多线程下防止发生指令重排序

private static  volatile Singleton3 INSTACE =null;

内部类实现懒汉式单例:

发生在懒汉式中的多线程安全问题并没有出现在饿汉式实现单例中,原因是我们将其INSTACE对象的创建赋值给了static修饰的变量,而被static修饰的代码最终都会放在静态代码块中执行,而静态代码块中的线程安全并不需要我们去考虑,java虚拟机会帮我们保证其安全

那我们只需想办法将懒汉式中的INSTACE对象创建过程放在一个静态的代码块中即可,如下所示:

内部类也是符合懒汉式的特征,如果内部类没有被使用,那么类的加载链接初始化过程也不会发生,自然写在其中的INSTACE也不会被重复创建,那么什么时候会使用到它呢?

就是调用getInstace方法的时候,因此我们只需要将类的使用过程写在getInstace方法中

import java.io.Serializable;public class Singleton3 implements Serializable {private Singleton3(){System.out.println("private Singleton3()");}private static class Holder {static Singleton3 INSTACE=new Singleton3();}public static Singleton3 getInstance(){return Holder.INSTACE;}public static void otherMethod() {System.out.println("otherMethod()");}
}
class Test_Singleton1 {public static void main(String[] args) {//触发当前类的加载链接--导致该类的初始化操作被创建,当我们调用otherMethod()方法时,该类已经被创建,因为在输出的内容中,包含该类私有构造中的内容Singleton3.otherMethod();System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");//下面我们调用getInstance()方法时,得到的对象为上述已经创建好的对象System.out.println(Singleton3.getInstance());System.out.println(Singleton3.getInstance());}
}

上述使用内部类的懒汉式,也正是我们所推荐的,不仅符合懒汉式的特征,而且可以保证线程安全