内容简介:本文重要关注点:单例模式属于管理实例的创造型类型模式。单例模式保证在你的应用种最多只有一个指定类的实例。读取项目的配置信息的类可以做成单例的,因为只需要读取一次,且配置信息字段一般比较多节省资源。通过这个单例的类,可以对应用程序中的类进行全局访问。无需多次对配置文件进行多次读取。
本文重要关注点:
- 线程安全的单例模式
- 防止对象克隆破坏单例模式Singleton
- 防止序列化破坏单例模式
单例模式
什么是单例模式
单例模式属于管理实例的创造型类型模式。单例模式保证在你的应用种最多只有一个指定类的实例。
单例模式应用场景
- 项目配置类
读取项目的配置信息的类可以做成单例的,因为只需要读取一次,且配置信息字段一般比较多节省资源。通过这个单例的类,可以对应用程序中的类进行全局访问。无需多次对配置文件进行多次读取。
- 应用日志类
日志器Logger在你的应用中是无处不在的。也应该只初始化一次,但是可以到处使用。
- 分析和报告类
如果你在使用一些数据分析 工具 例如Google Analytics。你就可以注意到它们被设计成单例的,仅仅初始化一次,然后在用户的每一个行为中都可以使用。
实现单例模式的类
-
将默认的构造器设置为private。阻止其他类从应用中直接初始化该类。
-
创建一个public static 的静态方法。该方法用于返回一个单例类实例。
-
还可以选择懒加载初始化更友好。
示例代码
示例代码参见以下类
- org.byron4j.cookbook.designpattern.singleton.Singleton
public class Singleton { private static Singleton instance; // 构造器私有化 private Singleton(){ } // 提供静态方法 public static Singleton getInstance(){ // 懒加载初始化,在第一次使用时才创建实例 if(instance == null){ instance = new Singleton(); } return instance; } public void display(){ System.out.println("Hurray! I am create as a Singleton!"); } } 复制代码
单元测试类:
package org.byron4j.cookbook.designpattern; import org.byron4j.cookbook.designpattern.singleton.Singleton; import org.junit.Test; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SingletonTest { @Test public void test(){ final Set<Singleton> sets = new HashSet<>(); ExecutorService es = Executors.newFixedThreadPool(10000); for(int i = 1; i <= 100000; i++){ es.execute(new Runnable() { public void run(){ Singleton s = Singleton.getInstance(); sets.add(s); } }); } System.out.println(sets); } } 复制代码
运行输出如下,结果生成了多个Singleton实例:
[org.byron4j.cookbook.designpattern.singleton.Singleton@46b91344, org.byron4j.cookbook.designpattern.singleton.Singleton@1f397b96]
线程安全的单例模式
线程安全对于单例类来说是非常重要的。上述Singleton类是非线程安全的,因为在线程并发的场景下,可能会创建多个Singleton实例。
为了规避这个问题,我们可以将 getInstance 方法用同步字 synchronized 修饰,这样迫使线程等待直到前面一个线程执行完毕,如此就避免了同时存在多个线程访问该方法的场景。
public static synchronized Singleton getInstance() { // Lazy initialization, creating object on first use if (instance == null) { instance = new Singleton(); } return instance; } 复制代码
这样确实解决了线程安全的问题。但是, synchronized
关键字存在严重的性能问题。我们还可以进一步优化 getInstance 方法,将实例同步,将方法范围缩小:
public static Singleton getInstance() { // Lazy initialization, creating object on first use if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } 复制代码
单元测试三种方式耗时比较:
package org.byron4j.cookbook.designpattern; import org.byron4j.cookbook.designpattern.singleton.Singleton; import org.byron4j.cookbook.designpattern.singleton.SingletonSynchronized; import org.byron4j.cookbook.designpattern.singleton.SingletonSynchronizedOptimized; import org.junit.Test; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SingletonTest { @Test public void test(){ final Set<Singleton> sets = new HashSet<>(); long startTime = System.currentTimeMillis(); ExecutorService es = Executors.newFixedThreadPool(10000); for(int i = 1; i <= 100000; i++){ es.execute(new Runnable() { public void run(){ Singleton s = Singleton.getInstance(); sets.add(s); } }); } System.out.println("test用时:" + (System.currentTimeMillis() - startTime)); System.out.println(sets); } @Test public void testSynchronized(){ final Set<SingletonSynchronized> sets = new HashSet<>(); long startTime = System.currentTimeMillis(); ExecutorService es = Executors.newFixedThreadPool(10000); for(int i = 1; i <= 100000; i++){ es.execute(new Runnable() { public void run(){ SingletonSynchronized s = SingletonSynchronized.getInstance(); sets.add(s); } }); } System.out.println("testSynchronized用时:" + (System.currentTimeMillis() - startTime)); System.out.println(sets); } @Test public void testOptimised(){ final Set<SingletonSynchronizedOptimized> sets = new HashSet<>(); long startTime = System.currentTimeMillis(); ExecutorService es = Executors.newFixedThreadPool(10000); for(int i = 1; i <= 100000; i++){ es.execute(new Runnable() { public void run(){ SingletonSynchronizedOptimized s = SingletonSynchronizedOptimized.getInstance(); sets.add(s); } }); } System.out.println("testOptimised用时:" + (System.currentTimeMillis() - startTime)); System.out.println(sets); } } 复制代码
运行测试用例,输出如下:
test用时:1564 [org.byron4j.cookbook.designpattern.singleton.Singleton@68eae58e] testSynchronized用时:3658 [org.byron4j.cookbook.designpattern.singleton.SingletonSynchronized@36429a46] testOptimised用时:2254 [org.byron4j.cookbook.designpattern.singleton.SingletonSynchronizedOptimized@21571826] 复制代码
可以看到,最开始的实现方式性能是最好的,但是是非线程安全的; Synchronized 锁住整个getInstance方法,可以做到线程安全,但是性能是最差的; 缩小Synchronized范围,可以提高性能。
单例Singleton和对象克隆
涉及单例类时还要注意clone方法的正确使用:
package org.byron4j.cookbook.designpattern.singleton; /** * 单例模式实例 * 1. 构造器私有化 * 2. 提供静态方法供外部获取单例实例 * 3. 延迟初始化实例 */ public class SingletonZClone implements Cloneable{ private static SingletonZClone instance; // 构造器私有化 private SingletonZClone(){ } // 提供静态方法 public static SingletonZClone getInstance(){ // 将同步锁范围缩小,降低性能损耗 if(instance == null){ synchronized (SingletonZClone.class){ if(instance == null){ instance = new SingletonZClone(); } } } return instance; } /** * 克隆方法--改为public * @return * @throws CloneNotSupportedException */ @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } public void display(){ System.out.println("Hurray! I am create as a SingletonZClone!"); } } 复制代码
默认情况下clone时protected修饰的,这里改为了public修饰,测试用例如下:
@Test public void testClone() throws CloneNotSupportedException { SingletonZClone singletonZClone1 = SingletonZClone.getInstance(); SingletonZClone singletonZClone2 = SingletonZClone.getInstance(); SingletonZClone singletonZClone3 = (SingletonZClone)SingletonZClone.getInstance().clone(); System.out.println(singletonZClone1 == singletonZClone2); System.out.println(singletonZClone1 == singletonZClone3); System.out.println(singletonZClone2 == singletonZClone3); } 复制代码
输出如下:
true
false
false
我们了解一下clone方法的API解释, clone 后的对象虽然属性值可能是一样的,但是已经不是同一个对象实例了:
x.clone() != x x.clone().getClass() == x.getClass() x.clone().equals(x)
clone方法返回一个被克隆对象的实例的副本,除了内存地址其他属性值都是一样的,所以副本和被克隆对象不是同一个实例。 可以看出clone方法破坏了单例类,为防止该问题出现,我们需要禁用clone方法,直接改为:
/** * 克隆方法--改为public * @return * @throws CloneNotSupportedException */ @Override public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } 复制代码
单例和序列化问题
Java序列化机制允许将一个对象的状态转换为字节流,就可以很容易地存储和转移。 一旦对象被序列化,你就可以对其进行反序列化--将字节流转为对象。 如果一个Singleton类被序列化,则可能创建重复的对象。 我们可以使用钩子hook,来解释这个问题。
在 Java 规范中有关于readResolve()方法的介绍:
对于可序列化的和外部化的类,readResolve() 方法允许一个类可以替换/解析从流中读取到的对象。 通过实现 readResolve 方法,一个类就可以直接控制反序列化后的实例以及类型。 定义如下:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException; 复制代码
readResolve 方法会在ObjectInputStream 从流中读取一个对象时调用。ObjectInputStream 会检测类是否定义了 readResolve 方法。 如果 readResolve 方法定义了,会调用该方法用于指定从流中反序列化后作为返回的结果对象。 返回的类型要与原对象的类型一致,不然会出现 ClassCastException。
@Test public void testSeria() throws Exception { SingletonZCloneSerializable singletonZClone1 = SingletonZCloneSerializable.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser")); oos.writeObject(singletonZClone1); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser")); SingletonZCloneSerializable test = (SingletonZCloneSerializable) ois.readObject(); ois.close(); System.out.println(singletonZClone1 == test); } 复制代码
测试输出: false; 说明反序列化的时候已经不是原来的实例了,如此会破坏单例模式。
所以我们可以覆盖 readResolve 方法来解决序列化破坏单例的问题:
类 SingletonZCloneSerializableReadResolve 增加 readResolve 方法:
/** * 反序列化时返回instance实例,防止破坏单例模式 * @return */ protected Object readResolve(){ return getInstance(); } 复制代码
执行测试用例:
@Test public void testSReadResolve() throws Exception { s = SingletonZCloneSerializableReadResolve.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser")); oos.writeObject(s); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser")); SingletonZCloneSerializableReadResolve test = (SingletonZCloneSerializableReadResolve) ois.readObject(); ois.close(); System.out.println(s == test); } 复制代码
输出true,有效防止了反序列化对单例的破坏。
你知道吗?
-
单例类是很少使用的,如果你要使用这个设计模式,你必须清楚的知道你在做什么。因为全局范围内仅仅创建一个实例,所以在资源受约束的平台是存在风险的。
-
注意对象克隆。 单例模式需要仔细检查并阻止clone方法。
-
多线程访问下,需要注意线程安全问题。
-
小心多重类加载器,也许会破坏你的单例类。
-
如果单例类是可序列化的,需要实现严格类型
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 设计模式——订阅模式(观察者模式)
- 设计模式-简单工厂、工厂方法模式、抽象工厂模式
- java23种设计模式-门面模式(外观模式)
- 设计模式-享元设计模式
- Java 设计模式之工厂方法模式与抽象工厂模式
- JAVA设计模式之模板方法模式和建造者模式
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。