单例模式是应用最为普遍的模式之一,也多是不少入门或初级工程师惟一会使用的设计模式之吧,在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只须要拥有一个实例类。有利于咱们的调用,避免一个相同的类重复建立实例,好比一个网络请求,图片请求/下载,数据库操做等,若是频繁建立同一个相同对象的话,很消耗资源,所以,没有理由让它们构造多个实例。全局都须要使用这个功能的时候,避免重复建立,就能够用单例,这就是单例使用场景。java
确保某个类只有一个实例,并且自行实例化并向整个系统提供这个实例。android
应用中重复使用某个类时,为了不屡次建立产生的资源消耗,那么这个时候就能够考虑使用单例设计模式。git
实现单例模式主要有以下几个关键点:github
单例模式是设计模式中比较简单的,只有一个单例类,没有其余层次结构与抽象。该模式须要确保该类只能生成一个对象,一般是该类须要消耗较多的资源或者没有多个实例的状况。例以下面的代码:数据库
public class DaoManager {
/** * 饿汉式单例 */
private static DaoManager instance = new DaoManager();
private DaoManager(){}
public static DaoManager getInstance(){
return instance;
}
}
复制代码
测试编程
@Test
public void test(){
String dao = DaoManager.getInstance().toString();
String dao1 = DaoManager.getInstance().toString();
String dao2 = DaoManager.getInstance().toString();
String dao3 = DaoManager.getInstance().toString();
System.out.println(dao);
System.out.println(dao1);
System.out.println(dao2);
System.out.println(dao3);
}
复制代码
Output设计模式
com.devyk.android_dp_code.singleton.DaoManager@28ba21f3
com.devyk.android_dp_code.singleton.DaoManager@28ba21f3
com.devyk.android_dp_code.singleton.DaoManager@28ba21f3
com.devyk.android_dp_code.singleton.DaoManager@28ba21f3
复制代码
从上面代码能够看到 DaoManager 不能经过 new 的形式构造对象,只能经过 getInstance() 拿到实例,而 DaoManager 对象是静态的,那么在声明的时候已经初始化了,这就保证了对象的惟一性,从输入结果中发现, DaoManager 四次输出的地址都是同样的。这个实现的核心在与将 DaoManager 类的构造方法私有化,使得外部程序不能经过构造来 new 对象,只能经过 getInstance() 来返回一个对象。安全
懒汉模式是声明了一个静态对象,而且在第一调用的时候进行初始化,而上面的饿汉纸则是在声明的时候已经初始化了。懒汉式的实现以下:网络
public class DaoManager2 {
private static DaoManager2 instance;
private DaoManager2(){}
/** * 保证线程安全的懒汉式 * @return */
public static synchronized DaoManager2 getInstance(){
if (null == instance) {
instance = new DaoManager2();
}
return instance;
}
}
复制代码
细心的读者可能已经发现了,getInstance() 方法中添加了 synchronized
关键字, getInstance 是一个同步方法,保证了在多线程状况下单例对象惟一性。细想下,你们可能会发现一个问题,即便 instance 已经被初始化,每次调用都会进行同步检查,这样会消耗没必要要的资源,这也是懒汉单例模式存在的最大问题。多线程
最后总结一下,懒汉单例模式的优势是单例只有再使用的时候进行初始化,在必定程度上节约了资源;缺点是第一次加载时须要进行初始化,反应稍慢,最大的问题就是每次调用的时候 getInstance 都进行同步,形成没必要要的开销。这种模式通常不建议使用。
DCL 方式实现单例模式的有点是既可以在须要时初始化单例,又能保证线程安全,且单例对象初始化后调用 instance 不进行同步锁,代码以下:
public class DaoManager3 {
private static DaoManager3 sinstance;
private DaoManager3() {
}
/** * 保证线程安全的懒汉式 * * @return */
public static DaoManager3 getInstance() {
if (null == sinstance) {
synchronized (DaoManager3.class) {
if (null == instance)
sinstance = new DaoManager3();
}
}
return sinstance;
}
}
复制代码
本段代码的亮点就在于 getInstance 方法上,能够看到 getInstance 方法对 instance 进行了两次判空;第一层判断主要是为了不没必要要的同步,第二层的判断则是为了在 null 的状况下建立实例。是否是看起来有点迷糊,下面在来解释下:
sinstance = new DaoManager3();
复制代码
这个步骤,其实在jvm里面的执行分为三步:
因为在 JDK 1.5 之前 Java 编译器容许处理器乱序执行,以及 JMM 没法保证 Cache, 寄存器(Java 内存模型)保证按照 1,2,3 的顺序执行。因此可能在 2 还没执行时就先执行了 3,若是此时再被切换到线程 B 上,因为执行了 3,sinstance 已经非空了,会被直接拿出来用,这样的话,就会出现异常。并且不易复现不易跟踪是一个隐藏的 BUG。
不过在 JDK 1.5 以后,官方也发现了这个问题,故而具体化了 volatile ,即在 JDK 1.6 之后,只要定义为 private volatile static DaoManager3 sinstance ; 就可解决 DCL 失效问题。volatile 确保 sinstance 每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。
DCL 优势:资源利用率高,第一次执行 getInstance 时单例对象才会被实例化,效率高。
DCL 缺点:第一次加载时,反应稍慢,也因为 Java 内存模型的缘由偶尔会失败。在高并发环境下也有必定的缺陷,虽然发生几率很小。
DCL 模式是使用最多的模式,它可以在须要时才被实例化,而且可以在绝大多数场景下保证单例对象的惟一性,除非你的代码在并发场景比较复杂或者低于 JDK 6 版本下使用,不然,这种方式通常可以知足需求。
DCL 虽然在必定程度上解决了资源消耗、多余的同步、线程安全等问题,可是,它仍是在某些状况下出现失效的问题,这个问题被称为双重检查锁定失效,在《Java 并发编程实践》一书的最后谈到了这个问题,并指出这种 “优化” 是丑陋的,不同意使用。而建议使用以下的代码替代。
public class DaoManager4 {
private DaoManager4(){}
public static DaoManager4 getInstance(){
return DaoManager4Holder.sInstance;
}
/** * 静态内部类 * */
private static class DaoManager4Holder{
private static final DaoManager4 sInstance = new DaoManager4();
}
}
复制代码
那么,静态内部类又是如何实现线程安全的呢?首先,咱们先了解下类的加载时机。
类加载时机:JAVA 虚拟机在有且仅有的 5 种场景下会对类进行初始化。
咱们再回头看下 getInstance() 方法,调用的是 DaoManager4Holder.sInstance ,取的是DaoManager4Holder 里的 sInstance 对象,跟上面那个 DCL 方法不一样的是 ,getInstance()方法并无屡次去 new 对象,故无论多少个线程去调用 getInstance() 方法,取的都是同一个sInstance 对象,而不用去从新建立。当 getInstance() 方法被调用时,DaoManager4Holder 才在 DaoManager4 的运行时常量池里,把符号引用替换为直接引用,这时静态对象sInstance 也真正被建立,而后再被 getInstance() 方法返回出去,这点同饿汉模式。那么sInstance 在建立过程当中又是如何保证线程安全的呢?在《深刻理解JAVA虚拟机》中,有这么一句话:
虚拟机会保证一个类的 () 方法在多线程环境中被正确地加锁、同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 () 方法,其余线程都须要阻塞等待,直到活动线程执行 () 方法完毕。若是在一个类的 () 方法中有耗时很长的操做,就可能形成多个进程阻塞 (须要注意的是,其余线程虽然会被阻塞,但若是执行 () 方法后,其余线程唤醒以后不会再次进入 () 方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞每每是很隐蔽的。
故而,能够看出 sInstance 在建立过程当中是线程安全的,因此说静态内部类形式的单例可保证线程安全,也能保证单例的惟一性,同时也延迟了单例的实例化。
那么,是否是能够说静态内部类单例就是最完美的单例模式了呢?其实否则,静态内部类也有着一个致命的缺点,就是传参的问题,因为是静态内部类的形式去建立单例的,故外部没法传递参数进去,例如 Context 这种参数,因此,咱们建立单例时,能够在静态内部类与 DCL 模式里本身斟酌。
前面讲解了几个单例模式的实现方式,这几个实现方式不是稍显麻烦就是会在某种状况下出现问题,那么还有没有更简单的实现方式勒? 咱们先来看看下面的实现方式。
public enum DaoManager5 {
INSTANCE;
public void doSomething(){
Log.i("DAO->","枚举单例");
}
}
复制代码
没错,就是枚举单例!
写法简单简单是枚举单例最大的优势,枚举在 Java 中与普通的类时同样的,不只可以拥有字段,还可以拥有本身的方法。最重要的是默认枚举实例的建立是线程安全的,而且在任何状况下它都是一个单例。
优势:枚举自己是线程安全的,且能防止经过反射和反序列化建立实例。
缺点:对 JDK 版本有限制要求,非懒加载。
学习了上面 5 大单例模式,最后在来介绍一种容器单例模式,请看下面代码实现:
public class DaoManager6 {
/** * 定义一个容器 */
private static Map<String,Object> singletonMap = new HashMap<>();
private DaoManager6(){}
public static void initDao(String key,Object instance){
if (!singletonMap.containsKey(key)){
singletonMap.put(key,instance);
}
}
public static Object getDao(String key){
return singletonMap.get(key);
}
}
复制代码
在程序的初始,能够将单例类型注入到统一管理类中,在使用的时候根据 key 获取对应单例对象,而且在使用时能够经过统一的接口进行获取操做,下降了用户的使用成本,也对用户隐藏了具体实现,下降了耦合度。
Android 源码中涉及了大量的单例模式,这里就拿较为熟悉的 context.getSystemService(String name); 容器单例模式,以 Context.LAYOUT_INFLATER_SERVICE 举例。
从 setContentView 入口,全方位分析 LayoutInflater
单例模式在应用中时属于使用频率最高的一种设计模式了,可是因为客户端一般没有高并发的状况,所以,选择哪一种实现方式并不会有太大的影响。固然,考虑效率和并发的场景仍是推荐你们使用 DCL 或 静态内部类单例模式。
注意:若是单例对象必须持有参数的话,那么最好建议使用弱引用来接收参数,若是是 Context 级别的类型,建议使用 context.getApplication() 不然容易形成内存泄漏;
感谢你的阅读,谢谢!