单例模式

单例模式

单例模式的应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。在生活中应用也非常广泛。例如,国家主席、公司CEO等。

实现步骤:

  1. 私有化构造器
  2. 创建一个该对象的成员变量
  3. 创建一个外部可以访问的方法,用来获取该对象

饿汉式

先看代码:

public class HungrySingleton {

    private final static HungrySingleton INSTANCE = new HungrySingleton();

    private HungrySingleton() {}

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

它还可以把初始化放在静态代码块中,如下

public class HungrySingleton {

    private final static HungrySingleton INSTANCE;

    private HungrySingleton() {}

    static {
        INSTANCE = new HungrySingleton();
    }

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

饿汉式单例是在类加载的时候就初始化,并创建单例对象。线程安全,在线程还没出现的时候就已经初始化了,不存在访问安全的问题。
优点:不需要加锁,性能高。
缺点:这种方式在类加载的时候就初始化了,所以,不管你用不用这个类,它都已经被创建了,浪费了空间。

懒汉式

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton(){}

    public synchronized LazySingleton getInstance() {
        if(instance == null) {
            instance = new LazySingleton();
        }

        return instance;
    }
}

懒汉式单例可以实现懒加载,在类第一次被调用的时候才会被初始化,但是存在线程安全问题。
优点:可以实现懒加载。
缺点:存在线程安全问题,需要加锁,性能比饿汉式低。

双重检查

用synchronized加锁,在线程数较多的情况下,性能会大幅下降。我们可以做双重检查锁的单例模式:

public class DoubleCheckSingleton {

    private volatile static DoubleCheckSingleton instance;

    private DoubleCheckSingleton(){

    }

    public static DoubleCheckSingleton getInstance() {
        if(instance == null) {
            synchronized (DoubleCheckSingleton.class) {
                if(instance == null) {
                    instance = new DoubleCheckSingleton();
                }
            }
        }

        return instance;
    }
}

静态内部类

public class StaticInnerSingleton {

    private static class holder {
        private static final StaticInnerSingleton instance = new StaticInnerSingleton();
    }

    private StaticInnerSingleton(){

    }

    public final static StaticInnerSingleton getInstance() {
        return holder.instance;
    }
}

静态内部类默然不加载,在被调用时会被加载,也就是返回结果以前,一定会先加载内部类。这样既可以保证线程是安全的,又可以实现懒加载。推荐使用。

反射破坏单例

虽然我们把构造方法私有化了,然而反射依然可以破坏这种访问机制,从而创建对象,看代码

public class Test {

    public static void main(String[] args) throws Exception {

        Class<StaticInnerSingleton> clz = StaticInnerSingleton.class;
        Constructor<StaticInnerSingleton> constructor = clz.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        StaticInnerSingleton staticInnerSingleton = constructor.newInstance(null);

        System.out.println(staticInnerSingleton == StaticInnerSingleton.getInstance());
    }
}

打印的结果大家应该猜的到了,用反射机制,通过对象的构造器又创建了一个新的对象,所以结果是false,破坏了单例模式。解决方案也比较简单,我们只要在构造器中判断一下,如果对象不为空就抛出一个异常,这样通过反射就不能成功创建对象了。如下:

private StaticInnerSingleton(){
        if(holder.instance != null) {
            throw new RuntimeException("对象已存在,不允许创建多个实例");
        }
    }

然后,我们再运行下之前的测试代码,结果如下:
在这里插入图片描述
这样就完美解决的反射破坏的问题。不过,我们知道Java创建对象有好几种方式,反射、new、clone、序列化等都可以,反射和new我们都解决了,clone我们没实现,所以也没问题,我在看下序列化行不行?

public class SerializeSingletonTest {

    public static void main(String[] args) throws Exception {

        StaticInnerSingleton s1 = null;
        StaticInnerSingleton s2 = StaticInnerSingleton.getInstance();
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("StaticInnerSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();
            FileInputStream fis = new FileInputStream("StaticInnerSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (StaticInnerSingleton)ois.readObject();
            ois.close();
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

当然,这段代码要跑得通,需要StaticInnerSingleton实现Serializable接口。我们看下打印的结果。
在这里插入图片描述
结果是false,明显地址不同,有两个对象,所以还是破坏了单例模式。为什么呢?我们来看下源码。
我们先进入readObject()方法
在这里插入图片描述
再进入readObject0(false)方法中

在这里插入图片描述
对象序列化,进入TC_OBJECT,我们再点开readOrdinaryObject

在这里插入图片描述
找到重点了。上图,红色的框中的代码,我们是熟悉的,通过反射创建了一个新的对象。那么怎么解决这个问题呢?我们继续往下面看

在这里插入图片描述
我们看到这里有个判断hasReadResolveMethod()方法,我们点进去看看
在这里插入图片描述
代码简单,再看下readResolveMethod是什么时候被赋值的?
在这里插入图片描述
在这里插入图片描述
根据readResolve方法名去寻找类中这个方法,如果有则调用这个方法。看到这里,我就有解决方案了,我们只需要加一个readResolve方法就OK了,把之前的代码修改一下,如下:

public class StaticInnerSingleton implements Serializable {

    private static class holder {
        private static final StaticInnerSingleton instance = new StaticInnerSingleton();
    }

    private StaticInnerSingleton(){
        if(holder.instance != null) {
            throw new RuntimeException("对象已存在,不允许创建多个实例");
        }
    }

    public final static StaticInnerSingleton getInstance() {
        return holder.instance;
    }

    public Object readResolve() {
        return holder.instance;
    }
}

再次运行下测试类,结果如下:
在这里插入图片描述
从结果中可以看出来,反序列化创建的对象还是跟之前的对象一样,但是通过源码,我们发现在调用readResolve方法之前,已经创建了一次对象,只是被readResolve方法覆盖了而已。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,有没有更好的解决方案呢?接下来我们看下注册式单例。

注册式单例

注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种方法:一为容器缓存,一为枚举登记。先看下枚举式单例的写法:

枚举单例

public enum EnumSingleton {

    INSTANCE;

    Object data;

    public Object getData() {
        return data;
    }

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

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

测试代码:

public class Test {

    public static void main(String[] args) {
        EnumSingleton instance = EnumSingleton.getInstance();
        instance.setData(new Object());
        EnumSingleton instance1 = null;
        try{
            FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance);
            oos.flush();
            oos.close();

            FileInputStream fileInputStream = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            instance1 = (EnumSingleton)objectInputStream.readObject();
            System.out.println(instance.getData());
            System.out.println(instance1.getData());
            System.out.println(instance.getData() == instance1.getData());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果:

java.lang.Object@6d6f6e28
java.lang.Object@6d6f6e28
true

是不是很神奇,结果是true,说明它是单例的。我们分析下为什么,还是先看readObject()方法中调用的readObject0的源码
在这里插入图片描述
我们进入readEnum()方法看看

private Enum<?> readEnum(boolean unshared) throws IOException {
        if (bin.readByte() != TC_ENUM) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        if (!desc.isEnum()) {
            throw new InvalidClassException("non-enum class: " + desc);
        }

        int enumHandle = handles.assign(unshared ? unsharedMarker : null);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(enumHandle, resolveEx);
        }

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;
    }

我们发现,它是根据类名和class对象来找到一个唯一的枚举对象。因此,枚举对象不可能会被加载多次。接下里我们测试下反射

private static void testByReflect() {
        try {
            Class<EnumSingleton> enumSingletonClass = EnumSingleton.class;

            Constructor<EnumSingleton> declaredConstructor = enumSingletonClass.getDeclaredConstructor(null);
            declaredConstructor.setAccessible(true);
            EnumSingleton enumSingleton = declaredConstructor.newInstance(null);
            System.out.println(enumSingleton == EnumSingleton.getInstance());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

调用后,输出结果:
在这里插入图片描述
抛异常了,没有不带参的构造方法(因为我本地的反编译软件编译出来的东西有问题,就不展示了),所以反射也不能破坏枚举单例。其实即使有无参的构造方法也是没办法创建的,我们可以看下Constructor的newInstance()方法
在这里插入图片描述
标红的区域,会判断是否是枚举类型,如果是,则会抛出异常,反射是无法破坏枚举单例的,JDK已经做了处理。

容器缓存单例

public class ContainerSingleton {

    private final static Map<String, Object> CONTAINER = new ConcurrentHashMap<>();

    private ContainerSingleton() {

    }

    public synchronized final static Object getInstance(String name) {
        if(!CONTAINER.containsKey(name)) {
            Object o = null;
            try {
                Class<?> clazz = Class.forName(name);
                o = clazz.newInstance();
                CONTAINER.put(name, o);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return o;
        } else {
            return CONTAINER.get(name);
        }
    }
}

容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。可参考下Spring的bean管理
在这里插入图片描述

ThreadLocal 线程单例

ThreadLocal单例不能保证对象全局唯一,但是能保证对象在单个线程中是唯一的,而且线程安全。

public class ThreadLocalSingleton {

    private static final ThreadLocal<ThreadLocalSingleton> SINGLETON = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };

    private ThreadLocalSingleton(){

    }

    public final static ThreadLocalSingleton getInstance() {
        return SINGLETON.get();
    }
}

测试代码

public class Test {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());

        new Thread(new ExectorThread()).start();
        new Thread(new ExectorThread()).start();
    }

    private static class ExectorThread implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
            System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
            System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
            System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
        }
    }
}

打印结果

main:threadlocalsingleton.ThreadLocalSingleton@1b6d3586
main:threadlocalsingleton.ThreadLocalSingleton@1b6d3586
main:threadlocalsingleton.ThreadLocalSingleton@1b6d3586
main:threadlocalsingleton.ThreadLocalSingleton@1b6d3586
main:threadlocalsingleton.ThreadLocalSingleton@1b6d3586
Thread-0:threadlocalsingleton.ThreadLocalSingleton@dbefca
Thread-0:threadlocalsingleton.ThreadLocalSingleton@dbefca
Thread-0:threadlocalsingleton.ThreadLocalSingleton@dbefca
Thread-0:threadlocalsingleton.ThreadLocalSingleton@dbefca
Thread-1:threadlocalsingleton.ThreadLocalSingleton@6464d21
Thread-1:threadlocalsingleton.ThreadLocalSingleton@6464d21
Thread-1:threadlocalsingleton.ThreadLocalSingleton@6464d21
Thread-1:threadlocalsingleton.ThreadLocalSingleton@6464d21

从结果看到,每条线程的对象引用地址都是一样的。为什么会这样呢?这里需要了解ThreadLocal的实现了,ThreadLocal将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。

单例模式小结

单例模式可以保证内存里只有一个实例,减少了内存的开销;避免对资源的多重占用。原理很简单,但是了解全部细节也没那么容易。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值