单例模式

定义

确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

模式结构

单例模式使结构最简单的设计模式,它只包含一个类,即单例类。

优点

(1)单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。

(2)由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。

(3)允许可变数目的实例。基于单例模式可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。

缺点

(1)由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。

(2)单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。

(3)Java语言提供了自动垃圾回收技术,因此如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

适用环境

(1)系统只需要一个实例对象,例如系统要求提供一个唯一的序列号生成器或资源管理器,或者因为资源消耗太大而只允许创建一个对象。

(2)客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

单例模式的实现

饿汉式

(1)在成员变量声明处初始化

public class HungrySingleton {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton() {}
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

(2)在静态代码块中初始化

public class HungrySingleton {
    private static final HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {}
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

上述两种代码本质上是相同的,都是在HungrySingleton加载时就完成了对单例对象的初始化,我们称之位饿汉式单例类

懒汉式

(1)未加锁

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton() {}
    public static LazySingleton getInstance() {
        if(null == lazySingleton) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

懒汉式注重的是延迟加载,只有在使用该对象时才会初始化该对象,但是上述代码存在线程安全问题。

(2)用synchronize关键字给静态方法加锁

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton() {}
    public synchronized static LazySingleton getInstance() {
        if(null == lazySingleton) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

当synchronize修饰在静态方法上时,会给这个LazySingleton.class加锁;当synchronize修饰在非静态方法上时,会给内存中的对象实例加锁。这个方法虽然解决了线程安全问题,但是每次用getInstance()都需要进行线程锁定判断,在多线程高并发访问环境中将会导致系统性能大大降低。

(3)双重检查锁定

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

为什么要加volatile关键字?

在下面这行代码中,虽然看上去只有一行,但实际上可以拆分成3个步骤:

lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();

a:分配内存给这个对象

b:初始化这个对象

c:设置lazyDoubleCheckSingleton指向刚分配的内存地址

如果不加volatile关键字,b和c的顺序会因为Java虚拟机所做的一些代码优化而颠倒,使得真正的顺序成为a -> c -> b。Java虚拟机保证这个指令的重排序不会影响单线程的执行结果,但是不保证多线程的执行结果。

考虑下面这种情形:

分配对象的内存空间(线程一)

                 ↓

设置instance指向内存空间(线程一)

                 ↓

判断instance是否为null(线程二)

                 ↓

线程二初次访问对象(线程二)

                 ↓

初始化对象(线程一)

                 ↓

线程一初次访问对象(线程一)

如上述流程所示,由于Java虚拟机的指令重排,当线程二访问对象时,线程一还没有完成对象的初始化,这就会出错。

饿汉式单例类与懒汉式单例类的比较

饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多个线程同时访问的问题,可以确保实例的唯一性;从调用速度和反应时间来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来将饿汉式单例不及懒汉式单例,并且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。

懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的几率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。

静态内部类

public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton() {}
    private static class InnerClass {
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }
}

在类的初始化阶段,Java虚拟机会获取StaticInnerClassSingleton.class这个锁,这个锁可以保证多线程不受指令重排序的影响。

Java语言规定,发生下述5种情况时,类会被初始化:

有一个A类型的实例被创建。

有一个A类的静态方法被调用。

有一个A类的静态成员被赋值。

A类中声明的一个静态非常量成员被使用。

A类是一个顶级类,并且在类中有嵌套的断言语句。

防御序列化攻击

以上述饿汉式单例模式为例,一旦该类实现了Serializable接口,就需要增加readResolve()方法:

private Object readResolve() {
    return hungrySingleton;
}

如果不增加readResolve()方法,通过序列化和反序列化的方式可以破坏单例模式。

防御反射攻击

没有任何一种单例模式能防御反射攻击,除了枚举类型。

枚举

public enum EnumInstance {
    INSTANCE{
        protected void printTest(){
            System.out.println("print test");
        }
    };
    protected abstract void printTest();
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumInstance getInstance(){
        return INSTANCE;
    }
}

枚举类型实现单例模式是《Effective Java》这本书推荐的单例模式,既能防御序列化攻击,也能防御反射攻击。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值