简介
保证一个类只有一个实例,并提供一个全局访问点。当一个对象在整个系统中都可以用到时,单例模式就比较有用了。客户端不在考虑是否要实例化的问题,而把责任都交给应该负责的类去处理。他属性创建型设计模式。
适用场景
1、确保在任何情况下都只要一个实例
2、想要可以简单的访问实例类
3、让类自己控制它的实例化
4、希望可以限制类的实例数
5、像线程池,缓存,对话框等功能,如果出现多个可能导致程序的行为异常,资源使用过度,或者不一致的情况。
优点
1、只有一个实例,减少内存开销
2、对资源没有多重占用
3、设置全局访问点,严格控制访问
缺点
没有接口,扩展困难
存在问题
1、如果存在多个类加载器,那么就会有多个实例,解决:自行指定类加载器,并且是相同的加载器。
2、1.2之前垃圾收集器有个bug,会把单例对象回收,1.2之后这个bug已经解决了。
3、不适合作为父类。
结合其他模式
1、抽象工厂模式,建造者模式,原型模式,享元模式都可以使用单例模式
2、Facade对象都是一个实例,因为只需要一个Facade对象
3、状态对象通常也只需要一个实例
重要条件
1、单例模式就是让他本身来实例化对象,只实例化一次;
2、必须自行创建这个实例,即使用private的构造函数,确保其他对象不能实例化该对象;
3、必须自行向整个系统提供这个实例,即定义一个public static operation(getInstance())来获取一个实例,如Singleton.getInstance()
示例代码
懒加载单例
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(null == lazySingleton){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
public class SingletonTest {
public static void main(String[] args) {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(lazySingleton);
}
}
该模式在单线程下是没有问题的,但是在多线程的情况下,就不能保证只创建一个实例了。
我们来模拟多线程debug,看看输出的实例。
public class MyRunnable implements Runnable {
public void run() {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(lazySingleton);
}
}
public class ThreadTest {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
Thread t2 = new Thread(new MyRunnable());
t1.start();
t2.start();
}
}
先写好多线程的代码,我这边使用的是idea,来对断点配置多线程debug,如图:
在断点处右键,就出来这个框了。接着一步一步debug。
可以在这里进行切换线程。
如图可以看到实例化了两个不同的对象:
懒加载多线程解决
public class LazyThreadSingleton {
private static LazyThreadSingleton lazyThreadSingleton;
private LazyThreadSingleton(){}
public synchronized static LazyThreadSingleton getInstance(){
if(null == lazyThreadSingleton){
lazyThreadSingleton = new LazyThreadSingleton();
}
return lazyThreadSingleton;
}
}
在方法里面加synchronized
,来控制多线程问题。debug查看只有一个线程可以进入getInstance()方法
线程1执行完之后,线程2就可以执行了
这种方式可以解决多线程的问题,但是对性能有很大的影响,synchronized是对整个类进行加锁。
懒加载双重检查锁
相比在方法中添加synchronized,双重检查锁的好处是:不用每次调用方法都需要加锁,只有在实例没有被创建的时候才会加锁处理。第2个的null判断是并发的标准判断:1锁2查3判断。这样才能保证第二个线程在进来之后不会在创建实例,因为已经创建了实例了。
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (null == lazyDoubleCheckSingleton) {
synchronized (LazyDoubleCheckSingleton.class) {
if (null == lazyDoubleCheckSingleton) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
使用双重检查锁的性能要比之前在方法上加锁要好,但是也会有问题,会出现指令重排序问题,如图:
步骤2、3进行了重排序,在单线程内是不会有问题的,但是在多线程里面就会出现问题,如图:
此时线程2判断为null的时候,发现不为null,就会执行第4步,这样就出现问题了。
在变量中增加volatile
来修饰,可以防止指令重排序。
静态内部类
为了解决指令重排序,可以使用静态内部类,让指令重排序对其他线程不可见,如:
代码示例:
public class StaticInnerSingleton {
private StaticInnerSingleton(){
}
private static class InnerClass {
private static StaticInnerSingleton staticInnerSingleton = new StaticInnerSingleton();
}
public static StaticInnerSingleton getInstance(){
return InnerClass.staticInnerSingleton;
}
}
饿汉式
public class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
因为类在加载的时候就被创建了,所以叫饿汉式。延迟加载是在类使用的时候才被创建,所以叫懒汉式。
序列化反序列化破坏单例
public class HungrySerializableSingleton implements Serializable {
private static HungrySerializableSingleton hungrySingleton = new HungrySerializableSingleton();
private HungrySerializableSingleton(){
}
public static HungrySerializableSingleton getInstance(){
return hungrySingleton;
}
}
public static void main(String[] args) throws Exception {
HungrySerializableSingleton instance = HungrySerializableSingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("instance"));
oos.writeObject(instance);
File file = new File("instance");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySerializableSingleton newInstance = (HungrySerializableSingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
输出结果:
com.cimu.creational.singleton.HungrySerializableSingleton@135fbaa4
com.cimu.creational.singleton.HungrySerializableSingleton@3b9a45b3
false
发现经过反序列化之后,不是同一个类了。
在HungrySerializableSingleton
中加入下面代码
private Object readResolve(){
return hungrySingleton;
}
输出结果:
com.cimu.creational.singleton.HungrySerializableSingleton@135fbaa4
com.cimu.creational.singleton.HungrySerializableSingleton@135fbaa4
true
反序列出来的是同一个类,原因分析:
ObjectInputStream
类中,如下代码:
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
hasReadResolveMethod该方法会先判断是否存在readResolve()方法,如果存在,那么会通过反射去调用类里面的readResolve()方法,反射调用主要是通过invokeReadResolve
方法。
反射防御
public static void main(String[] args) throws Exception {
Class objectClass = HungrySingleton.class;
Constructor declaredConstructors = objectClass.getDeclaredConstructor();
declaredConstructors.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = (HungrySingleton) declaredConstructors.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
输出结果:
com.cimu.creational.singleton.HungrySingleton@1540e19d
com.cimu.creational.singleton.HungrySingleton@677327b6
false
通过反射创建出来两个实例,那么如何来防御呢?
public class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
if(null != hungrySingleton){
throw new RuntimeException("反射攻击");
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
在私有构造函数中判断变量是否为空,如果不为空就抛出异常。但是在懒加载中是不起效果的。
枚举单例
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
使用反射攻击,报以下错误
查看Constructor代码,发现
使用序列化和反序列化发现输出的值是相等的,
可以看到如果是枚举的话,会根据name获取枚举的对象。可以通过jad反编译来查看枚举类,使用枚举来创建单例是比较推荐的做法。
容器单例
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> singletonMap = new HashMap<String, Object>();
public static void putInstance(String key,Object object){
if(null != key && !"".equals(key) && null != object){
if(!singletonMap.containsKey(key)){
singletonMap.put(key,object);
}
}
}
public static Object getInstance(String key){
return singletonMap.get(key);
}
}
public static void main(String[] args) {
ContainerSingleton.putInstance("object",EnumSingleton.getInstance());
System.out.println(ContainerSingleton.getInstance("object"));
}
可以通过该方法来存储一堆单例对象,但是存在反射和序列化的问题,可以使用ConcurrentHashMap来控制并发问题。
克隆破坏
第一不要实现Cloneable接口
第二,如果实现了,那么clone方法需要写成如下:
@Override
protected Object clone() throws CloneNotSupportedException {
return getInstance();
}
源码分析
jdk中应用
java.langRuntime
类中也使用了单例,如代码:
private static Runtime currentRuntime = new Runtime();
private Runtime() {}
public static Runtime getRuntime() {
return currentRuntime;
}
mybatis中应用
ErrorContext
类使用了单例,这边使用了ThreadLocal<ErrorContext>
的单例模式,如:
spring中应用
AbstractBeanFactory使用了单例,
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);
部分代码.......
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
是把bean放到ConcurrentHashMap对象中。
注意:本文归作者所有,未经作者允许,不得转载