前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >单例模式的六种写法

单例模式的六种写法

作者头像
Rouse
发布2019-08-13 23:55:47
3.8K0
发布2019-08-13 23:55:47
举报
文章被收录于专栏:Android补给站Android补给站

作者:酸辣汤 链接:https://juejin.im/post/5d484e2ff265da03ec2e4a47

1

定义

确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例

2

UML结构图

3

场景

确保某个类只有一个对象的场景,比如一个对象需要消耗的资源过多,访问io、数据库,需要提供全局配置的场景

4

几种单例模式

4.1

饿汉式

声明静态时已经初始化,在获取对象之前就初始化

优点:获取对象的速度快,线程安全(因为虚拟机保证只会装载一次,在装载类的时候是不会发生并发的)

缺点:耗内存(若类中有静态方法,在调用静态方法的时候类就会被加载,类加载的时候就完成了单例的初始化,拖慢速度)

代码语言:javascript
复制
代码语言:javascript
复制
 1public class EagerSingleton {
 2    //饿汉单例模式
 3    //在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
 4    private static EagerSingleton instance = new EagerSingleton();//静态私有成员,已初始化
 5
 6    private EagerSingleton() 
 7    {
 8        //私有构造函数
 9    }
10
11    public static EagerSingleton getInstance()    //静态,不用同步(类加载时已初始化,不会有多线程的问题)
12    {
13        return instance;
14    }
15
16}

4.2

懒汉式

synchronized同步锁:多线程下保证单例对象唯一性

优点:单例只有在使用时才被实例化,一定程度上节约了资源

缺点:加入synchronized关键字,造成不必要的同步开销。不建议使用。

代码语言:javascript
复制
代码语言:javascript
复制
 1    //懒汉式单例模式
 2    //比较懒,在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢
 3    private static LazySingleton intance = null;//静态私用成员,没有初始化
 4
 5    private LazySingleton()
 6    {
 7        //私有构造函数
 8    }
 9
10    public static synchronized LazySingleton getInstance()    //静态,同步,公开访问点
11    {
12        if(intance == null)
13        {
14            intance = new LazySingleton();
15        }
16        return intance;
17    }
18}
代码语言:javascript
复制

4.3

Double Check Lock(DCL)实现单例(使用最多的单例实现之一)

(双重锁定体现在两次判空)

优点:既能保证线程安全,且单例对象初始化后调用getInstance不进行同步锁,资源利用率高

缺点:第一次加载稍慢,由于Java内存模型一些原因偶尔会失败,在高并发环境下也有一定的缺陷,但概率很小。

代码示例:

代码语言:javascript
复制
代码语言:javascript
复制
 1public class SingletonKerriganD {
 2
 3    /**
 4     * 单例对象实例
 5     */
 6    private volatile static SingletonKerriganD instance = null;//这里加volatitle是为了避免DCL失效
 7
 8    //DCL对instance进行了两次null判断
 9    //第一层判断主要是为了避免不必要的同步
10    //第二层的判断则是为了在null的情况下创建实例。
11    public static SingletonKerriganD getInstance() {
12        if (instance == null) {
13            synchronized (SingletonKerriganD.class) {
14                if (instance == null) {
15                    instance = new SingletonKerriganD();
16
17            }
18        }
19        return instance;
20    }
21}
什么是DCL失效问题?

假如线程A执行到instance = new SingletonKerriganD(),大致做了如下三件事:

  1. 给实例分配内存
  2. 调用构造函数,初始化成员字段
  3. 将instance 对象指向分配的内存空间(此时sInstance不是null)

如果执行顺序是1-3-2,那多线程下,A线程先执行3,2还没执行的时候,此时instance!=null,这时候,B线程直接取走instance ,使用会出错,难以追踪。JDK1.5及之后的volatile 解决了DCL失效问题(双重锁定失效)

4.4

静态内部类单例模式

由于在调用 SingletonHolder.instance 的时候,才会对单例进行初始化,而且通过反射,是不能从外部类获取内部类的属性的。所以这种形式,很好的避免了反射入侵。

优点:线程安全、保证单例对象唯一性,同时也延迟了单例的实例化,避免了反射入侵

缺点:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久代的对象。(综合来看,私以为这种方式是最好的单例模式)

代码语言:javascript
复制
代码语言:javascript
复制
 1public class Singleton {
 2    private Singleton(){
 3
 4    }
 5    private static class SingletonHolder{
 6        private final static Singleton instance=new Singleton();
 7    }
 8    public static Singleton getInstance(){
 9        return SingletonHolder.instance;
10    }
11}
这种方式如何保证单例且线程安全?

当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,内部类SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

这种方式能否避免反射入侵?

答案是:不能。网上很多介绍到静态内部类的单例模式的优点会提到“通过反射,是不能从外部类获取内部类的属性的。所以这种形式,很好的避免了反射入侵”,这是错误的,反射是可以获取内部类的属性(想了解更多反射的知识请看 java反射全解),入侵单例模式根本不在话下,直接看下面的例子:

单例类如下:

代码语言:javascript
复制
代码语言:javascript
复制
 1package eft.reflex;
 2
 3public class Singleton {
 4
 5    private int a;
 6
 7    private Singleton(){
 8        a=123;
 9    }
10    private static class SingletonHolder{
11        private final static Singleton instance=new Singleton();
12    }
13    public static Singleton getInstance(){
14        return SingletonHolder.instance;
15    }
16
17    public int getTest(){
18        return a;
19    }
20}

入侵与测试代码如下:

代码语言:javascript
复制
代码语言:javascript
复制
 1    public static void main(String[] args) throws Exception {
 2        //通过反射获取内部类SingletonHolder的instance实例fInstance
 3        Class cInner=Class.forName("eft.reflex.Singleton$SingletonHolder");
 4        Field fInstance=cInner.getDeclaredField("instance");
 5
 6        //将此域的final修饰符去掉
 7        Field modifiersField = Field.class.getDeclaredField("modifiers");
 8        modifiersField.setAccessible(true);
 9        modifiersField.setInt(fInstance, fInstance.getModifiers() & ~Modifier.FINAL);
10
11        //打印单例的某个属性,接下来要通过反射去篡改这个值
12        System.out.println("a="+ Singleton.getInstance().getTest());
13
14        //获取该单例的a属性fieldA
15        fInstance.setAccessible(true);
16        Field fieldA=Singleton.class.getDeclaredField("a");
17
18        //通过反射类构造器创建新的实例newSingleton(这里因为无参构造函数是私有的,不能通过Class.newInstance创建实例)
19        Constructor constructor=Singleton.class.getDeclaredConstructor();
20        constructor.setAccessible(true);
21        Singleton newSingleton= (Singleton) constructor.newInstance();
22
23        //让fInstance指向新的实例newSingleton,此时我们的单例已经被偷梁换柱了!
24        fInstance.set(null,newSingleton);
25        //为盗版的单例的属性a设置新的值
26        fieldA.setAccessible(true);
27        fieldA.set(newSingleton,888);
28
29        //测试是否成功入侵
30        System.out.println("被反射入侵后:a="+ Singleton.getInstance().getTest());
31        fieldA.set(newSingleton,777);
32        System.out.println("被反射入侵后:a="+ Singleton.getInstance().getTest());
33}

输出结果:

代码语言:javascript
复制
代码语言:javascript
复制
1a=123
2被反射入侵后:a=888
3被反射入侵后:a=777
代码语言:javascript
复制

注意:上述四种方法要杜绝在被反序列化时重新声明对象,需要加入如下方法:

代码语言:javascript
复制
代码语言:javascript
复制
代码语言:javascript
复制
1private Object readResolve() throws ObjectStreamException{
2    return sInstance;
3}

为什么呢?因为当JVM从内存中反序列化地"组装"一个新对象时,自动调用 readResolve方法来返回我们指定好的对象

4.5

枚举单例

枚举反序列化不会生成新的实例

优点:线程安全

缺点:枚举耗内存,能不用枚举就不用

代码语言:javascript
复制
代码语言:javascript
复制
 1class Resource{
 2}
 3
 4public enum SomeThing {
 5    INSTANCE;
 6    private Resource instance;
 7    SomeThing() {
 8        instance = new Resource();
 9    }
10    public Resource getInstance() {
11        return instance;
12    }
13}

获取资源的方式很简单,只要 SomeThing.INSTANCE.getInstance() 即可获得所要实例。

这种方式如何保证单例?

首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。

上面示例中的枚举字节码文件如下:

代码语言:javascript
复制
代码语言:javascript
复制
1...
2public static final eft.reflex.SomeThing INSTANCE;
3    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
4
5...

可以看出,会自动生成 ACC_STATIC, ACC_FINAL这两个修饰符

枚举类型为什么是线程安全的?

我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。

4.6

使用容器实现单例模式

在程序的初始化,将多个单例类型注入到一个统一管理的类中,使用时通过key来获取对应类型的对象,这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行操作。这种方式是利用了Map的key唯一性来保证单例。

代码语言:javascript
复制
代码语言:javascript
复制
 1public class SingletonManager { 
 2
 3 private static Map<String,Object> map=new HashMap<String, Object>(); 
 4
 5 private SingletonManager(){}
 6
 7 public static void registerService(String key,Object instance){
 8     if (!map.containsKey(key)){
 9         map.put(key,instance); 
10     } 
11 } 
12
13 public static Object getService(String key){ 
14    return map.get(key); 
15 } 
16
17}
代码语言:javascript
复制

5

总结

所有单例模式需要处理得问题都是:

  1. 将构造函数私有化
  2. 通过静态方法获取一个唯一实例
  3. 保证线程安全
  4. 防止反序列化造成的新实例等。

推荐使用:DCL、静态内部类

5.1

单例模式优点

  1. 只有一个对象,内存开支少、性能好(当一个对象的产生需要比较多的资源,如读取配置、产生其他依赖对象时,可以通过应用启动时直接产生一个单例对象,让其永驻内存的方式解决)
  2. 避免对资源的多重占用(一个写文件操作,只有一个实例存在内存中,避免对同一个资源文件同时写操作)
  3. 在系统设置全局访问点,优化和共享资源访问(如:设计一个单例类,负责所有数据表的映射处理)

5.2

单例模式缺点

  1. 一般没有接口,扩展难
  2. android中,单例对象持有Context容易内存泄露,此时需要注意传给单例对象的Context最好是Application Context

6

android源码中的单例模式

单例模式应用广泛,根据实际业务需求来,这里只引出源码中个别场景,不再详解,有兴趣的读者可以深入查看源码

在平时的Android开发中,我们经常会通过Context来获取系统服务,比如ActivityManagerService,AccountManagerService等系统服务,实际上ContextImpl也是通过SystemServiceRegistry.getSystemService来获取具体的服务,SystemServiceRegistry是个final类型的类。这里使用容器实现单例模式

SystemServiceRegistry 部分代码:

代码语言:javascript
复制
代码语言:javascript
复制
 1final class SystemServiceRegistry {
 2    private static final HashMap<Class<?>, String> SYSTEM_SERVICE_NAMES = new HashMap<Class<?>, String>();
 3    private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS = new HashMap<String, ServiceFetcher<?>>();
 4    private SystemServiceRegistry() { }
 5
 6    static {
 7        registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
 8                new CachedServiceFetcher<LayoutInflater>() {
 9            @Override
10            public LayoutInflater createService(ContextImpl ctx) {
11                return new PhoneLayoutInflater(ctx.getOuterContext());
12            }});
13        registerService(Context.ACTIVITY_SERVICE, ActivityManager.class,
14                new CachedServiceFetcher<ActivityManager>() {
15            @Override
16            public ActivityManager createService(ContextImpl ctx) {
17                return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler());
18            }});
19        .......
20    }
21
22    public static Object getSystemService(ContextImpl ctx, String name) {
23        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
24        return fetcher != null ? fetcher.getService(ctx) : null;
25    }
26    private static <T> void registerService(String serviceName, Class<T> serviceClass, ServiceFetcher<T> serviceFetcher) {
27        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
28        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
29    }
30    ......
31}

WindowManagerGlobal:

代码语言:javascript
复制
1public static WindowManagerGlobal getInstance() {
2    synchronized (WindowManagerGlobal.class) {
3        if (sDefaultWindowManager == null) {
4            sDefaultWindowManager = new WindowManagerGlobal();
5        }
6        return sDefaultWindowManager;
7    }
8}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android补给站 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是DCL失效问题?
  • 这种方式如何保证单例且线程安全?
  • 这种方式能否避免反射入侵?
  • 这种方式如何保证单例?
  • 枚举类型为什么是线程安全的?
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档