单例模式 (Singleton)
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
图0-1使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。
Ⅰ:懒汉式-线程不安全
懒汉式:私有静态变量 uniqueInstance 先声明,实例化延迟。在未使用到该类时,不进行实例化,从而节约资源。
这个实现在多线程环境下是不安全的,如果多个线程同时进入if(uniqueInstance == null),各线程将会分别对 uniqueInstance 进行实例化,导致多次实例化。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
Ⅱ:饿汉式(静态初始化)-线程安全
在第一次引用类的成员时创建实例,公共语言运行库负责处理变量初始化,从而避免产生线程不安全的情况。
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {
}
public static Singleton getUniqueInstance() {
return uniqueInstance;
}
}
Ⅲ:懒汉式-线程安全方式实现
为避免产生线程不安全的情况,可以加入synchronized对getUniqueInstance() 方法加锁使得在同一时间点,只允许一个线程进入该方法,从而避免了多次实例化的情况。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
Ⅳ:懒汉式-双重锁定线程安全
对于上面第三种情况的线程安全实现,其效率相对要低一些;对于需要实例对象的操作,线程都需要对公有静态函数进行加锁操作,这其实是没有必要的,而只是在实例未被创建的时候,进行实例化加锁处理;同时也能够保证线程安全,这种做法被称为Double-Check Locking(双重锁定)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
注:此处给私有静态变量采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
1、为 uniqueInstance 分配内存空间
2、初始化 uniqueInstance
3、将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
即:使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
PS:补充一下概念:Java 内存模型中的可见性、原子性和有序性。
原子性
和数据库事务中的原子性一样,满足原子性特性的操作是不可中断的,要么全部执行成功要么全部执行失败,非原子操作会存在线程安全问题。
有序性
编译器和处理器为了优化程序性能而对指令序列进行重排序,也就是你编写的代码顺序和最终执行的指令顺序是不一致的,重排序可能会导致多线程程序出现内存可见性问题
可见性
多个线程访问同一个共享变量时,其中一个线程对这个共享变量值的修改,其他线程能够立刻获得修改以后的值
当一个变量定义为 volatile 之后,将具备两种特性:
1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。
2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
一道简单的面试题
请你介绍一下单例模式?再说一说懒汉式的单例模式如何实现单例? |
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。懒汉式的单例模式,对于私有静态变量声明时,先不进行实例化,就是使用时才会创建实例对象,在共有静态的获取实例对象的函数中,利用if语句对该类所私有的对象进行空值判断,从而保证单例,但多线程的情况下,不安全。 |
本文地址:https://blog.csdn.net/You_are_mine/article/details/107691646