单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。
通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。
参考文章:
什么是单例模式
单例模式指的是在应用整个生命周期内只能存在一个实例。单例模式是一种被广泛使用的设计模式。他有很多好处,能够避免实例对象的重复创建,减少创建实例的系统开销,节省内存。
单例模式和静态类的区别
首先理解一下什么是静态类,静态类就是一个类里面都是静态方法和静态field,构造器被private修饰,因此不能被实例化。Math类就是一个静态类。
知道了什么是静态类后,来说一下他们两者之间的区别:
- 首先单例模式会提供给你一个全局唯一的对象,静态类只是提供给你很多静态方法,这些方法不用创建对象,通过类就可以直接调用;
- 如果是一个非常重的对象,单例模式可以懒加载,静态类就无法做到;
那么时候时候应该用静态类,什么时候应该用单例模式呢?首先如果你只是想使用一些工具方法,那么最好用静态类,静态类比单例类更快,因为静态的绑定是在编译期进行的。如果你要维护状态信息,或者访问资源时,应该选用单例模式。还可以这样说,当你需要面向对象的能力时(比如继承、多态)时,选用单例类,当你仅仅是提供一些方法时选用静态类。
如何实现单例模式
饿汉式(线程安全,调用效率高,但是不能延时加载)
饿汉式就是立即加载,在类加载的时候已经产生。这种模式的缺点很明显,就是占用资源,当单例类很大的时候,其实我们是想使用的时候再产生实例。因此这种方式适合占用资源少,在初始化的时候就会被用到的类。但是饿汉式也有优点,就是线程安全,调用效率高。代码如下:
public class Singleton {
private static Singleton = new Singleton();
private Singleton() {}
public static getSignleton(){
return singleton;
}
}
懒汉式
懒汉式就是延迟加载,也叫懒加载。在程序需要用到的时候再创建实例,这样保证了内存不会被浪费。针对懒汉式,这里给出了4种实现方式,有些实现方式是线程不安全的,也就是说在多线程并发的环境下可能出现资源同步问题。
A) 单线程写法,用Thread.sleep(1000)模拟多线程并发
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
try {
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
return singleton;
}
}
测试类:
public class Test {
public static void main(String[] args) {
TestThred[] ThreadArr = new TestThred[10];
for (int i = 0; i < ThreadArr.length; i++) {
ThreadArr[i] = new TestThred();
ThreadArr[i].start();
}
}
}
class TestThred extends Thread{
@Override
public void run() {
System.out.println(Singleton.getInstance().hashCode());
}
}
结果:
1772827410
2057256033
2094435863
1474629390
739249834
1635036195
1152408485
429842367
197461654
1195510409
可以看到他们的hashCode不都是一样的,说明在多线程环境下,产生了多个对象,不符合单例模式的要求。
B) 使用synchronized关键字对getInstance方法进行同步 (线程安全,调用效率不高,但是能延时加载)
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
// 增加synchronized
public static synchronized Singleton getInstance() {
if (singleton == null) {
try {
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
return singleton;
}
}
结果:
652764634
652764634
652764634
652764634
652764634
652764634
652764634
652764634
652764634
652764634
虽然上面这种写法是可以正确运行的,但是效率太低,是同步运行的,下个线程想要取得对象,就必须要等上一个线程释放,才可以继续执行。因为每次调用getInstance()方法,都必须在synchronized这里进行排队,而真正遇到需要new的情况是非常少的。
C) 在实例化的时候加锁
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
try {
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 在这里加锁
synchronized(Singleton.class){
singleton = new Singleton();
}
}
return singleton;
}
}
结果:
1635036195
1073180601
2057256033
1745028020
963329488
377433899
2055569699
1474629390
1787454234
1523637982
从结果看来,这种方式不能保证线程安全,为什么呢?我们假设有两个线程A和B同时走到了‘代码1’,因为此时对象还是空的,所以都能进到方法里面,线程A首先抢到锁,创建了对象。释放锁后线程B拿到了锁也会走到‘代码2’,也创建了一个对象,因此多线程环境下就不能保证单例了。
D) 在同步代码块里面再一次做一下null判断,这种方式就是我们的DCL双重检查锁机制。 (线程安全,调用效率高,能延时加载,但是由于JVM底层模型原因,在jdk1.5版本前,会出问题)
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
try {
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(Singleton.class){
// 如果同时进来,当线程A抢到锁,创建对象后,线程B再进来,此时singleton对象就不为空了,就不会再创建新的对象
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
结果:
739249834
739249834
739249834
739249834
739249834
739249834
739249834
739249834
739249834
739249834
看似多此一举,但实际上却极大提升了并发度,进而提升了性能。为什么可以提高并发度呢?就像上文说的,在单例中new的情况非常少,绝大多数都是可以并行的读操作。因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,执行效率提高的目的也就达到了
静态内部类(线程安全,调用效率高,可以延时加载)
public class Singleton {
private static class Holder {
private static Singleton singleton = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return Holder.singleton;
}
}
结果:
378778546
378778546
378778546
378778546
378778546
378778546
378778546
378778546
378778546
378778546
可以看到使用这种方式我们没有显式的进行任何同步操作,那他是如何保证线程安全呢?和饿汉模式一样,是靠JVM保证类的静态成员只能被加载一次的特点,这样就从JVM层面保证了只会有一个实例对象。那么问题来了,这种方式和饿汉式又有什么区别呢?不也是立即加载么?实则不然,加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。
但是,上面提到的所有实现方式都有两个共同的缺点:
- 都需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。
- 可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
下面看一下序列化与反序列化的实现:
// 实现Serializable接口,开启序列化功能
public class Singleton implements Serializable{
private static final long serialVersionUID = 1L;
private static class Holder {
private static Singleton singleton = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return Holder.singleton;
}
}
主程序:
public class TestSerialize {
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());
System.out.println("====== 序列化 ======");
FileOutputStream fo = new FileOutputStream("tem");
ObjectOutputStream oo = new ObjectOutputStream(fo);
oo.writeObject(singleton);
oo.close();
fo.close();
System.out.println("====== 反序列化 ======");
FileInputStream fi = new FileInputStream("tem");
ObjectInputStream oi = new ObjectInputStream(fi);
Singleton Singleton2 = (Singleton) oi.readObject();
oi.close();
fi.close();
System.out.println(Singleton2.hashCode());
}
}
结果
1227229563
====== 序列化 ======
====== 反序列化 ======
1416233903
发现序列化和反序列化得到的对象的HashCode不一样,说明生成了一个新的实例。
解决办法就是在反序列化中使用readResolve()方法,使反序列化时,对象输出流不使用之前持久化的实例。
修改Singleton类如下:
public class Singleton implements Serializable{
private static final long serialVersionUID = 1L;
private static class Holder {
private static Singleton singleton = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return Holder.singleton;
}
protected Object readResolve(){
System.out.println("调用readResolve方法");
return Holder.singleton;
}
}
主程序不变,执行结果为:
1227229563
====== 序列化 ======
====== 反序列化 ======
调用readResolve方法
1227229563
查看ObjectInputStream
类,readUnshared()
方法有如下注释:
* Deserializing an object via readUnshared invalidates the stream handle
* associated with the returned object. Note that this in itself does not
* always guarantee that the reference returned by readUnshared is unique;
* the deserialized object may define a readResolve method which returns an
* object visible to other parties, or readUnshared may return a Class
* object or enum constant obtainable elsewhere in the stream or through
* external means. If the deserialized object defines a readResolve method
* and the invocation of that method returns an array, then readUnshared
* returns a shallow clone of that array; this guarantees that the returned
* array object is unique and cannot be obtained a second time from an
* invocation of readObject or readUnshared on the ObjectInputStream,
* even if the underlying data stream has been manipulated.
翻译如下:
通过readUnshared反序列化对象会使与返回对象关联的流句柄无效。 请注意,这本身并不总能保证readUnshared返回的引用是唯一的; 反序列化对象可以定义readResolve方法,该方法返回对其他方可见的对象,或者readUnshared可以返回可在流中的其他位置或通过外部方式获得的Class对象或枚举常量。 如果反序列化对象定义了readResolve方法,并且该方法的调用返回一个数组,那么readUnshared将返回该数组的浅层克隆; 这保证了返回的数组对象是唯一的,并且无法在ObjectInputStream上调用readObject或readUnshared时再次获取,即使已经操作了基础数据流。
简单来说,就是当对象定义了readResolve
方法时,JVM从内存中反序列化地"组装"一个新对象后,就会自动调用这个 readResolve方法来返回我们指定好的对象了, 单例规则也就得到了保证。readResolve()的出现允许程序员自行控制通过反序列化得到的对象。
枚举类(线程安全,调用效率高,不能延时加载,可以天然的防止反射和反序列化调用)
使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java推荐尽可能地使用枚举来实现单例。
public enum SingletonEnum {
// 枚举元素本身就是单例
INSTANCE;
// 添加自己需要的操作
public void singletonOperation() {
}
}
总结
如何选用:
- 单例对象占用资源少,不需要延时加载,枚举类 好于 饿汉式
- 单例对象占用资源多,需要延时加载,静态内部类 好于 懒汉式
最后,不管采取何种方案,请时刻牢记单例的三大要点:
- 线程安全
- 延迟加载
- 序列化与反序列化安全