单例模式的定义如下:
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例类自身保存它的唯一实例,这个类保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。
单例模式的一些特点:
构造方法私有化,防止外部通过访问构造方法创建对象;
提供一个全局方法使其单例对象被外部访问;
考虑多线程并发情况的单例唯一性。
单例模式的几种实现方式:
懒汉式、饿汉式、内部类、注册式、枚举类
这里强烈推荐的是内部类和枚举类的实现方式。
饿汉式
在类加载的时候就立即创建单例对象。
- 优点:绝对的线程安全,无锁,执行效率高;
- 缺点:即使单例在程序中一直用不到,也会在类加载的时候初始化,不管用或不用,都占据内存空间。
package com.faith.net;
/**
* 饿汉式
*/
public class Singleton {
private Singleton(){}
private static final Singleton hungry = new Singleton();
public static Singleton getInstance(){
return hungry;
}
}
懒汉式
当需要使用单例的时候才进行实例化。
package com.faith.net;
/**
* 懒汉式
*/
public class Singleton {
private Singleton(){}
private static Singleton lazy = null;
public static Singleton getInstance(){
if(lazy == null){
lazy = new Singleton();
}
return lazy;
}
}
需要注意的是以上获取单例不是线程安全的。
- 用synchronized改版
package com.faith.net;
/**
* 懒汉式
*/
public class Singleton {
private Singleton(){}
private static Singleton lazy = null;
public static synchronized Singleton getInstance(){
if(lazy == null){
lazy = new Singleton();
}
return lazy;
}
}
通过synchronized关键字保证了线程安全,但这种排队方式的处理在多线情况下效率较低。
双重锁定
双重锁定是懒汉式的一种实现方式,上面例子中synchronized关键字加在了方法上,导致了每个线程都会排队获取对象。
双重锁定的方式是不让线程每次都加锁,而是只在实例未被创建的情况下加锁,同时也能保证线程安全,这种做法称为双重锁定(Double-Check Locking)。
示例如下:
package com.faith.net;
import java.io.Serializable;
/**
* 单例类
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve() {
return singleton;
}
}
内部类
这种方式使用了内部类的一些特性:
- 内部类只有在外部类被调用的时候才会被加载;
- 内部类在方法调用之前完成初始化;
- 其次,类的加载机制保证了每个类只会被加载一次。
这是非常推荐的一种方式,它综合了懒汉式延迟加载和饿汉式线程安全的特性。
package com.faith.net;
/**
* 内部类方式
*/
public class Singleton {
private boolean initialized = false;
private Singleton(){}
/*
* static 保证单例共享,final保证方法不被重写
*/
public static final Singleton getInstance(){
return LazyHolder.LAZY; // 在返回结果以前,会先加载内部类
}
// 内部类
private static class Singleton{
private static final Singleton LAZY = new Singleton();
}
}
注册式
注册式是spring IOC容器使用的一种方式,通过将实例保存到Map容器中实现。
package com.faith.net;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 注册式
*/
public class BeanFactory {
private BeanFactory(){}
private static Map<String,Object> container = new ConcurrentHashMap<String,Object>();
public static synchronized Object getBean(String className){
if(!ioc.containsKey(className)){
Object obj = Class.forName(className).newInstance();
container.put(className,obj);
return obj;
}else{
return container.get(className);
}
}
}
枚举类
枚举有如下特性:
- 枚举对象,例如下面的INSTANCE由枚举机制保证了一定会是单例的;
- 枚举的加载机制保证了线程安全;
- 枚举保证了实例不会被反射破坏;
- 枚举的对象只有被使用时才会进行实例化,保证了延迟加载。
所以通过枚举实现,也会保证代码的高效、线程安全以及延迟加载的特性。
实现方式
- 返回自身对象
package com.faith.net;
public enum Singleton {
INSTANCE; // 单例对象
// 这里可以添加多个成员方法,例如
public void doSomething() {
// 省略具体代码
}
}
- 返回其他对象,例如ConcurrentHashMap
package com.faith.net;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public enum Singleton {
INSTANCE; // 枚举对象,由于枚举机制,这个也是单例的
private Map instance; // 需要做成单例的对象
Singleton() {
instance = new ConcurrentHashMap();
}
public Map getInstance() {
return instance;
}
}
反序列化对单例的破坏
对象的序列化指将对象转化为字节流;反序列化指将字节流转化为相应的对象。
反序列化的特点是,默认情况下,会根据字节流创建一个新的对象。那么这种特性会导致破坏单例。如下:
- 创建单例类
package com.faith.net;
import java.io.Serializable;
/**
* 单例类
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
- 客户端测试反序列化
package com.faith.net;
import java.io.*;
public class SerializableDemo1 {
public static void main(String[] args) throws Exception {
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(Singleton.getSingleton());
// 反序列化
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// 判断
System.out.println(newInstance == Singleton.getSingleton());
}
}
输出会为false。这种情况可以通过在单例类中添加readResolve方法解决,如下:
package com.faith.net;
import java.io.Serializable;
/**
* 单例类
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve() {
return singleton;
}
}
因为java对象的反序列化通过ObjectInputputStream实现的,通过readObject方法读取字节流并创建新的对象。
但在readObject方法中有一个判断,内容是判断被反序列化的类中是否包含readResolve方法,如果包含readResolve方法,就直接返回readResolve方法中的逻辑,因此readResolve能避免反序列化对单例模式的破坏。
反射对单例的破坏
懒汉式、饿汉式都可以被反射破坏,如下:
public static void main(String[] args) {
Class<?> clazz = Singleton.class;
//通过反射拿到私有的构造方法
Constructor c = clazz.getDeclaredConstructor(null);
//强制访问私有构造
c.setAccessible(true);
//调用了两次构造方法,相当于new了两次
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);
}
而枚举可以避免被反射破坏,因为枚举不能被反射方式访问。