单例模式应用场景
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。在Spring框架中ApplicationContext就是单例模式的应用。
饿汉式单例
饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。线程安全。
优点:没有加任何的锁、执行效率比较高。
缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton(){
}
public static final HungrySingleton getINSTANCE(){
return INSTANCE;
}
}
还有另一种写法,利用静态代码块的方式:
public class HungrySingleton {
private static final HungrySingleton INSTANCE;
static {
INSTANCE = new HungrySingleton();
}
private HungrySingleton(){
}
public static final HungrySingleton getINSTANCE(){
return INSTANCE;
}
}
以上写法都非常简单,饿汉式单例适用于在单例对象较少的情况下。
懒汉式
懒汉式单例的特点是:被外部类调用的时候内才会创建实例。
- 非线程安全
//懒汉式单例 //在外部需要使用的时候才进行实例化
public class LazySimpleSingleton {
private LazySimpleSingleton(){
}
//静态块,公共内存区域
private static LazySimpleSingleton lazy = null;
public static LazySimpleSingleton getInstance(){
if(lazy == null){
lazy = new LazySimpleSingleton();
}
return lazy;
}
以上写法在多线程调用的情况下会出现线程安全问题,有一定几率创建多个实例。
所以我们可以对以上代码进行修改,就是上锁:
- 方法加锁
public class LazySimpleSingleton {
private LazySimpleSingleton(){
}
//静态块,公共内存区域
private static LazySimpleSingleton lazy = null;
public synchronized static LazySimpleSingleton getInstance(){
if(lazy == null){
lazy = new LazySimpleSingleton();
}
return lazy;
}
此种加锁方式在多线程情况下会导致线程阻塞,程序运行性能大幅下降。所以我们可以进一步升级:
- 双重校验锁
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazy;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazy == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazy == null) {
lazy = new LazyDoubleCheckSingleton();
}
}
}
return lazy;
}
}
此种方法采用了双重校验锁的方式,在实例创建完成后,多线程调用的情况下也不会阻塞线程。但是此种方式在多线程都来初始化实例的时候就会形成锁竞争,也会对性能造成影响。
我们可以采用一种更为优雅的写法:
- 静态内部类
public class StaticInnerClassHurrySingleton {
private StaticInnerClassHurrySingleton(){
}
public static final StaticInnerClassHurrySingleton getINSTANCE(){
return Holder.SINGLETON;
}
private static class Holder{
private static final StaticInnerClassHurrySingleton SINGLETON = new StaticInnerClassHurrySingleton();
}
}
静态内部类的写法既没有饿汉式的内存浪费问题,也没有加锁带来的性能开销。内部类一定是在方法调用之前初始化的,巧妙的避免了线程安全问题。
单例模式的破坏
反射破坏单例
大家有没有发现,上面介绍的单例模式的构造方法除了加上private以外,没有做任何处理。如果我们使用反射来调用其构造方法,然后,再调用getInstance()方法,就可以创建两个不同的实例。现在来看一段测试代码,以StaticInnerClassHurrySingleton为例:
public class StaticInnerClassHurrySingletonTest {
public static void main(String[] args) {
try {
//很无聊的情况下,进行破坏
Class<?> clazz = StaticInnerClassHurrySingleton.class;
//通过反射拿到私有的构造方法
Constructor c = clazz.getDeclaredConstructor(null);
//强制访问
c.setAccessible(true);
//暴力初始化
Object o1 = c.newInstance();
//调用了两次构造方法,相当于 new了两次
Object o2 = c.newInstance();
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:false
显然创建了两个不同的实例,我们可以在构造方法中添加现在来,一旦出现多次创建,则直接抛出异常:
public class StaticInnerClassHurrySingleton {
private StaticInnerClassHurrySingleton(){
//添加实例检查
if(Holder.SINGLETON!=null){
//抛出异常
throw new RuntimeException("实例已被创建");
}
}
public static final StaticInnerClassHurrySingleton getINSTANCE(){
return Holder.SINGLETON;
}
private static class Holder{
private static final StaticInnerClassHurrySingleton SINGLETON = new StaticInnerClassHurrySingleton();
}
}
继续上面的测试,结果:
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.hiwei.pattern.singleton.hunger.LazyInnerClassSingletonTest.main(LazyInnerClassSingletonTest.java:19)
Caused by: java.lang.RuntimeException: 实例已被创建
at com.hiwei.pattern.singleton.hunger.StaticInnerClassHurrySingleton.<init>(StaticInnerClassHurrySingleton.java:10)
... 5 more
反射破坏单例漏洞就被修复掉了。
序列化破坏单例
当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码:
public class SeriableSingleton implements Serializable {
/* 序列化: 把内存中的状态通过转换成字节码的形式 从而转换一个 IO 流,写入到其他地方(可以是磁盘、网络 IO) 内存中状态给永久保存下来了 反序列化: 将已经持久化的字节码内容,转换为 IO 流 //通过 IO 流的读取,进而将读取的内容转换为 Java 对象 在转换过程中会重新创建对象new */
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton() {
}
public static SeriableSingleton getInstance() {
return INSTANCE;
}
}
测试代码:
public class SeriableSingletonTest {
public static void main(String[] args) {
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试结果:
com.hiwei.pattern.singleton.hunger.SeriableSingleton@6acbcfc0
com.hiwei.pattern.singleton.hunger.SeriableSingleton@60e53b93
false
结果表明单例模式已经被序列化破坏掉了。怎么避免呢?
增加readResolve()方法即可。来看优化代码:
/** * @author: hiwei * @create: 2018-06-25 22:24 */
public class SeriableSingleton implements Serializable {
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton() {
}
public static SeriableSingleton getInstance() {
return INSTANCE;
}
private Object readResolve(){
return INSTANCE; }
}
仍由上面测试方法测试,测试结果:
com.hiwei.pattern.singleton.hunger.SeriableSingleton@60e53b93
com.hiwei.pattern.singleton.hunger.SeriableSingleton@60e53b93
true
显然问题被解决了。
为什么加一个方法就解决了这个问题?我们可以看下JDK的源码,就是ObjectInputStream类的readObject()方法,通过这个方法我们可以看到以看到实际上实例化了两次,只不过新创建的对象没有被返回而已。这部分代码因为篇幅较多,就不在此解析了。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大。下面我们可以采用另一种方式来实现单例:
注册式单例
注册式单例有两种写法:一种为容器缓存,一种为枚举登记。
- 枚举式单例
枚举式单例的写法,来看代码,创建EnumSingleton类:
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
使用反编译工具来反编译该类,可以得到以下代码:
static {
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[]{
INSTANCE});
}
枚举式单例在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例的实现。
- 容器式单例
public class ContainerSingleton {
private ContainerSingleton(){
}
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
public static Object getInstance(String className){
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
} else {
return ioc.get(className);
}
}
}
}
spring的IOC容器使用的就是这种单例模式。
ThreadLocal线程单例
线程单例实现 ThreadLocal。ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,线程安全。
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){
}
public static ThreadLocalSingleton getInstance(){
return threadLocalInstance.get();
}
}
总结
自此单例模式都实现了。单例模式保证了类内存里面只有一个内存实例,减少了内存开销,避免了对内存资源的多重占用