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); //true表示追加写入
}

public static Logger getInstance() {
return instance;
}

public void log(String message) {
writer.write(mesasge);
}
}

// Logger类的应用示例:
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 {
// AtomicLong是一个Java并发库中提供的一个原子变量类型,
// 它将一些线程不安全需要加锁的复合操作封装为了线程安全的原子操作,
// 比如下面会用到的incrementAndGet().
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();
}
}

// IdGenerator使用举例
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 才会被加载,这个时候才会创建 instanceinstance 的唯一性、创建过程的线程安全性,都由 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 是线程 IDvalue 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,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() {
//...
}
}

//l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");

这种多例模式的理解方式有点类似工厂模式。
它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象

在上面的问题中如果 IdGenerator 分为 OrderIdGeneratorUserIdGenerator ,有什么办法解决呢?

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();
}

//Order实现类
public class OrderIdGenerator implements IIdGenerator {
@Override
public long getId() {
return 1;
}
}

//User实现类
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
/**
* Obtains the LayoutInflater from the given context.
*/
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
// 1 : service容器
private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
new HashMap<String, ServiceFetcher<?>>();

// 2: 注册服务器
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);
}


// 3: 静态语句块, 第一次加载该类时执行 ( 只执行一次, 保证实例的唯一性. )
static {
// 代码省略
// 注册Activity Servicer
registerService(Context.ACTIVITY_SERVICE, ActivityManager.class,
new CachedServiceFetcher<ActivityManager>() {
@Override
public ActivityManager createService(ContextImpl ctx) {
return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler());
}});

// 注册LayoutInflater service
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
// ....代码省略
}

// 4: 根据key获取对应的服务,
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 源码设计模式解析与实战》

以及上文中的链接