一、介绍:
单例模式是应用最广的模式之一;在应用这个模式时,单例对象的类必须保证只有一个实例的存在;许多时候,整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为;如在一个应用中,应该只有一个ImageLoader实例,这个ImageLoder中有含有线程池、缓存系统、网络请求等,很消耗资源,很消耗资源,因此,没有理由让他构造多个实例。这种不能自由构造对象的情况,其实就是单例模式的使用场景;
二、定义
当前进程确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例;
三、单例模式使用的场景
确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如:创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源,这时就要考虑使用单例模式;
四、单例模式的关键点
1、类的构造函数不对外开放,一般为Private;
2、通过一个静态方法或者枚举返回单例类对象;
3、确保单例类对象有且只有一个,特别是在多线程的环境下;
4、确保单例类对象在反序列化时不会重新构建对象;
单例模式的七种写法
- 饿汉式(线程安全)
类加载的时候就进行了初始化,容易浪费内存,它基于classloader 机制避免了多线程的同步问题!非懒加载
public class Singleton implements Serializable {
private static Singleton instance = new Singleton() ;
private Singleton(){}
public static Singleton getInstance(){
return instance ;
}
//防止单例对象在反序列化时重新生成对象
private Object readResolve() throws ObjectStreamException {
return instance ;
}
}
object Singleton : Serializable {
fun doSomething(){
println("do something")
}
//防止单例对象在反序列化时重新生成对象
private fun readResolve():Any{
return Singleton
}
}
- 懒汉式(线程不安全)
最简单的单例实现,为懒加载实现, 但不支持多线程,容易造成线程不安全。因为没有加锁,严格来说不算单例!
/**
* 懒汉式 线程不安全
*/
public class Singleton {
private static Singleton instance ;
private Singleton(){}
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance ;
}
}
//懒汉式: 线程不安全
class Singleton private constructor() {
companion object{
private var mInstance : Singleton? = null
get() {
return field?: Singleton()
}
@JvmStatic
fun getInstance() : Singleton{
return requireNotNull(mInstance)
}
}
fun doSomething(){
println("do something")
}
}
- 懒汉式(方法加锁,线程安全)
在上一种实现方式上,在获取单例的方法上加锁
synchronized
关键字,保证单例的实现,是懒加载实现,能够很好的在多线程中工作,第一次调用才初始化,避免内存浪费,但是效率很低,因为方法加锁会影响效率!
/**
* 懒汉式 方法加锁 线程安全
*/
public class Singleton {
private static Singleton instance ;
private Singleton(){}
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance ;
}
}
//懒汉式,方法加锁的懒汉式,线程安全
class Singleton private constructor() : Serializable {
companion object {
private var mInstance : Singleton ? = null
get() {
return field?: Singleton()
}
@JvmStatic
@Synchronized //添加同步锁
fun getInstance() : Singleton {
return requireNotNull(mInstance)
}
}
//防止单例对象在反序列化时生成新的对象
private fun readResolve():Any{
return Singleton.getInstance()
}
fun doSomething(){
println("do something")
}
//kotlin调用
fun test(){
Singleton.getInstance().doSomething()
}
}
- 懒汉式(双重校验锁,DCL double-check locking ,线程安全)
懒加载 ,采用双锁检查机制,避免在对象实例时,对象实例指令发生重排,造成对象空指针。在多线程下保存高性能,单例对象需要使用
volatile
关键字声明,volatile
关键字是线程同步的轻量级实现,能保证数据的可见性,但不能保证数据的原子性。可在实例域需要延迟化使用。
/**
* 懒汉式 DCL 线程安全
*/
public class Singleton {
//volatile 修饰变量,防止指令重排
private static volatile Singleton instance ;
private Singleton(){}
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance ;
}
}
//懒汉式: DCL 线程安全
class Singleton private constructor(){
companion object{
//使用lazy属性代理,并指定LazyThreadSafetyMode为synchronized模式保证线程安全
@JvmStatic
val instance : Singleton by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Singleton()
}
}
fun doSomething(){
println("do something")
}
}
//调用
fun test(){
Singleton.instance.doSomething()
}
- 静态内部类
能达到和DCL 一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是DCL。此种方式同样利用classloader机制来保证初始化单例只有一个线程,它和饿汉式不同的是:饿汉式只要类被装载了,那么instance 就会被实例化,而静态内部类实现单例是类被装载了,但instance不一定被初始化。因为内部类没有别主动使用,只有通过getInstance()调用时,才会显示装载内部类,从而实例instance。 实现时考虑:想让单例延迟加载,又不希望单例类加载时就实例化。
/**
* 静态内部类
*/
public class Singleton {
private Singleton(){}
private static class SingletonHolder{
private static final Singleton instance = new Singleton() ;
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
//静态内部类实现单例
class Singleton private constructor() {
companion object{
@JvmStatic
fun getInstance(): Singleton {
return SingletonHolder.mInstance
}
}
fun doSomething(){
println("do something")
}
//静态内部类
private object SingletonHolder {
val mInstance = Singleton()
}
}
- CAS 模式
算是 懒汉式加锁 的一个变种,
synchronized
是一种悲观锁, 而CAS
是乐观锁,相对较轻,更轻量级。
import java.util.concurrent.atomic.AtomicReference;
/**
* CAS模式 : 存在忙等的问题,可能会造成 CPU 资源的浪费
*/
public class Singleton {
private static AtomicReference INSTANCE = new AtomicReference() ;
private Singleton(){}
public static final Singleton getInstance(){
while (true){
Singleton instance = INSTANCE.get() ;
if (null == instance){
INSTANCE.compareAndSet(null,new Singleton());
}
return INSTANCE.get() ;
}
}
}
- 枚举实现
是多线程安全,非懒加载。没有被广泛使用。它很简介,自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化
/**
* 枚举实现单例
*/
public enum Singleton {
//定义一个枚举,代表Singleton的一个实例
INSTANCE ;
public void doSomething(){
System.out.println("do something");
}
//假设在外部调用
void test(){
Singleton.INSTANCE.doSomething();
}
}
//枚举实现单例
enum class Singleton {
INSTANCE ;
fun doSomething(){
println("do something")
}
}
总结
单例模式是使用频率很高的模式,但是,由于在客户端通常没有高并发的情况,因此,选择哪种实现方法并不会有太大的影响,即使如此,出于效率考虑,一般也是使用DCL方式和内部类单例的实现形式;
优点
1、单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁的创建和销毁时,而且创建和销毁的性能又无法优化,单例模式的优势就非常明显;
2、单例模式只生产一个实例,减少了系统的新能开销,当一个对象的场所需要较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决;
3、单例模式可以避免对资源的多重占用,例如一个写文件操作,由于只有一个实例存在内存中,避免对一个资源文件的同时写操作;
4、单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有的数据表的映射管理;
缺点
1、单例模式一般没有借口,扩展困难,如要扩展,除了修改代码基本上没有第二种途径可以实现;
2、单例对象如果持有Context,那么很容易引发内存泄漏,此时需要注意传给到单例对象的Context最好是Application Context ;
3、不利于测试,与单一职责原则有冲突
什么时候使用?
- 比如生成唯一序列号的环境
- 整个项目中需要一个共享的访问点或共享数据
- 创建一个对象需要消耗的资源过多
- 需要定义大量的静态常量和静态方法(如工具类)的环境
补充
volatile 关键字
: 该关键字与内存模型有关,需要先了解内存模型
计算机在执行程序时,每条指令都是在CPU中执行的,而执行过程中,需要进行数据的读取和写入。程序运行时的临时数据是存放在主存中(物理内存中),这就存在一个问题: 由于CPU执行速度很快,而从内存读取和写入数据的过程跟CPU执行指令的速度慢的多,因此如果任何时候对数据的操作都需要同内存进行交互,会大大降低指令执行的速,所以在CPU中有了高速缓存
。这样,当程序在运行过程中,会将运算需要的数据从主存
复制一份到 高速缓存
中,这样CPU进行计算时,就可以直接从高速缓存中读取和写入数据,当运算结束后,再将高速缓存的数据刷新到主存。
在多核CPU
中,每条线程可能运行在不同的CPU中,因此每个线程
都有自己的高速缓存
,因此,对于一个变量,在多线程运行中,可能在多个CPU中都有改变量的高速缓存,这样对该该变量就有可能出现缓存不一致
的问题.
并发编程中的三个问题
: 原子性问题、可见性问题、有序性问题。
原子性
:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就不执行。
在java中,对基本数据类型的变量的读取和赋值操作是原子性,要么执行,要么不执行。只有简单的读取、赋值才是原子操作(变量之间的操作就不是原子操作)。如果实现更大范围的原子性,可以通过
synchronize
和Lock
实现,能够保证任一时刻只能由一个线程执行该代码块,这样就不存在原子性问题了。
可见性
:当多个线程访问一个变量时,一个线程修改了这个变量的值,其他线程能够立即看的到修改的值。
Java 提供了
volatile
关键字来保证可见性,当一个变量被其修饰时,它会保存修改立即更新到主存,当其他线程需要获取时,它会去主存中读取新值。synchronized
和Lock
能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁后将变量的修改刷新到主存,因此可以保证可见性。
有序性
:即程序执行的顺序按照代码的先后顺序执行。
Java可以通过
volatile
关键字来保证一定的有序性
,synchronized
和Lock
也可保证有序性
指令重排
:一般来说,处理器为了提高程序的执行效率,可能对输入的代码进行优化,它不保证程序中的各个语句的执行顺序和代码中的顺序一致,但它会保证程序最终的执行结果和代码顺序执行的记过是一致的。
要想并发程序正确的执行,必须保证原子性、可见性、有序性,只要一个没有被保证,就有可能导致程序运行不正确。
Java内存模型具备一些先天的有序性
,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before
原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
happens-before
原则:
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
volatile
:一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1) 保证了不同线程对这个变量进行操作的可见性,即一个线程改变了某个变量的值,则新值对其他线程来说时立即可见的。 2) 禁止指令重排序。
最终结果:volatile
可以保证操作的可见性、有序性,但不能保证操作变量的原子性。
volatile关键字
参考Java并发编程:volatile关键字解析 - Matrix海子 - 博客园 (cnblogs.com)