转载

JAVA设计模式之:单例模式

一、前言

单例模式是一种非常常见的设计模式,在现实生活中应用也非常广泛。例如:公司CEO、部门经理等。J2EE标准中的ServletContextConfig、ServletContext等、Spring框架应用中ApplicationContext、数据库的连接池等也都是单例形式。并且在面试中却也是一个高频面试点。

二、定义

单例模式(SingletonPattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。

三、饿汉单例模式

饿汉单例模式在类被加载的时候就立即初始化,并且创建单例对象。他是绝对线程安全的,在线程还没有出现之前就实例化了,不可能出现线程安全问题。

优点:没有加锁,执行效率高,用户体验比懒汉单例好。

缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。

//第一种直接new创建对象
public class HungrySingleton{  
      
    private static final HungrySingleton instance = new HungrySingleton();  
  
    private HungrySingleton(){}  
  
    public static HungrySingleton getInstance(){  
        return instance;  
  }  
}

//第二种通过静态代码块进行单例对象初始化
public class HungryStaticSingleton {  
  
    private static final HungryStaticSingleton instance;  
  
    private HungryStaticSingleton(){}  
  
    static{  
        instance = new HungryStaticSingleton();  
    }  
  
    public static HungryStaticSingleton getInstance(){  
        return instance;  
    }   
}

三、懒汉单例模式

懒汉单例的特点就是在第一次被调用的时候,进行单例对象的初始化。

懒汉单例的实现方式有很多种,首先我们实现一种相对简单的。

1.懒汉单例的简单实现(线程不安全)

public class LazySimpleSingleton {  
  
    private static LazySimpleSingleton instance;  
  
    private LazySimpleSingleton(){}  
  
    public static LazySimpleSingleton getInstance(){  
        if(instance == null){  
            instance = new LazySimpleSingleton();  
        }  
        return instance;  
    }  
}

以上就是实现懒汉单例最简单例子,但是这样实现方式会带来线程安全问题。经过多次测试,会代码三种不同的结果

1.当第一个线程执行完if(instance == null)代码块时,没有其他线程同时在执行if(instance == null)代码块时, 这时结果是单例的

2.当多个线程同时执行if(instance == null)代码块时,并且其他线程还未从当前方法中获得返回值,最后一个进入if(instance == null)代码块的线程会 覆盖前面创建的实例 ,并且最终结果是所有线程得到了相同的实例。

3.当多个线程同时执行if(instance == null)代码块时,并且已经有线程执行完方法并获得返回值,但if(instance == null)代码块中还有其他线程在执行,最终结果可能就会出现各个线程之间获得的 实例不一致

第一种情况

JAVA设计模式之:单例模式

JAVA设计模式之:单例模式

JAVA设计模式之:单例模式

第一种情况只有一个线程执行了if(instance == null)中的逻辑,所有没有出现线程安全问题。

第二种情况

JAVA设计模式之:单例模式

两个线程同时走进了if(instance == null)代码块中

JAVA设计模式之:单例模式

线程1先走完,但是没有返回,这时看到实例地址是@631

JAVA设计模式之:单例模式

线程0在执行一遍if(instance == null)代码块中的逻辑,这时发现实例地址编程了@632,很明显后一个线程覆盖了前一个线程所创建的实例

JAVA设计模式之:单例模式

结果虽然相同但是实例已经创建了两次。

第三种情况

JAVA设计模式之:单例模式

线程0执行完getInstance()方法获得了实例,地址为@631

JAVA设计模式之:单例模式

线程1执行完getInstance()方法获得了实例,地址为@632

JAVA设计模式之:单例模式

最终返回不一样的两个实例对象

三种情况都演示完毕,那要怎么才能避免线程安全,很多人可能想到的是在方法上加上synchronized关键字,那我们来演示一下

2.线程安全的懒汉单例(性能低)

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

//或者这样
public class LazySyncSingleton {  
  
    private static LazySyncSingleton instance;  
  
    private LazySyncSingleton(){}  
  
    public static LazySyncSingleton getInstance(){  
        synchronized (LazySyncSingleton.class){  
            if(instance == null){  
                instance = new LazySyncSingleton();  
            }  
            return instance;  
    }  
}
}

以上两个方式虽然解决了线程安全问题,但是严重影响性能,不管单例对象是否已经实例化,只要有多个线程同时调用getInstance()方法,其中一个线程获得了锁,其他线程均会阻塞在外面。

那既要线程安全,又要保证性能问题,我们试着在第二种实现方式上做一些修改。

3.双重检查锁单例

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

这种实现方式也称为双重检查锁单例,这种实现方式的好处在于只有在实例对象进行初始化的时候才会造成线程阻塞,实例对象初始化完成后,可以通过外层的if判断消除synchronized带来的阻塞问题。

还有一点需要注意的是可能出现的指令重排序问题。这个问题的关键就在于由于指令重排优化的存在,导致初始化实例对象和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。

所以我们需要为instance属性加上volatile关键字。

虽然双重检查锁单例能够很好的解决线程安全和性能问题,但是这种设计方式,带来了另一个弊端就是代码不易于理解,那有没有更好的设计方式呢,下面来说一下另一种实现方式,静态内部类单例。

4.静态内部类单例

public class LazyStaticInnerClassSingleton{  
  
 private LazyStaticInnerClassSingleton(){}  
  
    public static LazyStaticInnerClassSingleton getInstance(){  
        return InnerClassSingleton.INSTANCE;  
    }  
  
    private static class InnerClassSingleton{  
        private static LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();  
    }
}

静态内部类看起来像是很完美,它解决了上面我们说的所有问题。但是假如我们通过反射的方式能否创建新的实例呢。

/**  
 * Create By Ke Shuiqiang  2020/3/9 22:33 
 * 静态内部类测试  
 */
public class LazyStaticInnerClassSingletonTest{  
      
    public static void main(String[] args) throws Exception{  
  
        Class<?> clazz = LazyStaticInnerClassSingleton.class;  
  
        Constructor<?> c = clazz.getDeclaredConstructor(); 
        c.setAccessible(true);  
        Object instance1 = c.newInstance();  
        Object instance2 = c.newInstance();  
        System.out.println(instance1);  
        System.out.println(instance2);  
        System.out.println(instance2 == instance1);  
    }
 }

JAVA设计模式之:单例模式

可以看到测试结果显示,通过反射创建一个多个对象。

那我们怎么避免反射破坏单例呢。

private LazyStaticInnerClassSingleton(){  
    if(InnerClassSingleton.INSTANCE != null){  
        throw new RuntimeException("非法操作,不允许反射创建");     
    }  
}

可以在默认构造器中抛异常来阻止反射创建。

那我们在来看看反序列化能否破坏单例。

/**  
 * 通过反序列化破坏单例 
 */
public static void main(String[] args) throws Exception {  
  
    LazyStaticInnerClassSingleton instance = LazyStaticInnerClassSingleton.getInstance();  
  
    ObjectOutputStream oos = null;  
    try {  
        oos = new ObjectOutputStream(new FileOutputStream("singleton.obj"));  
        oos.writeObject(instance);  
        oos.flush();  
        oos.close();  
  
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.obj"));  
        Object o = ois.readObject();  
        ois.close();  
 
        System.out.println(instance);  
        System.out.println(o);  
  
  } catch (Exception e) {  
        e.printStackTrace();  
  }  
}

JAVA设计模式之:单例模式

测试结果显示反序列化生成了新的实例。

那如何避免反序列化破坏单例呢。

private Object readResolve(){  
    return InnerClassSingleton.INSTANCE;  
 }

JAVA设计模式之:单例模式

测试结果显示两个实例是同一个对象,是不是很神奇,我们只是在类中添加了readResolve()的方法,并把单例对象返回了,就能把反序列化问题给解决了。让我们来看看readObject()中的源码

ObjectInputStream.readObject()源码:

public final Object readObject()  throws IOException, ClassNotFoundException {  
    if (enableOverride) {  
        return readObjectOverride();  
    }  
    // if nested read, passHandle contains handle of enclosing object  
    int outerHandle = passHandle;  
    try {  
        Object obj = readObject0(false);  
        handles.markDependency(outerHandle, passHandle);  
        ClassNotFoundException ex = handles.lookupException(passHandle);  
        if (ex != null) {  
            throw ex;  
        }  
        if (depth == 0) {  
            vlist.doCallbacks();  
        }  
        return obj;  
    } finally {  
        passHandle = outerHandle;  
        if (closed && depth == 0) {  
            clear();  
        }  
    }  
}
/**  
 * Underlying readObject implementation. 
 */
private Object readObject0(boolean unshared) throws IOException {  
    boolean oldMode = bin.getBlockDataMode();  
    if (oldMode) {  
        int remain = bin.currentBlockRemaining();  
    if (remain > 0) {  
        throw new OptionalDataException(remain);  
    } else if (defaultDataEnd) {  
        throw new OptionalDataException(true);  
    }  
        bin.setBlockDataMode(false);  
    }  
  
    byte tc;  
    while ((tc = bin.peekByte()) == TC/_RESET) {  
        bin.readByte();  
    handleReset();  
    }  
  
    depth++;  
    totalObjectRefs++;  
    try {  
        switch (tc) {  
            case TC_NULL:  
                return readNull();  
                
            case TC_REFERENCE:  
                return readHandle(unshared);  
            
            case TC_CLASS:  
                return readClass(unshared);  
            
            case TC_CLASSDESC:  
            case TC_PROXYCLASSDESC:  
                return readClassDesc(unshared);  
            
            case TC_STRING:  
            case TC_LONGSTRING:  
                return checkResolve(readString(unshared));  
            case TC_ARRAY:  
                return checkResolve(readArray(unshared));  
            
            case TC_ENUM:  
                return checkResolve(readEnum(unshared));  
            
            case TC_OBJECT:  
                return checkResolve(readOrdinaryObject(unshared));  
  
            case TC_EXCEPTION:  
                IOException ex = readFatalException();  
                throw new WriteAbortedException("writing aborted", ex);  
  
            case TC_BLOCKDATA:  
            case TC_BLOCKDATALONG:  
                if (oldMode) {  
                    bin.setBlockDataMode(true);  
                    bin.peek(); 
                    // force header read  
                    throw new OptionalDataException(  
                        bin.currentBlockRemaining());  
                } else {  
                    throw new StreamCorruptedException(  
                        "unexpected block data");  
                }  
  
            case TC_ENDBLOCKDATA:  
                if (oldMode) {  
                    throw new OptionalDataException(true);  
                } else {  
                    throw new StreamCorruptedException(  
                        "unexpected end of block data");  
                }  
  
            default:  
                throw new StreamCorruptedException(  
                    String.format("invalid type code: %02X", tc));  
        }  
    } finally {  
        depth--;  
        bin.setBlockDataMode(oldMode);  
    }  
}

JAVA设计模式之:单例模式

ObjectInputStream.readObject() --> readObject0(boolean unshared) --> readOrdinaryObject(boolean unshared)

可以看到标出的两处代码desc.hasReadResolveMethod()表示该类是否有readResoleve()方法,desc.invokeReadResolve(obj)表示调用该方法获得返回值。

注册式单例模式

注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例分为两种,一种是枚举单例,另一种是容器式单例。

1.枚举单例

public enum EnumSingleton implements Serializable {  
  
    INSTANCE;  
  
    private Object date;  
  
    public Object getDate() {  
        return date;  
    }  
  
    public void setDate(Object date) {  
        this.date = date;  
    }  
  
    public static EnumSingleton getInstance(){  
        return INSTANCE;  
    }  
}

枚举式单例是天然的能防止反射和反序列化破坏单例的实现方式。我们可以来测试一下。

反射

public static void main(String[] args) throws Exception{  
  
    Class<?> clazz = EnumSingleton.class;
    //枚举没有默认构造器
    Constructor<?> c = clazz.getDeclaredConstructor(String.class, int.class);  
    c.setAccessible(true);  
    Object instance = c.newInstance();  
    System.out.println(instance);  
}

JAVA设计模式之:单例模式

测试结果抛异常,我们从Constructor.newInstance(Constructor.java:417)点进去看看。

JAVA设计模式之:单例模式

原来JDK内部就有这种机制,不允许我们通过反射创建枚举对象。

反序列化

public static void main(String[] args) throws Exception{  
  
    EnumSingleton instance = EnumSingleton.getInstance();  
    instance.setDate(new Object());  
  
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));  
    oos.writeObject(instance);  
    oos.flush();  
    oos.close();  
  
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));  
    EnumSingleton o = (EnumSingleton)ois.readObject();  
    ois.close();  
  
    System.out.println(instance.getDate());  
    System.out.println(o.getDate());  
}

JAVA设计模式之:单例模式

测试结果显示是同一个实例。

让我们看看枚举式如果让反序列化生成的是同一个实例的,我们通过JAD反编译工具(下载地址: https://varaneckas.com/jad/) ,来看看枚举背后的源码是长什么样子的。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.  
// Jad home page: http://www.kpdus.com/jad.html  
// Decompiler options: packimports(3)   
// Source File Name:   EnumSingleton.java  
  
package com.ksq.em;  
  
import java.io.Serializable;  
  
public final class EnumSingleton extends Enum  
    implements Serializable  
{  
  
    public static EnumSingleton[] values()  
    {  
        return (EnumSingleton[])$VALUES.clone();  
    }  
  
    public static EnumSingleton valueOf(String name)  
    {  
        return (EnumSingleton)Enum.valueOf(com/ksq/em/EnumSingleton, name);  
    }  
  
    private EnumSingleton(String s, int i)  
    {  
        super(s, i);  
    }  
  
    public Object getDate()  
    {  
        return date;  
    }  
  
    public void setDate(Object date)  
    {  
        this.date = date;  
    }  
  
    public static EnumSingleton getInstance()  
    {  
        return INSTANCE;  
    }  
  
    public static final EnumSingleton INSTANCE;  
    private Object date;  
    private static final EnumSingleton $VALUES[];  
  
    static   
    {  
        INSTANCE = new EnumSingleton("INSTANCE", 0);  
        $VALUES = (new EnumSingleton[] {  
            INSTANCE  
        });  
    }  
}

这里可以看到,枚举式单例模式在静态代码块中就给INSTANCE进行了赋值,并且是饿汉式单例模式的实现。

那我们再从ObjectInputStream.readObject()看看是如果实现的。

JAVA设计模式之:单例模式

JAVA设计模式之:单例模式

JAVA设计模式之:单例模式

JAVA设计模式之:单例模式

JAVA设计模式之:单例模式

可以看到整个调用链的最终结果就是调用枚举类的values()方法,以此获得被加载好的EnumSingleton数组,然后通过INSTANCE标识获得当前INSTANCE所对应的EnumSingleton。

JAVA设计模式之:单例模式

JAVA设计模式之:单例模式

2.容器式单例

public class ContainerSingleton {  
  
    private static final Map<String, Object> ioc = new ConcurrentHashMap<>();  
  
    private ContainerSingleton(){}  
  
    public static Object getInstance(String beanName){  
  
        Object instance = null;  
        if((instance = ioc.get(beanName)) == null) {  
            synchronized (ioc) {  
                if ((instance = ioc.get(beanName)) == null) {  
                    instance = new ContainerSingleton();  
                    ioc.put(ContainerSingleton.class.getName(), instance);  
                }  
            }  
        }  
        return instance;  
    }  
}

容器式单例模式适用于实例非常多的情况,便于管理。但它是非线程安全的,同样的在示例中添加了双重检查锁,容器式单例是仿照Spring的IOC容器实现的。

六、ThreadLocal线程单例

ThreadLocal不能保证其创建的对象是全局唯一的,但是能保证在单个线程中是唯一的,天生是线程安全的。

public class ThreadLocalSingleton {  
  
    private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>() {  
        @Override  
        protected ThreadLocalSingleton initialValue() {  
            return new ThreadLocalSingleton();  
        }  
    };  
  
    private ThreadLocalSingleton() {}  
  
    public static ThreadLocalSingleton getInstance() {  
        return threadLocal.get();  
    }  
  
}
public class ThreadLocalSingletonTest {  
  
    public static class Task implements Runnable{  
  
        @Override  
        public void run() {  
            System.out.println(Thread.currentThread().getName() + " " + ThreadLocalSingleton.getInstance());  
            System.out.println(Thread.currentThread().getName() + " " + ThreadLocalSingleton.getInstance());  
        }  
    }  
  
    public static void main(String[] args) {  
  
        Task task = new Task();  
  
        Thread t1 = new Thread(task);  
        Thread t2 = new Thread(task);  
  
        t1.start();  
        t2.start();  
  
        System.out.println(Thread.currentThread().getName() + " " + ThreadLocalSingleton.getInstance());  
        System.out.println(Thread.currentThread().getName() + " " + ThreadLocalSingleton.getInstance());  
        System.out.println(Thread.currentThread().getName() + " " + ThreadLocalSingleton.getInstance());  
  }  
}

JAVA设计模式之:单例模式

我们发现,在主线程中无论调用多少次,获取到的实例都是同一个,都在两个子线程中分别获取到了不同的实例。那么ThreadLocal是如何实现这样的效果的呢?我们知道,单例模式为了达到线程安全的目的,会给方法上锁,以时间换空间。ThreadLocal 将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。

七、总结

1).使用单例模式可以保证内存中只有一个实例对象,极大的减少内存开销和提升性能。

2).如果一个单例的初始化过程较快,暂用内存较少,可以考虑使用饿汉单例,因为它不需要担心线程安全问题

3).当我们考虑使用懒汉式单例时,我们既要保证线程安全,也要保证性能。

4).除了枚举式单例,任何一种单例模式,我们都要注意可能遭受到的反射和反序列化破坏。

5).当一个应用的单例过多时,我们应该考虑使用容器单例。

6).如果只是保证线程之间的单例,我们可以使用ThreadLocal来实现。

原文  https://segmentfault.com/a/1190000021973727
正文到此结束
Loading...