单例模式

a 夏天 提交于 2020-01-21 02:14:50

单例模式

单例模式的应用场景

单例模式是确保一个类在任何情况下都绝对只有一个实例,并且提供一个全局访问点。单例模式是创建型模式。单例模式的应用非常广泛。在J2EE标准的,ServletContext、ServletContextConfig等;在Spring框架中ApplicationContext;数据库连接池都是单例模式。

单例模式的特点:构造函数私有,全局只有一个实例

恶汉式单例

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

**优点:**没有加任何的锁、执行效率比较高、用户体验比懒汉式更好

**缺点:**类加载的时候就初始化,不管是不是用都会占用内存空间。

Spring中IOC容器ApplicationContext就是恶汉式单例。

恶汉式单例实现代码:

静态变量实现:

package com.zyk.hungry;
public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();
    private HungrySingleton() {
    }
    public static HungrySingleton getInstance() {
        return instance;
    }
}

静态代码块实现

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

上面两种写法的都很简单,原理都是利用类的静态属性优先加载的机制。

恶汉式单例适用于对象较少的情况。

懒汉式单例

懒汉式单例的特点:只有被外部调用的时候内部类才会加载

实现代码

package com.zyk.lazy;
public class LazySingleton {
    //静态,公共内存,线程不安全
    private static LazySingleton instance = null;
    private LazySingleton() {
    }
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

**问题:**线程步安全,当两个线程同时获取单例对象的时候,如果此时两个线程的到的instance都是null的话,就会产生线程安全的问题。导致单例对象被实例化两次。

测试代码:

package com.zyk;
import com.zyk.lazy.LazySingleton;
public class App {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(LazySingleton.getInstance());
        }).start();
        new Thread(() -> {
            System.out.println(LazySingleton.getInstance());
        }).start();
    }
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZDpgsdud-1579488499164)(file:///C:/Users/19176/Documents/My Knowledge/temp/cc5c2097-9671-4890-9e62-0b4282cd8e20/128/index_files/41289703.png)]

可以看到出现了两个不一样的对象。意味着上面的单例存在着线程安全的隐患。可以通过Idea的线程调试工具,来观察结果。

通过使用synchronized关键字使得这个方法变成线程同步的方法。

package com.zyk.lazy;
public class LazySingleton {
    //静态,公共内存,线程不安全
    private static LazySingleton instance = null;
    private LazySingleton() {
    }
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

使用synchronized关键字,使得线程安全的问题得到了解决。但是通过synchronizde加锁,在线程数量比较多的情况下,如果CPU分配压力上升,会导致大批的线程出现阻塞,从而导致程序的运行性下降。可以基于双重检验锁来优化性能。

package com.zyk.lazy;
public class LazySingleton {
    //静态,公共内存,线程不安全
    private static LazySingleton instance = null;
    private LazySingleton() {
    }
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

这样对效率有一定的提升,如果只是读取单例对象,是不需要加锁的。但是用到了synchronized关键字,还是要加锁,对程序的性能还是有影响的。

静态内部类实现单例模式

实现代码

package com.zyk.lazy;
public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton(){
    }
    //每一个关键字都是有用的
    // static是为了使单例的空间共享
    //final保证这个方法不会被重写
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.INSTANCE;
    }
    private static class LazyHolder{
        private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
    }
}

这种方式的原理是:在使用外部类的时候,默认会初始化内部类。这种方式兼顾了恶汉式的线程安全的优点也能消除加锁带来的性能问题。

反射破坏单例

单例模式的构造方法加上了private,没有做任何的处理。我们可以通过反射调用其私有方法,然后在调用getInstance()方法,获取单例,会获取到两个不同的实例。现在看下面一段测试代码,以双重检验锁实现单例的方式为例:

public class App {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 获取单例对象的字节码
        Class<LazySingleton> clazz = LazySingleton.class;
        // 获取构造器
        Constructor<LazySingleton> constructor =
                clazz.getDeclaredConstructor();
        // 设置为可访问
        constructor.setAccessible(true);
        // 创建对象
        LazySingleton singleton = constructor.newInstance();
        System.out.println(singleton == LazySingleton.getInstance());
    }
}

运行结果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4qDVd6kw-1579488499165)(file:///C:/Users/19176/Documents/My Knowledge/temp/cc5c2097-9671-4890-9e62-0b4282cd8e20/128/index_files/47341171.png)]

可以看出创建了两个不同的实例。我们可以通过对构造函数做一些限制。一旦出现重复创建,直接抛出异常。优化后的代码如下:

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton(){
        if(LazyHolder.INSTANCE !=null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }
    //每一个关键字都是有用的
    // static是为了使单例的空间共享
    //final保证这个方法不会被重写
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.INSTANCE;
    }
    private static class LazyHolder{
        private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
    }
}

编写测试代码:

package com.zyk;
import com.zyk.lazy.LazyInnerClassSingleton;
import com.zyk.lazy.LazySingleton;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class App {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 获取单例对象的字节码
        Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
        // 获取构造器
        Constructor<LazyInnerClassSingleton> constructor =
                clazz.getDeclaredConstructor();
        // 设置为可访问
        constructor.setAccessible(true);
        // 创建对象
        LazyInnerClassSingleton singleton = constructor.newInstance();
        System.out.println(singleton == LazyInnerClassSingleton.getInstance());
    }
}

运行测试代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0OnnGfZ9-1579488499166)(file:///C:/Users/19176/Documents/My Knowledge/temp/cc5c2097-9671-4890-9e62-0b4282cd8e20/128/index_files/47593281.png)]

序列化破坏单例

当我们将一个对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘内读取到对象,反序列化为内存对象。反序列化后的对象会重新分配内存,重新创建对象。如果序列化的目标对象是单例对象,就会破坏单例对象。来看下面一段代码:

public class SerializableSingleton implements Serializable {
    public static final SerializableSingleton instance = new SerializableSingleton();
    private SerializableSingleton(){
    }
    public static SerializableSingleton getInstance() {
        return instance;
    }
}

序列化:序列化就是把内存中的状态转换为字节码的形式,从而可以通过IO流写到其他地方,将内存中的数据永久的保存下来。

反序列化:就是将已经持久化的字节码内容,转化为IO流,通过IO流的读取,进而将读取的内容转换为Java对象

编写测试代码

package com.zyk.serialize;
import java.io.*;
public class SerializableTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        FileOutputStream fileOutputStream = null;
        SerializableSingleton instance = SerializableSingleton.getInstance();
        fileOutputStream = new FileOutputStream(new File("single.obj"));
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(instance);
        objectOutputStream.flush();
        objectOutputStream.close();
        FileInputStream fileInputStream = new FileInputStream(new File("single.obj"));
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Object object = objectInputStream.readObject();
        System.out.println(SerializableSingleton.getInstance());
        System.out.println(object);
        System.out.println(SerializableSingleton.getInstance() == object);
    }
}

运行结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cMfdzK2X-1579488499168)(file:///C:/Users/19176/Documents/My Knowledge/temp/cc5c2097-9671-4890-9e62-0b4282cd8e20/128/index_files/48953375.png)]

运行结果中可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的原则。解决方法,在单例模式中增加readResolve()方法即可。优化代码如下:

public class SerializableSingleton implements Serializable {
    public static final SerializableSingleton instance = new SerializableSingleton();
    private SerializableSingleton(){
    }
    public static SerializableSingleton getInstance() {
        return instance;
    }
    public Object readResolve(){
        return instance;
    }
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e9NzQySm-1579488499169)(file:///C:/Users/19176/Documents/My Knowledge/temp/cc5c2097-9671-4890-9e62-0b4282cd8e20/128/index_files/49110640.png)]

为什么这样写就能解决问题呢?我们来看下JDK的源码。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();
            }
        }
    }

在readObject中调用了一个readObject0()方法,readObject0()的代码如下所示:

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) {
            /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
            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);
    }
}

在switch判断中,看到TC_OBJECT中判断,调用了readOrdinaryObject的方法,继续看源码

private Object readOrdinaryObject(boolean unshared)
    throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }
        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();
        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }
        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }
        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }
        handles.finish(passHandle);
        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) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }
        return obj;
    }

首先会调用isInstantiable()判断构造函数是否为空,不为空就会返回true,意味着只要有无参构造函数就会实例化。继续向下看可以发现会调用hasReadResolveMethod()方法:如果有就会调用invokeReadResolve()方法。通过源码可以看出,虽然增加了readResolve()方法返回实例,解决的单例被破坏的问题。其实是实例化了两次,只不过新创建的对象没有被返回。

注册式单例

注册时单例,就是将实例都保存在一个容器中,然后通过唯一的标识来获取实例。注册式单例主要有两种写法:一种是容器缓存,一种是枚举

枚举单例代码实现:

package com.zyk.eumn;
public enum  EnumSingleton {
    INSTANCE("Java");
    private Object data;
    EnumSingleton(Object data) {
        this.data = data;
    }
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
}

编写测试代码:

package com.zyk.eumn;
import java.io.*;
public class EnumTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        FileOutputStream fileOutputStream = new FileOutputStream(new File("Enum.obj"));
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        EnumSingleton.INSTANCE.setData("Java");
        objectOutputStream.writeObject(EnumSingleton.INSTANCE);
        FileInputStream fileInputStream = new FileInputStream("Enum.obj");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Object object = objectInputStream.readObject();
        System.out.println(object == EnumSingleton.INSTANCE);
    }
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OgDRnQFM-1579488499169)(file:///C:/Users/19176/Documents/My%20Knowledge/temp/cc5c2097-9671-4890-9e62-0b4282cd8e20/128/index_files/1263296.png)]

原理:通过反编译工具Jad,反编译EnumSingleton可以得到如下代码:

public final class EnumSingleton extends Enum
{
    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }
    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(com/zyk/eumn/EnumSingleton, name);
    }
    private EnumSingleton(String s, int i)
    {
        super(s, i);
    }
    public Object getData()
    {
        return data;
    }
    public void setData(Object data)
    {
        this.data = data;
    }
    public static final EnumSingleton INSTANCE;
    private Object data;
    private static final EnumSingleton $VALUES[];
    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

可以看出枚举的实现方式是恶汉式单例的方式。所以枚举单例天生线程安全。并且Java对枚举做了特殊的处理,在使用反射处理单例的时候,Java的newInstance方法中,对单例做了处理,如果通过反射创建单例对象,就会报错。

枚举单例是推荐使用的方式。

容器实现单例模式

package com.zyk.vector;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SingletonContainer {
    private SingletonContainer() {
    }
    private static  Map<String, Object> hashMap = new HashMap<String, Object>();
    public static Object getInstance(String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        synchronized (hashMap){
            if (hashMap.containsKey(className)){
                return hashMap.get(className);
            }else {
                return hashMap.put(className,Class.forName(className).newInstance());
            }
        }
    }
}

ThreadLocal线程单例

实现代码:

package com.zyk.threadLocal;
public class SingletonThreadLocal {
    private static ThreadLocal<SingletonThreadLocal> threadLocal =
            new ThreadLocal<SingletonThreadLocal>() {
                @Override
                protected SingletonThreadLocal initialValue() {
                    return new SingletonThreadLocal();
                }
            };
    private SingletonThreadLocal() {
    }
    public static SingletonThreadLocal getInstance() {
        return threadLocal.get();
    }
}

测试代码

package com.zyk.threadLocal;
public class ThreadLocalTest {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(SingletonThreadLocal.getInstance());
        }).start();
        new Thread(() -> {
            System.out.println(SingletonThreadLocal.getInstance());
        }).start();
        new Thread(() -> {
            System.out.println(SingletonThreadLocal.getInstance());
        }).start();
    }
}

运行结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CGU2ZVf9-1579488499170)(file:///C:/Users/19176/Documents/My%20Knowledge/temp/cc5c2097-9671-4890-9e62-0b4282cd8e20/128/index_files/3114203.png)]

结论:在同一个线程中无论创建多少次获取对象的方法,获取到的实例都是同一个,在不同的线程获取的都是不同的对象。Thread的原理是将所有的对象全部放在一个ThreadLocalMap中,为每一个线程都提供对象,实际上是使用容器实现。

单例模式总结:

单例模式的要点:全局唯一一个单例,提供全局的的访问方法。从实现来看要实现构造函数私有,并且提供一个全局的访问方法。

hreadLocalTest {
public static void main(String[] args) {
new Thread(() -> {
System.out.println(SingletonThreadLocal.getInstance());
}).start();
new Thread(() -> {
System.out.println(SingletonThreadLocal.getInstance());
}).start();
new Thread(() -> {
System.out.println(SingletonThreadLocal.getInstance());
}).start();
}
}


运行结果

[外链图片转存中...(img-CGU2ZVf9-1579488499170)]

**结论:在同一个线程中无论创建多少次获取对象的方法,获取到的实例都是同一个,在不同的线程获取的都是不同的对象。Thread的原理是将所有的对象全部放在一个ThreadLocalMap中,为每一个线程都提供对象,实际上是使用容器实现。**

### 单例模式总结:

单例模式的要点:全局唯一一个单例,提供全局的的访问方法。从实现来看要实现构造函数私有,并且提供一个全局的访问方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1H2IbnaF-1579488499172)(file:///C:/Users/19176/Documents/My%20Knowledge/temp/cc5c2097-9671-4890-9e62-0b4282cd8e20/128/index_files/4194625.png)]
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!