Java设计模式:23种设计模式(一) 单例模式

目录

什么是单例

单例的利与弊

单例模式实现

懒汉式单例

双重检查锁定——DCL(Double Check Lock)

饿汉式单例

懒汉式与饿汉式的不同

其他单例模式实现

静态内部类单例

枚举单例

登记式单例

破坏单例的方式及解决方法

反序列化

反射

克隆


什么是单例

单例模式(Singleton Pattern)被认为是最简单、最易理解的设计模式。但事实上,要用好、用对它并不是一件简单的事。

那么什么是单例模式呢?它用于产生一个类的实例,并确保系统中该类有且只有这一个实例

可能你会觉得没怎么接触过单例模式,其实我们日常使用的Windows系统就有很多地方应用单例模式:

  1. 任务管理器就是很典型的单例模式,你无法打开两个任务管理器窗口。
  2. 回收站也是单例应用,在系统运行过程中,回收站一直维护这仅有的一个实例。
  3. 时间计数器,系统需要保证运行过程中时间的唯一性。

 

单例的利与弊

在Java语言中,单例能为我们能带来两大好处:

  1. 对于频繁使用的对象,可以省略创建对象所花费的时间,尤其是那些重量级对象
  2. 由于new操作的次数减少,对系统内存的使用频率也会降低,这将减轻GC(垃圾回收)的压力

单例的弊端:

  1. 由于单例模式没有抽象层,使得单例类的扩展有很大的困难
  2. 单例类的职责过重,违背了“单一职责原则”
  3. 滥用单例将带来一些负面问题,比如实例化的对象长时间不使用,会被系统认为是垃圾而回收,导致对象状态的丢失

 

单例模式实现

单例模式有3个特点:

  1. 单例类只有一个实例对象
  2. 该实例对象只能由单例类自行创建
  3. 单例类需要对外提供一个访问该实例对象的全局访问点

用代码描述即:

  1. 私有化该类的构造方法
  2. 在本类中创建一个本类对象
  3. 定义一个公有方法,将在本类中创建的对象返回

单例模式通常有两种实现方式:懒汉式单例和饿汉式单例。

懒汉式单例

懒汉式单例的特点在‘懒’,什么是懒?懒就是能拖就拖,可以待会儿做的事情绝不在现在做。‘懒’这个字也道出了懒汉式单例的特点:只有在第一次被用到的时候才创建实例。因此懒汉式单例是延迟加载的。

public class LazySingleton {
	// 类加载时不创建实例
	private static LazySingleton instance;
	// 私有构造方法
	private LazySingleton() {}
	// 第一次调用此方法时,创建实例对象
	public static LazySingleton getInstance() {
		if (null == instance) {
			instance = new LazySingleton();
		}
		return instance;
	}

}

LazySingleton在单线程下可以保证只存在一个实例对象,但是在多线程中却无法保证实例对象的唯一性

public class TestLazySingletonByThread extends Thread {
	
	@Override
	public void run() {
		System.out.println("Thread: " + Thread.currentThread().getName() 
				+ ", " + LazySingleton.getInstance().hashCode());
	}

	public static void main(String[] args) {
		for(int i = 0; i < 10; i++) {
			new TestLazySingletonByThread().start();
		}
	}
	
}

打印结果如下(每次的打印结果会略有差异):

Thread: Thread-1, 1259307936
Thread: Thread-2, 693535270
Thread: Thread-8, 1259307936
Thread: Thread-5, 1259307936
Thread: Thread-6, 1259307936
Thread: Thread-4, 1259307936
Thread: Thread-3, 424549596
Thread: Thread-0, 1259307936
Thread: Thread-7, 1259307936
Thread: Thread-9, 1259307936

我们没有重写LazySingleton的hascode()方法,因此如果是同一个LazySingleton实例对象调用hascode()方法,返回的值肯定是一样的。然而打印结果中共出现3种不同的code值,这说明在10个线程中出现了3个LazySingleton实例。 

要解决这个问题很简单,我们只需要将getInstance()修改为同步方法即可:

public static synchronized LazySingleton getInstance()

但是这样又会带来性能问题——如果同时存在多个线程获取LazySingleton实例,那么在其中一个线程获取实例的时间内,其余线程都会进入等待状态。

因此我们不能同步整个getInstance()方法,只同步关键代码块。

public class LazySingletonV2 {
	
	private static LazySingletonV2 instance;
	
	private LazySingletonV2() {}
	
	public static LazySingletonV2 getInstance() {
		if (null == instance) {
			// 不同步方法,只同步关键代码块
			synchronized (LazySingletonV2.class) {
				instance = new LazySingletonV2();				
			}
		}
		return instance;
	}

}

在多线程中测试LazySingletonV2能否保证在程序运行时实例对象的唯一性。

public class TestLazySingletonV2ByThread extends Thread {
	
	@Override
	public void run() {
		System.out.println("Thread: " + Thread.currentThread().getName() 
				+ ", " + LazySingletonV2.getInstance().hashCode());
	}

	public static void main(String[] args) {
		for(int i = 0; i < 10; i++) {
			new TestLazySingletonV2ByThread().start();
		}
	}
	
}

打印结果如下(每次的打印结果会略有差异):

Thread: Thread-0, 702608182
Thread: Thread-9, 41609535
Thread: Thread-5, 41609535
Thread: Thread-6, 41609535
Thread: Thread-2, 1259307936
Thread: Thread-8, 956620190
Thread: Thread-7, 41609535
Thread: Thread-1, 1259307936
Thread: Thread-4, 1259307936
Thread: Thread-3, 1259307936

通过打印结果,我们可以发现在多线程中仍然创建了多个LazySingletonV2实例对象。之所以会出现这种情况,是因为如果多个线程同时到达同步代码块的位置,那么在进入同步代码块之前的判断instance是否为null就失去了作用。

两个线程调用如下:

TimeThread AThread B
T1检查到instance为null检查到instance为null
T2获取锁等待
T3为instance分配实例对象(第一个实例)等待
T4释放锁获取锁
T5 为instance分配实例对象(第二个实例)
T6 释放锁

双重检查锁定——DCL(Double Check Lock)

针对上面的情况,我们需要在同步代码块中再一次对instance判null,也就是所谓的“双重检查锁定”。

public class LazySingletonV3 {
	
	private static LazySingletonV3 instance;
	
	private LazySingletonV3() {}
	
	public static LazySingletonV3 getInstance() {
		if (null == instance) {
			synchronized (LazySingletonV3.class) {
				// 再一次对instance判null
				if (null == instance) {
					instance = new LazySingletonV3();
				}
			}
		}
		return instance;
	}

}

使用双重检查锁定基本上就可以解决懒汉式单例在多线程中问题。为什么要说“基本上”呢?上述写法看似解决了问题,实则有个很大的隐患——实例化对象的那行代码(instance = new LazySingletonV3()),实际上可以分解成以下三个步骤:

  1. 为实例对象分配内存空间
  2. 初始化对象
  3. 将引用指向刚分配的内存空间

但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就变成:

  1. 为实例对象分配内存空间
  2. 将引用指向刚分配的内存空间
  3. 初始化对象

考虑重排序后,两个线程发生了以下调用:

TimeThread AThread B
T1检查到instance为null 
T2获取锁 
T3再次检查到instance为null 
T4为实例对象分配内存空间 
T5将instance指向内存空间 
T6 检查到instance不为null
T7 返回并访问instance(此时实例对象还未完成初始化)
T8初始化实例对象 
T9释放锁 

为了解决上述问题,需要用关键字volatile修饰instance。使用volatile修饰后,重排序被禁止,所有对instance的写操作都将发生在读操作之前。至此双重检查锁定就可以正常运行了。

饿汉式单例

饿汉式单例也很容易理解,饥饿的人肯定急着要吃饭,这种单例模式的特点体现在‘急’上,它会在类初始化(调用类中静态方法)时就完成对象的实例化。由于JVM会保证类的初始化只会进行一次,因此饿汉式单例模式避免了多线程的同步问题

public class HungrySingleton {

	private static final HungrySingleton instance = new HungrySingleton();
	
	private HungrySingleton() {}
	// 调用该方法会初始化HungrySingleton类
	public static HungrySingleton getInstance() {
		return instance;
	}

}

:用final关键字修饰instance并不是必须的,只是因为单例类的实例对象只需要创建一次,所以用final修饰更为严谨。

我在网上查阅很多资料,其中很多都认为饿汉式单例在类加载时创建实例对象,会占用内存资源,但事实并不是这样。

类加载分为三个阶段:加载、连接、初始化,在初始化阶段才会为静态变量赋初始值。类的加载由JVM的具体实现决定,但是类的初始化只有出现下面7种情况之一才会发生:

  1. 创建类的实例
  2. 访问某个类、接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射
  5. 初始化一个类的子类
  6. JVM启动时被标记为启动类的类(包含main()方法)
  7. JDK1.7开始提供动态语言支持(很少使用)

很明显,想要初始化HungrySingleton类,正常情况下只有通过调用该类中的getInstance()方法才可以。因此如果不调用单例类对外暴露的静态方法,就不会创建HungrySingleton实例对象,也就不存在占用内存资源一说。

换而言之,饿汉式和懒汉式一样,都是延迟加载的。

懒汉式与饿汉式的不同

在饿汉式单例中,我提到饿汉式和懒汉式都是延迟加载的,而且饿汉式还不用担心多线程的同步问题,那么懒汉式还有什么存在的必要呢?

作为一个类应该要纯粹,然而现实往往不是这样。假如懒汉式单例类LazySingleton和饿汉式单例类HungrySingleton中除了获取单例对象的静态方法getInstance(),还存在下面的静态方法:

public static void sayHello() {
	System.out.println("hello");
}

分别通过两个类调用该方法,那么LazySingleton类并不会创建实例对象,而HungrySingleton类会创建实例对象。

所以在这种情况下,懒汉式依然可以保持延迟加载,等到调用getInstance()方法时才会创建实例对象;而饿汉式则会立刻创建实例对象,出现浪费内存资源的情况。

 

其他单例模式实现

除了上述两种主要的单例实现方式,还有其他几种单例的实现方式。

静态内部类单例

静态内部类单例可以看做是懒汉式单例的一种变式。这种单例模式一方面具有懒汉式单例延迟加载的特性,另一方面又避免了懒汉式单例在多线程中的同步问题。可谓“取其精华,去其糟粕”,该实现是比较推荐的一种做法。

静态内部类单例解决多线程同步问题的方式和饿汉式单例一样,利用JVM只会初始化类一次,使用静态内部类的静态成员变量记录单例类的实例对象。

public class StaticInnerClassSingleton {
	
	private StaticInnerClassSingleton() {}
	
	public static StaticInnerClassSingleton getInstance() {
		return InstanceHolder.instance;
	}
	
	private static class InstanceHolder {
		private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
	}

}

枚举单例

枚举类型是有“实例控制”的类,确保不会同时存在两个实例,也就是说在任何情况下都是单例。枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很好。

public enum EnumSingleton {

	INSTANCE;
	
	public void doSomething() {
		// TODO
	}
	
}

登记式单例

登记式单例的作用是将多种单例类统一管理,使用时根据相应的key获取对应的单例对象。这种实现方式的好处是在对用户隐藏具体实现、降低代码耦合度的同时,降低了用户的使用成本。简易版代码实现如下:

public class SingletonManger {
	
	private static Map<String, Object> objMap = new HashMap<>();
	
	private SingletonManger() {}
	
	public static void registerService(String key, Object instance) {
		if (objMap.get(key) != null) {
			objMap.put(key, instance);
		}
	}
	
	public static Object getService(String key) {
		return objMap.get(key);
	}
	
}

 

破坏单例的方式及解决方法

诚然我们的本意是希望通过单例模式保证系统中只存在一个实例对象,但是世界上没就没有什么完美的东西,单例模式也并不完美,下面几种方式就可以破坏单例。(:以下几种破坏单例的方式对枚举单例无效。)

反序列化

我们知道,序列化可以将一个实例对象写到磁盘,实现数据的持久化,也能实现实例对象数据的远程传输。

那么如果单例类实现Serializable接口,即使私有化它的构造方法,在反序列化单例对象时依然会通过特殊的途径再创建一个新的实例对象,相当于调用了单例类的构造方法获得一个新实例。见下面一个例子:

public class SerSingleton implements Serializable {
    
	private static final long serialVersionUID = 417892990361624082L;
	
	private static SerSingleton instance = new SerSingleton();
	
	private SerSingleton() {}
	
	public static SerSingleton getInstance() {
		return instance;
	}
	
	public static void main(String[] args) {
		// 通过SerSingleton提供的getInstance()方法获取实例
		SerSingleton singleton = SerSingleton.getInstance();
		SerSingleton singleton2 = SerSingleton.getInstance();
		System.out.println("singleton == singleton2: " + (singleton == singleton2));
		try {
			// 序列化SerSingleton实例
			ObjectOutputStream oos = new ObjectOutputStream(
					new FileOutputStream("serfile/SerSingleton.ser"));
			oos.writeObject(singleton);
			oos.close();
			// 反序列化
			ObjectInputStream ois = new ObjectInputStream(
					new FileInputStream("serfile/SerSingleton.ser"));
			SerSingleton deserializeSingleton = (SerSingleton) ois.readObject();
			ois.close();
			// 反序列化结果与实例对比
			System.out.println("singleton == deserializeSingleton: " 
					+ (singleton == deserializeSingleton));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
}

打印结果如下:

singleton == singleton2: true
singleton == deserializeSingleton: false

通过打印结果我们发现,反序列化可以破坏单例模式。所以为了避免单例对象在反序列化时重新生成对象,需要在实现Serializable接口的同时实现readResolve()方法(定义于ObjectStreamClass类中),以保证反序列化的时候获得原来的对象。

readResolve()是反序列化操作提供的一个钩子函数,它在从流中读取对象的readObject() 方法执行之后被调用,可以让开发人员控制对象的反序列化。我们只需要在readResolve()方法中用instance替换掉从流中读取后新创建的实例对象,就可以避免使用序列化对单例模式的破坏。

在SerSingleton类中加入readResolve()方法:

private Object readResolve() {
	return instance;
}

再次运行程序,打印结果如下:

singleton == singleton2: true
singleton == deserializeSingleton: true

反射

除了反序列化,反射也可以破坏单例。反射可以通过setAccessible(true)使权限检查失效,从而调用单例类的私有构造方法达到创建对象的目的。

public class ReflectSingleton {
   
	private static ReflectSingleton instance = new ReflectSingleton();
	
	private ReflectSingleton() {}

	public static ReflectSingleton getInstance() {
		return instance;
	}
	
	public static void main(String[] args) {
		// 通过ReflectSingleton提供的getInstance()方法获取实例
		ReflectSingleton singleton = ReflectSingleton.getInstance();
		ReflectSingleton singleton2 = ReflectSingleton.getInstance();
		System.out.println("singleton == singleton2: " + (singleton == singleton2));
		try {
			Constructor<ReflectSingleton> constructor = 
					ReflectSingleton.class.getDeclaredConstructor();
			// 通过反射使权限检查失效
			constructor.setAccessible(true);
			ReflectSingleton reflectSingleton = constructor.newInstance();
			// 反射生成实例与单例实例对比
			System.out.println("singleton == reflectSingleton: " 
					+ (singleton == reflectSingleton));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
}

打印结果如下:

singleton == singleton2: true
singleton == reflectSingleton: false

为了防止这种情况的发生,我们需要改进单例类的构造方法。在单例类的构造方法中对instance进行判null处理,一旦有第二次创建单例对象的行为发生,就抛出异常(:在构造方法中只能抛出非受检查异常,即RuntimeException及其子类)。

对ReflectSingleton的构造方法进行改造:

private ReflectSingleton() {
	if (null != instance) {
		throw new RuntimeException("cannot create instance more");
	}
}

再次运行程序,打印结果如下:

singleton == singleton2: true
java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at org.hu.singleton.ReflectSingleton.main(ReflectSingleton.java:29)
Caused by: java.lang.RuntimeException: cannot create instance more
    at org.hu.singleton.ReflectSingleton.<init>(ReflectSingleton.java:11)
    ... 5 more

克隆

clone()方法定义于Object类中,我们知道Java中每个类都是Object的子类,所以单例类也继承了clone()方法。clone()方法并不是通过调用构造方法创建对象,而是直接拷贝内存区域。因此若单例类实现了Cloneable接口,尽管其构造方法是私有的,仍然可以通过clone()方法创建一个新的单例实例,从而让单例模式失效。

public class CloneSingleton implements Cloneable {
	
	private static CloneSingleton instance = new CloneSingleton();
	
	private CloneSingleton() {}
	
	public static CloneSingleton getInstance() {
		return instance;
	}
	
	public static void main(String[] args) {
		// 通过CloneSingleton提供的getInstance()方法获取实例
		CloneSingleton singleton = CloneSingleton.getInstance();
		CloneSingleton singleton2 = CloneSingleton.getInstance();
		System.out.println("singleton == singleton2: " + (singleton == singleton2));
		try {
			// 通过clone()创建CloneSingleton实例
			CloneSingleton cloneSingleton = (CloneSingleton) singleton.clone();
			// 反序列化结果与实例对比
			System.out.println("singleton == cloneSingleton: " 
			+ (singleton == cloneSingleton));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
}

打印结果如下:

singleton == singleton2: true
singleton == cloneSingleton: false

 针对这种情况,我们只需要重写clone()方法即可。在CloneSingleton类中添加下面的代码:

@Override
protected Object clone() throws CloneNotSupportedException {
	throw new CloneNotSupportedException();
}

可能你会觉得奇怪——为什么要大费周章的复写clone()方法,不让单例类实现Cloneable接口不就可以了吗?确实作为一个单例类不应该实现Cloneable接口,但是虽然很少见,有时单例类会存在父类。如果单例类的父类实现了clone()方法,那么我们就需要在单例类中复写clone()方法避免对单例模式的破坏。

 

参考:

单例模式(单例设计模式)详解

设计模式(二)单例模式的七种写法

Java中的双重检查锁(double checked locking)

面试官所认为的单例模式

那些年,我们一起写过的“单例模式”

[改善Java代码]不要在构造函数中抛出异常

别再用懒汉模式了——从JVM的角度看单例模式

深入理解单例模式:静态内部类单例原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java设计模式是一套经过总结和整理的编程思想,用于解决软件开发中常见的问题。《Java设计模式23种设计模式全面解析(超级详细)》是一本详细介绍这23种设计模式的书籍,它可以帮助读者深入理解每种设计模式的原理和用法。 这本书分为23个章节,每个章节介绍一种设计模式。每个章节都从问题的背景出发,描述该设计模式的目标和应用场景。然后,通过实例代码和图形示意来展示如何使用该设计模式解决问题。 书中详细介绍了创建型设计模式、结构型设计模式和行为型设计模式。在创建型设计模式方面,包括单例模式、工厂模、抽象工厂模、建造者模和原型模等。结构型设计模式包括适配器模、装饰器模、代理模、组合模和桥接模等。行为型设计模式包括策略模、观察者模、迭代器模、命令模和模板方法模等。 每个设计模式都有其特定的使用场景和适用性,读者可以根据自己的需求选择合适的设计模式来解决问题。通过学习这本书,读者可以深入理解设计模式的原理和思想,并且能够应用到实际的软件开发中。 总的来说,《Java设计模式23种设计模式全面解析(超级详细)》是一本对于Java开发者来说非常有价值的书籍。它通过详细的示例和解释,帮助读者深入理解23种设计模式的使用方法,使读者能够灵活地应用设计模式解决实际开发中遇到的问题。无论是初学者还是有一定经验的开发者,都可以从这本书中受益匪浅。 ### 回答2: 《Java设计模式23种设计模式全面解析(超级详细)》是一本介绍Java设计模式的全面解析书籍。设计模式是软件工程领域中的一种解决问题的思维方式和经验总结,能够提供可复用的解决方案。这本书详细介绍了23种经典的设计模式,包括创建型、结构型和行为型三种类型的模。 在创建型设计模式方面,书中包含了单例模式、原型模、工厂方法模、抽象工厂模、建造者模。这些模都是用来创建对象的,通过不同的实现方式能够灵活地创建对象,并且符合原则,如单一职责原则、开闭原则等。 在结构型设计模式方面,书中介绍了适配器模、装饰器模、代理模、外观模等。这些模通过组合不同的类和对象,来解决不同类间关系的问题,增加了程序的可扩展性和灵活性。 在行为型设计模式方面,书中讲解了观察者模、模板方法模、策略模、命令模等。这些模着重于对象之间的通信和协作,通过定义不同的行为和规则,让对象能够更好地进行交互,降低了对象间的耦合度。 此外,书中还介绍了其他几种分类的设计模式,如迭代器模、访问者模、备忘录模等,这些模在特定的应用场景中发挥着重要作用。 总的来说,《Java设计模式23种设计模式全面解析(超级详细)》是一本详细介绍了Java设计模式的书籍,对于想深入了解和应用设计模式Java开发人员来说,是一本很好的资料,能够帮助他们理解,并在实际项目中应用这些经典的设计模式。 ### 回答3: Java设计模式是一套被广泛应用于软件设计的规范和经验总结。设计模式可以提供可重用和可维护的代码,能够帮助开发人员解决常见的软件设计问题。 Java设计模式一共有23种,分为创建型模、结构型模和行为型模三个类别。 创建型模包括单例模式、工厂模、抽象工厂模、建造者模和原型模单例模式确保一个类只有一个实例,工厂模对象的创建委托给工厂类,抽象工厂模允许客户端使用抽象接口来创建一组相关对象,建造者模通过一步步构建复杂对象,原型模通过克隆已有对象来创建新对象。 结构型模包括适配器模、桥接模、组合模、装饰器模、外观模、享元模和代理模。适配器模将一个类的接口转换成客户端期望的接口,桥接模将抽象和实现分离,组合模对象组合成树形结构以表示“部分-整体”的层次结构,装饰器模动态地给对象添加职责,外观模为多个子系统提供一个统一的接口,享元模共享对象以减少内存的使用,代理模为其他对象提供一个代理以控制对这个对象的访问。 行为型模包括模板方法模、命令模、迭代器模、观察者模、中介者模、备忘录模、解释器模、状态模、策略模、职责链模和访问者模。模板方法模定义了一个操作中的算法骨架,而将一些步骤延迟到子类中实现,命令模将请求封装成对象,迭代器模提供一种方法来顺序访问聚合对象的元素,观察者模定义对象之间的一对多依赖关系,中介者模定义了一个封装一组对象交互的对象,备忘录模在不破坏封装的前提下捕获一个对象的内部状态,解释器模为语言创建解释器,状态模允许一个对象在其内部状态改变改变其行为,策略模定义了一系列算法,职责链模将请求的发送者和接收者解耦,访问者模将算法与数据结构分离开来。 这些设计模式在实际的软件开发中有着广泛的应用,对于提高代码的可重用性、可维护性和可扩展性都具有很好的作用。了解和熟练运用这些设计模式,对于Java开发人员来说是非常重要的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值