前言:
今天开始将23种设计模式梳理一遍。
概述:
单例模式是创建者模式,它的主要关注点是"怎样创建对象",它的主要特点是将对象的创建和使用分离,这样可以降低系统的耦合度,使用者不要关注对象的创建细节。
单例模式是Java种最简单的设计模式之一,这种类型的设计模式属于创建者模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一种单一的类,该类创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
1.单例模式的结构
单例模式主要有以下两个角色:
单例类:只能创建一个实例的类
访问类:使用单例类
2.单例模式的实现
单例设计模式分为两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例被创建,而是首次使用该对象时才会被创建
2.1 饿汉式
2.1.1 饿汉式(静态变量)
/**
* 饿汉式:静态成员变量
*/
public class Singleton {
// 1.私有化构造方法
private Singleton() {}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
2.1.2 饿汉式(静态代码块)
// 饿汉式:静态代码块
public class Singleton {
private Singleton() {}
private static Singleton instance; // null
static {
instance = new Singleton();
}
public static Singleton getInstance() {
return instance;
}
}
说明:
饿汉式在类加载时就已经创建instance对象,如果后续一直不使用该对象的话,且该对象足够大的话,会造成内存的浪费..
2.1.3 饿汉式(枚举)
枚举类实现单例模式也是一个比较好的单例模式实现方式,因为枚举类是线程安全的,并且只会装载一次,设计者利用枚举这个特性去实现单例模式。
/**
* 枚举实现方式
*/
public enum Singleton {
INSTANCE;
}
2.2 懒汉式
2.2.1 懒汉式(线程不安全)
/**
* 懒汉式
*/
public class Singleton {
// 私有化构造方法
private Singleton() {}private static Singleton instance; // 为null
// 声明Singleton类型的变量Instance
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
说明:
判断为空和实例化操作分为两步 可能带来线程不安全的问题。
2.2.2 懒汉式(线程安全)
/**
* 懒汉式
*/
public class Singleton {
// 私有化构造方法
private Singleton() {}private static Singleton instance; // 为null
// 声明Singleton类型的变量Instance
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
说明:
加上synchronized同步锁使其变为一个同步方法
2.2.3 懒汉式(双重检查锁)
对于getInstance来说,大部分操作是读操作,没有必要抢锁,所以需要调整加锁的时机。
public class Singleton {
// 私有化构造方法
private Singleton() {
}
// 声明Signleton类型的变量
private static Singleton instance;
// 对外提供公共的访问方式
public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) { // 锁对象为当前类的字节码对象
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
说明:
双重检查锁模式是一个比较好的单例实现模式,解决了性能,单例,线程安全问题。但其实存在问题,即在多线程的情况下,可能出现空指针的问题,出现的原因是JVM在实例化对象的时候会进行优化和指令重排序。
2.2.4 懒汉式(volatile)
使用volatile解决指令重排序的问题
public class Singleton {
// 私有化构造方法
private Singleton() {
}
// 声明Signleton类型的变量(解决指令重排序的问题)
private static volatile Singleton instance;// 对外提供公共的访问方式
public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) { // 锁对象为当前类的字节码对象
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}}
2.2.5 懒汉式(静态内部类)
静态内部类的单例模式由内部类来创建,由于JVM在加载外部类的时候,是不会记载静态内部类的,只有内部类的属性/方法被调用的时候才会被加载,并初始化静态属性,静态属性由static修饰,确保只被实例化一次,并且严格保证实例化顺序。
public class Singleton {
// 私有构造方法
private Singleton() {}
// 定义一个静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 提供公共的访问方式
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
3.单例模式的破坏
3.1 序列化反序列化破坏单例模式
下面是单例模式的代码,在这我们使用的是懒汉式的静态内部类方式:
public class Singleton implements Serializable {
// 私有构造方法
private Singleton() {}// 定义一个静态内部类
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
// 提供公共的访问方式
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
我们先将这个Singleton对象写入文件,在读取出来,看看先后读取的是不是同一个对象,用于判断是否破坏了单例模式
public class Client {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//writeObject2File();
readObject2File();
readObject2File();
}public static void writeObject2File() throws IOException {
// 获取singleton对象
Singleton instance = Singleton.getInstance();
// 创建对象输出流输出对象
ObjectOutputStream ois = new ObjectOutputStream(new FileOutputStream("D://a.txt"));
ois.writeObject(instance);
ois.close();
}public static void readObject2File() throws IOException, ClassNotFoundException {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("D://a.txt"));
Singleton o = (Singleton)is.readObject();
System.out.println(o);
is.close();
}
}
结果如下:
可以看到两次使用readObject2File所取得对象的地址并不一样,说明单例模式已经被破坏。
3.2 通过反射破坏单例模式
Singleton还是用静态内部类的方式。这边我们使用反射分别创建两个Singleton对象,观察他们两的地址是否是相同的。
public class client {
public static void main(String[] args) throws Exception {
// 获取Singleton的字节码对象
Class<Singleton> singletonClass = Singleton.class;
// 获取无参的构造方法
Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
// 取消访问检查
declaredConstructor.setAccessible(true);
// 创建Singleton对象
Singleton singleton = declaredConstructor.newInstance();
Singleton singleton1 = declaredConstructor.newInstance();
System.out.println(singleton == singleton1);
}
}
结果如下:
由此看出反射破坏了单例模式。
4. 单例模式的解决方法
4.1 对序列化的解决方法
在Singleton类中添加readResolve()方法,在反序列时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new的对象。
public class Singleton implements Serializable {
// 私有构造方法
private Singleton() {}// 定义一个静态内部类
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
// 提供公共的访问方式
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}// 当进行反序列化,会自动调用该方法,将该方法的返回值直接返回
public Object readResolve(){
return SingletonHolder.INSTANCE;
}
}
再次进行文件读取,发现两次读取的对象地址是一样的:
4.2 对反射的解决方法
反射是根据私有的构造方法去创建对象的,我们在私有的构造方法去进行相应的操作逻辑:
public class Singleton implements Serializable {
private static boolean flag = false;
// 私有构造方法
private Singleton() {
synchronized (Singleton.class) {
if (flag) {
throw new RuntimeException("对象不能被重复创建");
}
// 将flag设置为true
flag = true;
}
}// 定义一个静态内部类
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
// 提供公共的访问方式
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
可以看到反射想要创建两个对象时,系统会抛出异常:
补充:
jdk中的单例模式,可以看到用的是饿汉式。
最后:
今天的分享就到这里。如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!(๑`・ᴗ・´๑)