内容简介:本文重要关注点:单例模式属于管理实例的创造型类型模式。单例模式保证在你的应用种最多只有一个指定类的实例。读取项目的配置信息的类可以做成单例的,因为只需要读取一次,且配置信息字段一般比较多节省资源。通过这个单例的类,可以对应用程序中的类进行全局访问。无需多次对配置文件进行多次读取。
本文重要关注点:
- 线程安全的单例模式
- 防止对象克隆破坏单例模式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方法。
-
多线程访问下,需要注意线程安全问题。
-
小心多重类加载器,也许会破坏你的单例类。
-
如果单例类是可序列化的,需要实现严格类型
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Design and Analysis of Distributed Algorithms (Wiley Series on P
Nicola Santoro / Wiley-Interscience / 2006-10-27 / USD 140.95
This text is based on a simple and fully reactive computational model that allows for intuitive comprehension and logical designs. The principles and techniques presented can be applied to any distrib......一起来看看 《Design and Analysis of Distributed Algorithms (Wiley Series on P》 这本书的介绍吧!