设计模式之单例模式(Java)

概述

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式提供一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

要点有三个:

  • 某个类只能有一个实例;
  • 必须自行创建这个实例;
  • 必须自行向整个系统提供这个实例。

单例模式是一种对象创建型模式,又名单件模式或单态模式。

UML图
在这里插入图片描述

实现

懒汉式

public class Singleton {
	private static Singleton instance;
    private Singleton () {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

所谓懒汉式,就是在类初始化时并没有实例化,需要时才去实例化。

上面的写法是线程不安全的。解决方法:在方法上增加synchronized关键词。

饿汉式

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton () {}
    public static Singleton getInstance() {
        return instance;
    }
}

基于classloader机制,instance在类装载时就实例化。

饿汉变形

public class Singleton {
    private Singleton instance = null;
    static {
        instance = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return this.instance;
    }
}

枚举

public enum Singleton {
	INSTANCE;
	// 可不加
	public static Singleton getInstance() {
		return INSTANCE;
	}
}

编译后,可发现使用static final。测试如下:

public static void main(String[] args) {
	Singleton s = Singleton.INSTANCE;
	Singleton s1 = Singleton.getInstance();
	System.out.println(s.equals(s1));
}

静态内部类

public class Singleton {
    private static class SingletonHolder {
    	private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton () {
    }
    public static final Singleton getInstance() {
    	return SingletonHolder.INSTANCE;
    }
}

是懒汉式,线程安全的写法。

DCL

public class Singleton {
    private volatile static Singleton singleton;
    private Singleton() {
    }
    public static Singleton getInstance(){
        if (singleton == null) {
            synchronized(Singleton.class) {
                if (singleton == null)
                    singleton = new Singleton();
                }
        }
        return singleton;
    }
}

应用

JDK

Runtime.getRuntime是单例模式。

public class Runtime {
	private static Runtime currentRuntime = new Runtime();

	public static Runtime getRuntime() {
		return currentRuntime;
	}
	private Runtime() {}
}

MyBatis

MyBatis单例模式举例,ErrorContext和LogFactory,ErrorContext是用在每个线程范围内的单例,用于记录该线程的执行环境错误信息,而LogFactory则是提供给整个Mybatis使用的日志工厂,用于获得针对项目配置好的日志对象。

public class ErrorContext {
	private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
	
	private ErrorContext() {
	}
	
	public static ErrorContext instance() {
		ErrorContext context = LOCAL.get();
		if (context == null) {
			context = new ErrorContext();
			LOCAL.set(context);
		}
		return context;
	}
}

构造函数是private修饰,具有一个static的局部instance变量和一个获取instance变量的方法,在获取实例的方法中,先判断是否为空如果是的话就先创建,然后返回构造好的对象。
LOCAL的静态实例变量使用ThreadLocal修饰,也就是说它属于每个线程各自的数据,而在instance()方法中,先获取本线程的该实例,如果没有就创建该线程独有的ErrorContext。

问题

static

一个类中所有的方法和属性都被标注为static,是单例吗?
是的,不过有线程安全问题。

性能

哪种写法性能最好?

安全

单例模式不是绝对安全,可以被破坏,即可以拿到不相等的两个实例。

反射

考虑如下代码:

public class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getSingleton();
        try {
            Class<Singleton> singleClass = (Class<Singleton>)Class.forName("aa.bb.Singleton");
            Constructor<Singleton> constructor = singleClass.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            Singleton singletonByReflect = constructor.newInstance();
            System.out.println("singleton : " + singleton);
            System.out.println("singletonByReflect : " + singletonByReflect);
            System.out.println("singleton == singletonByReflect : " + (singleton == singletonByReflect));
        } catch (Exception e) {
        }
    }
}

输出:

singleton : aa.bb.Singleton@55d56113
singletonByReflect : aa.bb.Singleton@148080bb
singleton == singletonByReflect : false

结论:通过发射的方式即可获取到一个新的单例对象,单例模式被破坏。
解决:在Singleton的构造函数中增加判断

private Singleton() {
    if (singleton != null) {
        throw new RuntimeException("Singleton constructor called.");
    }
}

反序列化

先将单例对象序列化后保存到临时文件中,然后再从临时文件中反序列化出来:

public class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getSingleton();
        // Write Obj to file
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
            oos.writeObject(singleton);
            // Read Obj from file
            File file = new File("tempFile");
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
            Singleton singletonBySerialize = (Singleton)ois.readObject();
            System.out.println("singleton : " + singleton);
            System.out.println("singletonBySerialize : " + singletonBySerialize);
            System.out.println("singleton == singletonBySerialize : " + (singleton == singletonBySerialize));
        } catch (Exception e) {
        }
    }
}

输出:

singleton : aa.bb.Singleton@617faa95
singletonBySerialize : aa.bb.Singleton@5d76b067
singleton == singletonBySerialize : false

解决:
在Sinleton中增加readResolve()方法,指定要返回的对象的生成策略即可:

private Object readResolve() {
    return getSingleton();
}

在反序列化过程中,在反序列化执行过程中会执行到ObjectInputStream#readOrdinaryObject方法,这个方法会判断对象是否包含readResolve方法,如果包含的话会直接调用这个方法获得对象实例:

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);
    }
}

没有readResolve方法,则反序列化时通过反射创建对象。

Spring

Spring提供5种scope:singleton、prototype、request、session、global session,参考文档。其中常用的是前两者,默认是单例。

单例bean与原型bean的区别
如果一个bean被声明为单例的,在处理多次请求的时候在Spring容器里只实例化出一个bean,后续的请求都共用这个对象,这个对象会保存在一个map里面。当有请求来的时候会先从缓存map里查看有没有,有的话直接使用这个对象,没有的话才实例化一个新的对象,即单例模式。但是对于原型(prototype)bean来说当每次请求来的时候直接实例化新的bean,没有缓存以及从缓存查的过程。这个实现过程的源码在AbstractBeanFactory.doGetBean()方法里面,源码挺长,略。

单例bean的优势

  1. 减少新生成实例的消耗
    新生成实例消耗包括两方面,第一,spring会通过反射或者cglib来生成bean实例这都是耗性能的操作,其次给对象分配内存也会涉及复杂算法。
  2. 减少JVM垃圾回收
  3. 可以快速获取到bean
    因为单例的获取bean操作除了第一次生成之外其余的都是从缓存里获取的所以很快。

单例bean的劣势
不能保证线程安全!由于所有请求都共享一个bean实例,有状态bean在并发下会出现问题;原型bean则不会有这样问题(例外,如被单例bean依赖),因每个请求都新创建实例。

另外,Spring对单例模式的实现,仅保证提供一个全局的访问点,即BeanFactory。但没有从构造器级别去控制单例的唯一性,因为Spring管理的是任意的Java对象。

参考

如何破坏单例

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

johnny233

晚饭能不能加鸡腿就靠你了

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值