如何正确的使用单例

通常,我们的单例模式都需要有一个静态的函数来获取instance,如:

public static synchronized DataManager getInstance() {
    if (null == sInstance) {
        sInstance = new DataManager();
    }
    return sInstance;
}

在使用的时候,我们可以:

DataManager.getInstnace().initialize(context);
DataManager.getInstnace().doSomething();

但是,也有这种写法,如:

public static synchronized DataManager getInstance(Context context) {
    if (null == sInstance) {
        sInstance = new DataManager(context);
    }
    return sInstance;
} 

在使用的时候,我们可以:

DataManager.getInstnace(context).doSomething();

讨论:两种方式看起来都是正确的,我们应该如何取舍呢,哪种方式更好?
支持getInstance(context)的观点:
1. 在初始化单实例时,需要必要的参数来创建对象,DataManager.getInstnace().initialize(context),那就会有一个疑问,每次getInstance()之后,我到底要不要再调用一个函数来初始化?它无法保障DataManager已被正确的初始化。

支持getInstance()的观点:
1. 单例的设计,一般不需要初始化参数来创建,如果需要一个或者多个参数来初始化,那为什么要考虑使用单例呢?使用一个普通的类不是更好吗?单例提供的是一个唯一的访问据点,更多的用来做管理类(XXXManager),维护全局数据,一般它不需要额外的参数来初始化本身。
2. 既然是单例,就不要产生歧义,如果还需要参数的话,不同的参数(不一定是context,可以是其它数据)是否会产生不同的实例?
3. 使用getInstance(context),每次传进去context,都会重新初始化(或重置)我的单例子参数吗?
4. 使用getInstance(context),在调用层次很深的类里面,我还得要缓存个context,或者调用函数里面都得加上context参数?

结论:从上来的观点来看,个人更加倾向于使用无参的getInstnace()方式。
如果你真的需要参数(可能不止一个)来构造或者初始化你的instance,可以考虑提供initialize方法,并在程序的入口处初始化,这样,在其它任何地方使用时,不需要考虑初始化问题,看上去似乎有些牵强,不过我们可以找一个具体的使用实例。我们看看大名鼎鼎的Android-Universal-Image-Loader的使用方式:

public class UILApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        initImageLoader(getApplicationContext());
    }

    public static void initImageLoader(Context context) {
        // This configuration tuning is custom. You can tune every option, you may tune 
        // some of them, or you can create default configuration by
        // ImageLoaderConfiguration.createDefault(this); method.
        ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
                .threadPriority(Thread.NORM_PRIORITY - 2)
                .denyCacheImageMultipleSizesInMemory()
                .diskCacheFileNameGenerator(new Md5FileNameGenerator())
                .diskCacheSize(50 * 1024 * 1024) // 50 Mb
                .tasksProcessingOrder(QueueProcessingType.LIFO)
                .writeDebugLogs() // Remove for release app
                .build();
        // Initialize ImageLoader with configuration.
        ImageLoader.getInstance().init(config);
    }
}

在Android源码中,有无数单例,我们也一起看看其使用方式:
\frameworks\base\core\java\android\view\WindowManagerGlobal.java

public static WindowManagerGlobal getInstance() {
    synchronized (WindowManagerGlobal.class) {
        if (sDefaultWindowManager == null) {
            sDefaultWindowManager = new WindowManagerGlobal();
        }
        return sDefaultWindowManager;
    }
}

像以下类(仅列出几个),都是采用的上面的方式,这种类型的使用是用得最广泛的:
\frameworks\base\core\java\android\app\ResourcesManager.java
\frameworks\base\core\java\android\hardware\display\DisplayManagerGlobal.java
\frameworks\base\core\java\android\webkit\CookieManager.java
\frameworks\base\core\java\android\view\accessibility\AccessibilityInteractionClient.java
\frameworks\base\core\java\android\net\http\CertificateChainValidator.java

private static class NoPreloadHolder {
    private static final CertificateChainValidator sInstance = new CertificateChainValidator();
    private static final HostnameVerifier sVerifier = HttpsURLConnection
            .getDefaultHostnameVerifier();
}

public static CertificateChainValidator getInstance() {
    return NoPreloadHolder.sInstance;
}

带参数的getInstance(Context context),也有不少,但数量要小得多:
\frameworks\base\core\java\android\view\accessibility\AccessibilityManager.java
\frameworks\base\core\java\android\hardware\location\GeofenceHardwareImpl.java
\frameworks\base\core\java\android\appwidget\AppWidgetManager.java

Context mContext;

public static AppWidgetManager getInstance(Context context) {
    synchronized (sManagerCache) {
        if (sService == null) {
            IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
            sService = IAppWidgetService.Stub.asInterface(b);
        }

        WeakReference<AppWidgetManager> ref = sManagerCache.get(context);
        AppWidgetManager result = null;
        if (ref != null) {
            result = ref.get();
        }
        if (result == null) {
            result = new AppWidgetManager(context);
            sManagerCache.put(context, new WeakReference<AppWidgetManager>(result));
        }
        return result;
    }
}

private AppWidgetManager(Context context) {
    mContext = context; // 缓存context
    mDisplayMetrics = context.getResources().getDisplayMetrics();
}

public void updateAppWidget(int[] appWidgetIds, RemoteViews views) {
    try {
        sService.updateAppWidgetIds(appWidgetIds, views, mContext.getUserId());
    }
    catch (RemoteException e) {
        throw new RuntimeException("system server dead?", e);
    }
}

上面的这个例子,每次在获取单例子时,需要带上context,用起来麻烦。但也有好处,实例在初始化后并将context缓存起来,当内部的函数需要使用context(如updateAppWidget函数),直接使用缓存的mContext即可。
如果让我来设计这个类,我有两种改造方式:

  • 不缓存context,在需要使用context的地方,让调用者以参数的形式表现出来,如下:
public void updateAppWidget(Context context, int[] appWidgetIds, RemoteViews views)
  • 缓存context,提供ininalize方法来设置context。但很明显这有比较大的漏洞,因为在使用其它方法前,我必须要确保initialize方法已被调用过,否则在调用像updateAppWidget这种需要context的方法时,其行为就不正确了。

(写到这里,其实我对我之前的观点也动摇了,因为各有优缺点,因此,还是得需要根据实际情况来做判断)

在源码中搜了一下,还有一些另类的getInstance,支持多参数,仔细看一下,发现它就起到了一个缓存的作用,严格的来讲,已经不是单例的范畴了。
\frameworks\base\core\java\android\text\method\DigitsKeyListener.java

public class DigitsKeyListener extends NumberKeyListener {
    private static DigitsKeyListener[] sInstance = new DigitsKeyListener[4];
    public DigitsKeyListener() {
        this(false, false);
    }

    public DigitsKeyListener(boolean sign, boolean decimal) {
        mSign = sign;
        mDecimal = decimal;

        int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
        mAccepted = CHARACTERS[kind];
    }

    public static DigitsKeyListener getInstance() {
        return getInstance(false, false);
    }

    public static DigitsKeyListener getInstance(boolean sign, boolean decimal) {
        int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);

        if (sInstance[kind] != null)
            return sInstance[kind];

        sInstance[kind] = new DigitsKeyListener(sign, decimal);
        return sInstance[kind];
    }
}

最后,推荐一篇经典的文章,它教你如何构造一个完美的单例。
http://www.iteye.com/topic/575052

另外,再记录一个《Head First Design Patterns》中推荐的单例:

public class Singleton {
    /**
     * The volatile keyword ensures that multiple threads
     * handle the sInstance variable correctly when it
     * is being initialized to the Singleton instance.
     */
    private volatile static Singleton sInstance;
    private Singleton() {}
    public static Singleton getInstance() {
        if(sInstance == null) {
            synchronized (Singleton.class) {
                if(sInstance == null) {
                    sInstance = new Singleton();
                }
            }
        }
        return sInstance;
    }
}
<think>好的,我需要回答用户关于模式使用时需要注意的问题。首先,根据用户提供的引用资料,模式主要有饿汉式和懒汉式两种实现方式,每种都有其优缺点和注意事项。我需要将这些信息整理出来,并确保回答结构清晰,符合用户的要求。 首先,用户提到的引用[1]和[4]详细说明了饿汉式和懒汉式的区别。饿汉式在类加载时就创建实,线程安全但可能浪费资源;懒汉式延迟加载,但需要考虑线程安全问题。引用[3]和[5]给出了双重检查锁定的实现方法,并提到volatile关键字的重要性。引用[4]还讨论了资源利用率和性能问题。这些都需要涵盖在注意事项中。 接下来,我需要组织这些点,确保每个问题都明确列出。如,线程安全是懒汉式的关键点,必须用双重检查锁和volatile来避免问题。序列化和反射攻击也是潜在问题,引用资料中没有直接提到,但根据我的知识,模式在序列化时可能被破坏,可以通过实现readResolve方法解决。反射攻击则需要防止通过反射调用构造函数,这可以通过在构造器中抛出异常来实现。此外,模式可能违反一职责原则,这也是引用[1]提到的一个坏处。 另外,用户提供的引用[4]提到资源利用率和性能,需要指出饿汉式可能影响启动时间,而懒汉式虽然节省资源但可能增加复杂性。还有,模式不适合有继承需求的场景,因为构造函数是私有的,这也是需要注意的地方。 最后,生成相关问题,参考用户之前的子,问题应该围绕模式的实现、线程安全、优缺点、应用场景等方面。需要确保问题相关且有深度,帮助用户进一步理解。 总结时,要分点列出注意事项,每个点结合引用资料和补充知识,同时保持语言自然,符合中文习惯。还要检查是否遵循了系统级指令,比如数学表达式是否正确使用$...$,但这里可能不需要数学公式。确保引用标识正确添加,如关于线程安全的部分引用[3][4][5],资源管理引用[1][4]等。</think>### 模式使用注意事项及潜在问题 模式虽然能有效管理全局唯一实,但在实际使用中需注意以下关键问题: --- #### 1. **线程安全性(懒汉式重点)** - **问题**:懒汉式(延迟加载)在多线程环境下可能因并发调用导致多次实化,破坏唯一性。 - **解决方案**: - 使用**双重检查锁定(Double-Checked Locking)**,并通过`volatile`关键字禁止指令重排序(避免返回未初始化完成的对象)[^3][^5]。 - 或改用**静态内部类**实现延迟加载(利用类加载机制保证线程安全)。 ```java public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); } } } return instance; } } ``` --- #### 2. **资源利用率与初始化时机** - **饿汉式**:类加载时即初始化实,可能占用未使用资源,影响启动速度[^4]。 - **懒汉式**:延迟加载节省资源,但首次访问时可能因初始化耗时导致性能波动(如数据库连接池)[^4]。 --- #### 3. **序列化与反序列化破坏** - **问题**:若类实现`Serializable`接口,反序列化时会创建新实。 - **解决方案**: - 重写`readResolve()`方法返回现有实: ```java protected Object readResolve() { return getInstance(); } ``` --- #### 4. **反射攻击** - **问题**:通过反射调用私有构造函数可创建新实。 - **防御方法**:在构造器中添加检查,若实已存在则抛出异常: ```java private Singleton() { if (instance != null) { throw new IllegalStateException("类禁止反射构造!"); } } ``` --- #### 5. **设计原则与扩展性** - **一职责原则**:类可能承担过多职责(如管理资源+业务逻辑),导致代码臃肿[^1]。 - **扩展性限制**:类无法通过继承扩展(构造函数私有),需通过组合或依赖注入改进[^1][^4]。 --- #### 6. **测试与依赖耦合** - **问题**:作为全局状态,可能导致测试困难(如无法隔离不同测试用的影响)。 - **改进方向**:使用依赖注入框架(如Spring)管理生命周期,降低耦合度。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值