Android设计模式-单例模式

单例模式介绍

单例模式是应用最广的模式之一。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需一个全局对象,这样有利于协调系统的整体行为。如在一个应用中,应当只有一个ImageLoader实例,这个ImageLoader中又含有线程池、缓存系统、网络请求等,很消耗资源,所以,没有理由让它构造多个实例。这种不能自由构造对象的情况,就是单例模式的使用场景。

单例模式定义

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

单例模式的使用场景

确保某个类有且只有一个对象,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如,创建一个需要消耗过多的资源过多,如访问IO和数据库等资源时,这时应考虑使用单例模式。

实现单例模式主要的关键点:

(1)构造函数不对外开放,一般为private;

(2)通过一个静态方法或枚举返回单例类对象;

(3)确保单例类的对象有且只有一个,尤其是在多线程情况下;

(4)确保单例类对象在反序列化时不会重新构建对象。

示例

下面以公司里的CEO为例来简单演示一下,一个公司可以有几个VP、无数个员工,但CEO只能有一个:

//员工
public class Staff {
    public void work() {

    }
}

//副总裁
public class VP extends Staff {
    @Override
    public void work() {

    }
}


//CEO,恶汉单例模式
public class CEO extends Staff {
    private static final CEO sCeo = new CEO();
    //构造方法私有化
    private CEO() {

    }
    //公有静态方法,对外暴露获取单例对象的接口
    public static CEO getCeo() {
        return sCEO;
    }
    @Override
    public void work() {
        //管理VP
    }
}


public class Company {
    private List<Staff> allStaffs = new ArrayList<Staff>();
    public void addStaff(Staff per) {
        allStaffs.add(per);
    }
    public void showAllStaffs() {
        for(Staff per : allStaffs) {
            System.our.println("Obj : " + per.toString());
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Company cp = new Company();
        //对象只能通过getCEO函数获取
        Staff ceo1 = CEO.getCeo();
        Staff ceo2 = CEO.getCeo();
        cp.addStaff(ceo1);
        cp.addStaff(ceo2);
        //通过new创建VP对象
        Staff vp1 = new VP();
        Staff vp2 = new VP();
        //通过new创建Staff对象
        Staff staff1 = new Staff();
        Staff staff2 = new Staff();
        Staff staff3 = new Staff();

        cp.addStaff(vp1);
        cp.addStaff(vp2);
        cp.addStaff(staff1);
        cp.addStaff(staff2);
        cp.addStaff(staff3);

        cp.showAllStaffs();

    }
}
//输出
Obj : com.android.dp.book.chapter2.company.CEO@5e8fce95
Obj : com.android.dp.book.chapter2.company.CEO@5e8fce95
Obj : com.android.dp.book.chapter2.company.VP@3343c8b3
Obj : com.android.dp.book.chapter2.company.VP@222d2a10
Obj : com.android.dp.book.chapter2.company.Staff@1aa8c488
Obj : com.android.dp.book.chapter2.company.Staff@22998b08

从上述代码看出,CEO类不能通过new的形式构造对象,只能通过CEO.getCeo()函数来获取,而这个CEO对象是静态对象,并且在声明的时候就已经初始化了(这便是恶汉模式,单例的实现方式之一),这保证了CEO对象的唯一性。

单例模式的其它实现方式

懒汉模式

懒汉模式是声明一个静态对象,并且在第一次调用静态方法时初始化,并返回实例,它与恶汉模式的区别是:恶汉模式在类加载的时候就实例化了对象,而懒汉模式在第一次使用该实例的时候才初始化。

下面是懒汉模式的实现:

public class Singleton {
    private static Singleton instance;
    private Singleton() {

    }
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

静态方法getInstance()添加了synchronized关键字,它保证了在多线程的情形下也能实例唯一,但这也是懒汉模式存在的问题:每次调用getInstance()方法都会进行同步,消耗不必要的资源。

小结:

  • 恶汉模式:优点:线程安全;缺点:在类加载时实例化,无论是否使用该单例,都会实例化,会造成一定的资源浪费。

  • 懒汉模式:优点:只在第一次使用时才初始化,且线程安全;缺点:每次使用该实例时都会进行同步,效率较低。

双检锁(Double-Check-Lock)实现单例

双检锁模式是懒汉模式的升级版本,同时克服了懒汉模式和恶汉模式的缺点:

public class Singleton {
    private static Singleton sInstance = null;
    private Singleton() {

    }
    public void doSth() {
        System.out.println("do sth.");
    }
    public static Singleton getInstance() {
        if(sInstance == null) {
            synchronized(Singleton.class) {
                if(sInstance == null) {
                    sInstance = new Singleton();

                }
            }
        }
        return sInstance;
    }
}

考虑一种极端的情况:在多线程情况下,如果有A、B两个线程同时调用getInstance()方法,A线程先进入第一个判空,发现实例为空,此时A线程挂起,B线程进入第一个判空条件,发现为空,此时B线程挂起,A线程执行,进入synchronized临界区,第二次判空,条件满足,实例初始化,临界区执行结束,A线程挂起,B线程执行,进入临界区,第二次判空时发现实例已被初始化,退出临界区,B线程挂起,A线程执行,返回实例,A线程结束,B线程执行,返回实例,B线程结束。

接下来再有线程调用getInstance时,只会进行第一个if判断,不会再进入synchronized临界区。

小结:

双检锁的优点:资源利用率高,第一次执行getInstance时才被实例化,只需同步一次。

双检锁的缺点:第一次加载时反应稍慢。

除非并发线程数很大,否则双检锁的模式一般都可以满足需求。

静态内部类实现单例模式

《Java并发编程实践》谈到了双检锁模式的问题:并发量很大时,双检锁模式不可靠。

而使用静态内部类的方式可以解决这个问题:

public class Singleton {
    private Singleton() {

    }
    public static Singleton getInstance() {
        return SingletonHolder.sInstance;
    }
    private static class SingletonHolder {
        private static final Singleton sInstance = new Singleton();

    }
}

当第一次加载Singleton类时,不会初始化sInstance(因为加载Singleton类时,静态内部类SingletonHolder 并不会被一并加载),只有在第一次调用getInstance()方法时,静态内部类才会被初始化。 这种方式既保证了线程安全性,同时也能够保证单例对象的唯一性,而且延迟了单例的实例化,这是推荐的单例模式实现方式。

枚举单例

枚举单例是一种简单的单例实现方式:

public enum SingletonEnum {
    INSTANCE;
    public void doSth() {
        System.out.println("do sth.");
    }
}

枚举也是一种类,它也可以有字段、方法,而且创建枚举类时是线程安全的,任何情况它都是一个单例。即便是在序列化反序列化时,枚举也能保证单例的正确性。

不管哪种方式实现单例模式,它们的核心原理都是将构造方法私有化,并且通过静态方法获取一个唯一的实例,在这个获取过程中,必须保证线程安全、防止反序列化导致重新生成实例对象等问题。

运用单例模式

ImageLoader是最为常用的Android开发工具之一,最著名的ImageLoader当属Universal-Image-Loader,它的大概使用流程为:

public void initImageLoader(Context context) {
    //1、使用Builder构建ImageLoader
    ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
    //加载图片的线程数
    .threadPriority(Thread.NORM_PRIORITY - 2//编码图像的大尺寸,将在内存中缓存先前解码图像的小尺寸
    .denyCacheImageMultipleSizeInMemory()
    //设置磁盘缓存文件名称
    .discCacheFileNameGenerator(new Md5FileNameGenerator())
    //设置加载显示图片队列进程
    .tasksProcessingOrder(QueueProcessingType.LIFO)
    .writeDebugLogs()
    .build();

    //2、使用配置对象初始化ImageLoader
    ImageLoader.getInstance().init(config);
    //3、加载图片
    ImageLoader.getInstance().displayImage("图片Url", myImageView);
}

下面是一个自实现的ImageLoader:

public final class ImageLoader {
    //ImageLoader实例
    private static ImageLoader sInstance;
    //网络请求队列
    private RequestQueue mImageQueue;
    //缓存
    private volatile BitmapChche = new MemoryCache()
    //图片加载配置对象
    private ImageLoaderConfig mConfig;
    //私有的构造方法
    private ImageLoader() {

    }

    /**
     * 获取ImageLoader单例,双检锁模式
     * @return 单例对象
     */
     public static ImageLoadergetInstance() {
         if(sInstance == null) {
             synchronized(ImageLoader.class) {
                 if(sInstance == null) {
                     sInstance = new ImageLoader();

                 }
             }
         }
     }
     /**
      * 通过配置类初始化ImageLoader,设置线程数量、缓存策略、加载策略等
      * @param config 配置对象
      */
      public void init(ImageLoaderConfig congig) {
          mConfig = config;
          mCache = mConfig.bitmapCache;
          checkConfig();
          mImageQueue = new RequestQueue(mConfig.threadCount);
          mImageQueue.start();
      }
      //代码策略
      //加载图片接口
      public void displayImage(ImageView imageView, String url) {
          displayImage(imageView, url, null, null);
      }
      public void displayImage(final ImageView imageView, final String url, final Displayconfig, final ImageListener listener) {
          BitmapRequest request = new BitmapRequest(imageView, url, config, listener);
          request.displayConfig = request.displayConfig != null ? request.displayConfig : mConfig.displayConfig;
          // 添加到队列中
          mImageQueue.addRequest(request);
      }
      public void stop() {
          mImageQueue.stop();
      }
      //加载图片Listener, 加载完成后回调给客户端代码
      public static interface ImageListener {
          public void onComplete(ImageView imageView, Bitmap bitmap, String url);
      } 

}

总结

单例模式是运用频率很高模式,但是,由于在客户端没有高并发的情况,所以,选择哪种实现没有太大影响,即便如此,仍然推荐使用静态内部类和枚举的实现单例的方式。

  • 单例模式的优点:

(1)由于在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁创建销毁时。

(2)减少了内存开销,一个对象需要比较多的资源时,如读取配置、产生其他依赖对象时,可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式解决。

(3)单例模式可以避免对资源的多重占用。如一个写文件操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。

(4)单例模式可以在系统设置全局的访问点,优化和共享资源。如,设计一个单例类,负责所有数据表的映射处理。

  • 缺点:

(1)单例模式一般没有借口,难以扩展。

(2)单例如果持有Context,那么很容易引发内存泄漏,此时需要注意传递给单例对象Context最好是Application Context。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值