博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
今天,我们来学习设计模式中最简单一种模式之一就是 —— 单例模式(singleton pattern) 。那么何为单例模式呢?它主要在程序中起到什么作用?为什么要使用这种模式以及它的模板代码?带着前面几个问题,我们一一解剖这个所谓的单例模式。
单例模式(singleton pattern)
单例,从命名的名词角度来解释,它的意思是单独的一个实例。没错,其实它就是这个意思。而在我们程序中,它也是仅仅存在的一个全局的实例,也就是实例化一次。它的创建只能通过自身来实例化,仅提供外部的一个唯一的访问自己的方法。
1、特点
- 仅有一个实例
- 自己实例化自己
- 提供一个唯一访问自身实例的方法
2、作用
保证自身的唯一性,以确保数据的准确、唯一。在多线程情况下,多次访问数据对其进行操作则会造成数据紊乱,而使用单例则能确保数据是准确了。
3、实现方式
单例模式的实现方式主要有4种,也是我们最经常写的几种,我们分别看它们的实现方式以及区别。
一、懒汉式(线程安全)
优点:使用时才实例化,避免内存开销,可以做到随用随实例化。
缺点:虽然保证了线程安全,但 synchronized 关键字会导致效率降低,因为每个线程进来时都必须枷锁。
/**
* @Created by xww.
* @Creation time 2018/8/14.
*/
public class Singleton {
private static Singleton instance;
public static synchronized Singleton getIns() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
二、饿汉式(线程安全)
优点:没有 synchronized 枷锁,运行效率提升。
缺点:在类加载时就被实例化,导致内存开销增大。
/**
* @Created by xww.
* @Creation time 2018/8/14.
*/
public class Singleton {
private static final Singleton instance = new Singleton();
public static Singleton getIns() {
return instance;
}
}
三、双重校验锁(线程安全)
优点:使用双重校验锁机制,于懒汉模式有所优化,在多线程中依然保持高效率,而且在使用时才实例化,综合以上两个优缺点于一身。这里对 instance 变量加不加 volatile 关键字是区别比较大的。
注意:
如果不加 volatile 关键字,线程并不是绝对的安全,因为在 jdk1.5 版本后,声明变量与实例化是并发的,也就是说有可能变量声明后,实例化还未完成,导致变量的引用还是为空,就可能造成多个实例。volatile 也是在 jdk1.5 版本后出现的,可以解决这个问题。
/**
* @Created by xww.
* @Creation time 2018/8/14.
*/
public class Singleton {
private volatile static Singleton instance;
public static Singleton getIns() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
四、静态内部类(线程安全)
优点:在使用时才实例化,而且也是线程安全的,因为 new 只做了一次,综合以上两个优缺点于一身。与双重校验锁的方式相比,实现方式简单了点,代码也比较优雅,推荐使用!
/**
* @Created by xww.
* @Creation time 2018/8/14.
*/
public class Singleton {
private static class Holder {
private static final Singleton instance = new Singleton();
}
public static Singleton getIns() {
return Holder.instance;
}
}
五、枚举类(线程安全)
枚举类也是线程安全的,它最简洁,是单例模式的最佳方式,支持序列化机制,防止被多次的实例化。而以上几种单利模式在序列化时都会出现多个实例的情况发生。
-
几种单例模式的比较
- 一般建议使用饿汉式,不建议使用懒汉式。
- 如果在类装载过于耗时的处理时,使用静态内部类方式,要用才实例化,也可以用双重校验锁方式。
- 如果需要反序列化,用枚举类的方式。
-
单例模式的运用 — 国王只有一位
在多线程并发的情况下去获取一个国王类,可能就会导致国王被重复的创建,因为国王只有一位,所以要设置为单利,下面我们来看看单利模式如何应用吧,我就使用优雅的静态内部类来编写这个例子,其他几种方式也都一样,所以我就不重复的去写例子了,大家自己可以去尝试!
国王类:
package com.xww.dp.singleton;
/**
* 静态内部类
* @author xww
*
*/
public class King {
private King(){
System.out.println("国王被创建了");
}
private static final class Holder {
private static King instance = new King();
}
public static King getInstance() {
return Holder.instance;
}
}
多线程去创建国王类,获取国王的实例:
package com.xww.dp.singleton;
/**
* 23种设计模式 —— 单利模式
*
* @author xww
*
*/
public class SingletonPatternClient {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
King.getInstance();
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
King.getInstance();
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
打印日志如下:
在第一次运行时,国王类会被创建,往后无论哪个线程再去调用 getInstance() 方法时,始终返回的国王类的实例只存在一个,这就是单利模式的应用。
看看源码中的单利模式 —— AsyncTask
AsyncTask 是 Android SDK 中提供的一个异步处理操作,想要了解的可以看看我这篇写的对 AsyncTask 源码分析,在源码中初始化线程池操作就是一个典型的单利模式,代码如下:
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// We want at least 2 threads and at most 4 threads in the core pool,
// preferring to have 1 less than the CPU count to avoid saturating
// the CPU with background work
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE_SECONDS = 30;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
/**
* An {@link Executor} that can be used to execute tasks in parallel.
*/
public static final Executor THREAD_POOL_EXECUTOR;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
上面的代码用到了是一个 static 修饰的代码块,而这部分只会被创建一次,所以我们的单利模式并不是只有前面所讲的那几种写法,只要符合单利模式的特点,就可以被称作单利模式,至于代码如何写,其实并不重要,规范法其实也就是让别人都我们的代码的时候,不至于看不懂。
不仅如此,AsyncTask 还用到了我们之前所说的枚举类作为 Task 的状态标记,我们知道枚举就是一个单利,并且它还是线程安全的,看看源码如何写:
/**
* Indicates the current status of the task. Each status will be set only once
* during the lifetime of a task.
*/
public enum Status {
/**
* Indicates that the task has not been executed yet.
*/
PENDING,
/**
* Indicates that the task is running.
*/
RUNNING,
/**
* Indicates that {@link AsyncTask#onPostExecute} has finished.
*/
FINISHED,
}
使用时:
private volatile Status mStatus = Status.PENDING;
mStatus = Status.RUNNING;
mStatus = Status.FINISHED;
好了,到此为止,我们从单利模式的介绍及作用开始,并书写的几种常见的单利模式以及它们的优缺点,还写了一个单利模式的例子,验证在多线程并发时,单利模式永远只有一个实例,最后还看了 AsyncTask 的源码里面是如何使用单利模式的,单利模式其实就是这么简单。