对于重量级、无状态的对象,对象的重复创建和销毁是有巨大开销的,在系统中可以保持唯一实例以支持重复使用。
受Spring管理的对象默认都是单例,另外自己创建的线程池、连接池也应该保证单例。
如何创建:私有化构造器,并提供一个全局唯一的访问入口以获取单例对象。
饿汉式单例模式
利用类加载机制创建出对象,由jvm保证对象唯一。
在类加载的 ‘准备’阶段时就创建好了对象,等第一次通过getInstance()方法获取对象时就可以直接拿到,所以叫饿汉。
缺点是对象过早创建,即使它并不一定会被使用,造成资源浪费。
/**
* 饿汉单例模式
*/
public class EagerSingleton {
//私有化构造器
private EagerSingleton(){
}
// 利用类加载保证只执行一次
// 注意final 修饰才能在类加载的 ‘准备’阶段直接赋初始值,也就是直接执行了new EagerSingleton()
private static final EagerSingleton eagerSingleton = new EagerSingleton();
// 提供获取单例对象的访问
public static EagerSingleton getInstance(){
return eagerSingleton;
}
}
懒汉式单例模式
懒汉单例模式-基础版本
/**
* 懒汉单例模式-基础版本
* 当getInstance()第一次被调用时创建出对象,后续再调用getInstance()时判断对象已有创建,则直接return
*
* 在多线程下,可能会创建出多实例
*/
public class LazySingleton {
private LazySingleton() {}
private static LazySingleton instance;
public static LazySingleton getInstance() {
if(instance == null) {
instance = new LazySingleton();
}
return instance; //对象已有创建,则直接return
}
}
懒汉单例模式-双重校验锁
/**
* 懒汉单例模式-双重校验锁
* 第一个if(instance == null) 的作用:
* 判断对象已存在则不用加锁,降低锁粒度;
* 第二个if(instance == null) 的作用:
* 仍然可能多个线程都通过了第一个if,未抢到锁的线程将被阻塞,再重新获得synchronized 锁后,
* 会继续执行synchronized中的内容,没有第二个if仍将创建出多实例,需要再判断一次。
*
* volatile 的作用:禁止指令重排
* new LazySingleton()只有一句代码,但在字节码层面是三个步骤:
* 1. 分配堆内存空间
* 2. 在堆上初始化这个对象
* 3. 堆堆引用地址赋值给变量
* 2和3都依赖于1,但2和3之间没有依赖关系,可能发生指令重排序为 1->3->2 当引用赋值后,instance 不再等于 null,
* 其他线程可直接返回instance ,而实际上第2步,对象初始化并未完成,那么拿到instance 的线程将发生空指针。
*/
public class LazySingleton {
private LazySingleton() {}
private static volatile LazySingleton instance; // volatile 禁止指令重排
public static LazySingleton getInstance() {
if(instance == null) {
synchronized (LazySingleton.class) {
if(instance == null) {
instance = new LazySingleton();
}
}
}
return instance; //对象已有创建,则直接return
}
}
静态内部类单例模式
public class SingleTon{
private SingleTon(){}
private static class SingleTonHolder{
private static SingleTon instance = new SingleTon();
}
public static SingleTon getInstance(){
return SingleTonHolder.instance;
}
}
结合类加载的知识。
外部类和内部类一样,经过加载、连接后并不立刻初始化。(此时内部类的instance 只会赋默认值null)
只有当调用外部类:LazySingleton.getInstance() 并返回调用 内部类 静态instance属性时,触发内部类初始化,为instance 类变量赋初始值。这个初始值就是 new LazySingleton();
为instance 类变量赋初始值这个过程只会发生一次,且由JVM保证这个过程是单线程的。new LazySingleton() 的过程也是instance 赋初始值的过程,而赋初始值的过程是单线程的,就保证了new 对象期间不会发生doubleCheck中那样线程上下文切换。
以上的写法都是自己刚好就是需要被保证单例的资源类;但其实也可以是仅仅作为一个util类,对第三方的资源类提供一个获取单例的入口,而不涉及对这个资源类的代码改动。
利用反射创建出多实例
以上饿汉、懒汉、内部类创建的单例模式其实不能阻止利用反射创建出多个实例;
SingleTon st1= SingleTon.getInstance();
Constructor<SingleTon> constructor = SingleTon.class.getDeclaredConstructor(); // 获取无参构造器
constructor.setAccessible(true); // 改变私有属性
SingleTon st2= constructor.newInstance();
System.out.println(st1== st2); // false
解决办法是:在私有化的构造器中再加上判断实例如果已存在,则抛出异常RuntimeException
public class SingleTon{
private SingleTon(){
if (SingleTonHolder.instance != null){
throw new RuntimeException("不能创建多个实例");
}
}
private static class SingleTonHolder{
private static SingleTon instance = new SingleTon();
}
public static SingleTon getInstance(){
return SingleTonHolder.instance;
}
}
利用反序列化创建出多实例
防反序列化,需要通过提供readResolve方法,返回instance。
枚举单例模式
外部通过 SomeThing.INSTANCE.getInstance() 就可以访问单实例。
public enum SomeThing {
INSTANCE;
private Resource instance;
private SomeThing() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
class Resource{
// Resource 是我们要单保证例的资源,网络连接,数据库连接,线程池等等...
}
// 有个疑问。虽然能保证通过SomeThing.getInstance() 拿到的Resource对象是单例,但并不能防止在其他地方直接 new Resource()...
// 这个写法就是针对的依赖包,只保证通过我提供的方式获取到单例。
// 例如项目中线程池、连接池,能保证从池里拿到都是同一个实例,但没有限制不能自己创建链接、自行new Thread()
枚举类型单例,写在枚举容器,也不会对原资源做改造,可以防被反序列化和反射生成多个实例。
如果需要被保证单实例的资源类本就是自行创建,可以换一个写法:
(这个Resource 就不能是第三方jar包里的类了,如果是我们自己创建的类,并且需要保证单例,推荐这个内部枚举的方式)
public class Resource {
// Resource 是我们要单保证例的资源,网络连接,数据库连接,线程池等等...
private Resource {
}
private enum SomeThing {
INSTANCE;
private final Resource instance;
private SomeThing() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
public static Resource getInstance() {
SomeThing.INSTANCE.getInstance();
}
}
使用反射declaredConstructor.newInstance()的源码中,会判断如果当前是枚举类型,则抛出异常,也就意味着如果通过枚举创建单例模式可以防止反射创建多实例。