1 为什么要使用单例? 为什么我们需要单例这种设计模式?它能解决哪些问题?
比如文件访问、数据库访问。
在多线程环境下,如果两个线程同时给同一个共享变量加 1,因为共享变量是竞争资源,所以,共享变量最后的结果有可能并不是加了 2,而是只加了 1。
我们可以用类级别锁 来解决这个问题,因为在不同线程创建不同的对象,所以对象锁 是没有用的。
我们还可以使用并发队列 BlockingQueue ,但是实现起来比较麻烦。
最后比较简单的解决思路就是使用单例模式 了,单例模式 相对于之前类级别锁 的好处是,不用创建那么多对象,一方面节省内存空间,另一方面节省系统文件句柄(对于操作系统来说,文件句柄也是一种资源,不能随便浪费)。
举个栗子🌰 文件日志,在多线程情况下肯定也会出现上面的问题,线程同时执行,会覆盖前一次的写入。但是单例就没问题了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class Logger { private FileWriter writer; private static final Logger instance = new Logger(); private Logger () { File file = new File("/Users/wangzheng/log.txt" ); writer = new FileWriter(file, true ); } public static Logger getInstance () { return instance; } public void log (String message) { writer.write(mesasge); } } public class UserController { public void login (String username, String password) { Logger.getInstance().log(username + " logined!" ); } } public class OrderController { public void create (OrderVo order) { Logger.getInstance().log("Created a order: " + order.toString()); } }
从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。
举个栗子🌰,唯一递增 ID 号码生成器,如果程序中有两个对象,那就会存在生成重复 ID 的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.util.concurrent.atomic.AtomicLong;public class IdGenerator { private AtomicLong id = new AtomicLong(0 ); private static final IdGenerator instance = new IdGenerator(); private IdGenerator () {} public static IdGenerator getInstance () { return instance; } public long getId () { return id.incrementAndGet(); } } long id = IdGenerator.getInstance().getId();
2 如何实现一个单例? 2.1 饿汉式 在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载(不是在真正用到 IdGenerator 的时候创建实例),从名字中我们也可以看出这一点。
1 2 3 4 5 6 7 8 9 10 11 public class IdGenerator { private AtomicLong id = new AtomicLong(0 ); private static final IdGenerator instance = new IdGenerator(); private IdGenerator () {} public static IdGenerator getInstance () { return instance; } public long getId () { return id.incrementAndGet(); } }
因为不支持延迟加载,导致实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。有些需要提前初始化的时候可以这样用,在真正使用的时候节省了时间。
2.2 懒汉式 有饿汉式,对应的,就有懒汉式。懒汉式相对于饿汉式的优势是支持延迟加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class IdGenerator { private AtomicLong id = new AtomicLong(0 ); private static IdGenerator instance; private IdGenerator () {} public static synchronized IdGenerator getInstance () { if (instance == null ) { instance = new IdGenerator(); } return instance; } public long getId () { return id.incrementAndGet(); } }
不过懒汉式的缺点也很明显,我们给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。 如果这个单例类偶尔会被用到,那这种实现方式还可以接受。 但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。
2.3 双重检测 饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class IdGenerator { private AtomicLong id = new AtomicLong(0 ); private static IdGenerator instance; private IdGenerator () {} public static IdGenerator getInstance () { if (instance == null ) { synchronized (IdGenerator.class) { if (instance == null ) { instance = new IdGenerator(); } } } return instance; } public long getId () { return id.incrementAndGet(); } }
在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。
这里有一个问题,在低版本 Java 中,因为指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。 要解决这个问题,我们需要给 instance 成员变量加上 volatile 关键字,禁止指令重排序才行。
2.4 静态内部类 静态内部类比双重检测更加简单,利用 Java 静态内部类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class IdGenerator { private AtomicLong id = new AtomicLong(0 ); private IdGenerator () {} private static class SingletonHolder { private static final IdGenerator instance = new IdGenerator(); } public static IdGenerator getInstance () { return SingletonHolder.instance; } public long getId () { return id.incrementAndGet(); } }
只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。
2.5 枚举 还有一种最简单的实现方式,基于枚举类型的单例实现。
1 2 3 4 5 6 7 8 public enum IdGenerator { INSTANCE; private AtomicLong id = new AtomicLong(0 ); public long getId () { return id.incrementAndGet(); } }
这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。
3 如何实现线程唯一的单例? 定义中提到, “一个类只允许创建唯一一个对象” 。那对象的唯一性的作用范围是什么呢?是指线程内只允许创建一个对象,还是指进程内只允许创建一个对象?答案是后者,也就是说,单例模式创建的对象是进程唯一的。
那如何实现一个线程唯一的单例呢?
在代码中,我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class IdGenerator { private AtomicLong id = new AtomicLong(0 ); private static final ConcurrentHashMap<Long, IdGenerator> instances = new ConcurrentHashMap<>(); private IdGenerator () {} public static IdGenerator getInstance () { Long currentThreadId = Thread.currentThread().getId(); instances.putIfAbsent(currentThreadId, new IdGenerator()); return instances.get(currentThreadId); } public long getId () { return id.incrementAndGet(); } }
4 单例存在哪些问题? 大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、公共类等。 单例模式书写简洁、使用方便,在代码中,我们不需要创建对象,直接通过类似 IdGenerator.getInstance().getId() 这样的方法来调用就可以了。 但是,这种使用方法有点类似硬编码(hard code),会带来诸多问题。
4.1 单例对代码的扩展性不友好 单例这种设计模式对于抽象、继承、多态都支持得不好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Order { public void create (...) { long id = IdGenerator.getInstance().getId(); } } public class User { public void create (...) { long id = IdGenerator.getInstance().getId(); } }
如果未来某一天,我们希望针对不同的业务采用不同的 ID 生成算法。比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。为了应对这个需求变化,我们需要修改所有用到 IdGenerator 类的地方,这样代码的改动就会比较大。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Order { public void create (...) { long id = IdGenerator.getInstance().getId(); long id = OrderIdGenerator.getIntance().getId(); } } public class User { public void create (...) { long id = IdGenerator.getInstance().getId(); long id = UserIdGenerator.getIntance().getId(); } }
4.2 单例对代码的可测试性不友好 单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。
除此之外,如果单例类持有成员变量(比如 IdGenerator 中的 id 成员变量),那它实际上相当于一种全局变量,被所有的代码共享。如果这个全局变量是一个可变全局变量,也就是说,它的成员变量是可以被修改的,那我们在编写单元测试的时候,还需要注意不同测试用例之间,修改了单例类中的同一个成员变量的值,从而导致测试结果互相影响的问题。
5 如何实现一个多例模式? 跟单例模式概念相对应的还有一个多例模式。那如何实现一个多例模式呢?
“单例” 指的是,一个类只能创建一个对象。对应地,“多例” 指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class BackendServer { private long serverNo; private String serverAddress; private static final int SERVER_COUNT = 3 ; private static final Map<Long, BackendServer> serverInstances = new HashMap<>(); static { serverInstances.put(1L , new BackendServer(1L , "192.134.22.138:8080" )); serverInstances.put(2L , new BackendServer(2L , "192.134.22.139:8080" )); serverInstances.put(3L , new BackendServer(3L , "192.134.22.140:8080" )); } private BackendServer (long serverNo, String serverAddress) { this .serverNo = serverNo; this .serverAddress = serverAddress; } public BackendServer getInstance (long serverNo) { return serverInstances.get(serverNo); } public BackendServer getRandomInstance () { Random r = new Random(); int no = r.nextInt(SERVER_COUNT)+1 ; return serverInstances.get(no); } }
还有一种理解方式:同一类型的只能创建一个对象,不同类型的可以创建多个对象。这里的 “类型” 如何理解呢?
在代码中,logger name 就是刚刚说的 “类型” ,同一个 logger name 获取到的对象实例是相同的,不同的 logger name 获取到的对象实例是不同的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Logger { private static final ConcurrentHashMap<String, Logger> instances = new ConcurrentHashMap<>(); private Logger () {} public static Logger getInstance (String loggerName) { instances.putIfAbsent(loggerName, new Logger()); return instances.get(loggerName); } public void log () { } } Logger l1 = Logger.getInstance("User.class" ); Logger l2 = Logger.getInstance("User.class" ); Logger l3 = Logger.getInstance("Order.class" );
这种多例模式的理解方式有点类似工厂模式。 它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象
在上面的问题中如果 IdGenerator 分为 OrderIdGenerator 和 UserIdGenerator ,有什么办法解决呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public interface IIdGenerator { long getId () ; } public class OrderIdGenerator implements IIdGenerator { @Override public long getId () { return 1 ; } } public class UserIdGenerator implements IIdGenerator { @Override public long getId () { return 2 ; } } public class IdGeneratorPresenter { private static final Map<Long, IIdGenerator> imgInstances = new HashMap<>(); public static final Long ORDER = 1L ; public static final Long USER = 2L ; static { imgInstances.put(ORDER, new OrderIdGenerator()); imgInstances.put(USER, new UserIdGenerator()); } public static IIdGenerator getInstance (Long type) { return imgInstances.get(type); } } IdGeneratorPresenter.getInstance(IdGeneratorPresenter.ORDER).getId() IdGeneratorPresenter.getInstance(IdGeneratorPresenter.USER).getId()
类似于简单工厂和单例的结合。
6 Android 源码中的单例模式实现 我们来看看 LayoutInflater 是如何拿到的。
通常我们使用 LayoutInflater.from(Context) 来获取 LayoutInflater 服务, 下面我们看看 LayoutInflater.from(Context) 的实现。
1 2 3 4 5 6 7 8 9 10 public static LayoutInflater from (Context context) { LayoutInflater LayoutInflater =(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (LayoutInflater == null ) { throw new AssertionError("LayoutInflater not found." ); } return LayoutInflater; }
可以看到 from(Context) 函数内部调用的是 Context 类的 getSystemService(String key) 方法,我们跟踪到 Context 类看到,该类是抽象类。Context的实现类为 ComtextImpl 类。我们继续跟踪到 ContextImpl 类。
1 2 3 4 @Override public Object getSystemService (String name) { return SystemServiceRegistry.getSystemService(this , name); }
查看 SystemServiceRegistry 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS = new HashMap<String, ServiceFetcher<?>>(); private static <T> void registerService (String serviceName, Class<T> serviceClass, ServiceFetcher<T> serviceFetcher) { SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName); SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher); } static { registerService(Context.ACTIVITY_SERVICE, ActivityManager.class, new CachedServiceFetcher<ActivityManager>() { @Override public ActivityManager createService (ContextImpl ctx) { return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler()); }}); registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class, new CachedServiceFetcher<LayoutInflater>() { @Override public LayoutInflater createService (ContextImpl ctx) { return new PhoneLayoutInflater(ctx.getOuterContext()); }}); } public static Object getSystemService (ContextImpl ctx, String name) { ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name); return fetcher != null ? fetcher.getService(ctx) : null ; }
从 ContextImpl 类的部分代码中可以看到,在虚拟机第一次加载该类时会注册各种服务,其中就包含了 LayoutInflater Service,将这些服务以键值对的形式存储在一个 HashMap 中,用户使用时只需要根据 key 来获取到对应的服务,从而达到单例的效果。
源码中和我之前写的 IIdGenerator 类似 但是源码在初始化的时候并没有真正的 new PhoneLayoutInflater() 而在 getSystemService 的时候才去创建实例( getService 中的 service 不会创建第二次),所以我之前写的代码还可以优化延迟加载。 源码中在实例化的时候封装了一层属于多对象单例 ,我写的属于利用多态实现的单对象多例 。
系统核心服务以单例形式存在,并且延迟加载,减少了资源消耗。再次为源码点赞!
7 总结 优点
由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决;
单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
缺点
单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
感谢
设计模式之美
《Android 源码设计模式解析与实战》
以及上文中的链接