目录
(2)以Instance = new SingletonLazy( );为例分析
1.什么是单例模式?
单例模式是最常见的设计模式之⼀
Q:啥是设计模式?
A:编程中典型场景的解决方案,设计模式好⽐象棋中的”棋谱”.红⽅当头炮,⿊⽅⻢来跳.针对红⽅的⼀些⾛法,⿊⽅应招的时候有⼀些固定的套路.按照套路来⾛局势就不会吃亏
单例模式即某个类在进程中又能有唯一实例
- 有且只有一个对象,不会new出来多个对象,这样的对象就是“单例”
2.如何保证单例?
(1)保证单例:instance只有唯一一个,初始化也只是执行一次的
- 保证单例:instance只有唯一一个,初始化也只是执行一次的
static修饰的,其实是“类属性”,就是在“类对象”上的,每个类的类对象在JVM中只有一个,里面的静态成员,只有一份 - 后续需要使用这个类的实例,就可以直接通过getInstance来获取已经new好的这个,而不是重新new
(2) 核心操作:private:禁止外部代码来创建该类的实例
- 怎么操作?类之外的代码,尝试new的时候,势必就要调用构造方法,由于构造方法私有,无法调动,就会编译出错
3.两种写法
(1)饿汉模式(早创建)
唯一实例创建时机非常早,类似于饿了很久的人,看到了吃的就赶紧开始吃(急迫)
package thread;
//单例,饿汉模式
//唯一实例创建时机非常早,类似于饿了很久的人,看到了吃的就赶紧开始吃(急迫)
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton() {
}
}
public class Demo27 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
//Singleton s3 = new Singleton();
}
}
(2)懒汉模式(缓执行,可能不执行)
懒是提高效率,节省开销的体现 啥时候调用,就啥时候创建,如果不调用,就不创建了
package thread;
//单例模式,懒汉模式的实现
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy (){
}
}
public class Demo28 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
//SingletonLazy s3 = new SingletonLazy();
}
}
Q:如果有多个线程同时调用getInstance,是否会产生线程安全问题?
A:
- 饿汉模式不存在线程安全问题,而懒汉模式存在
- 饿汉模式:实例早就有了,每个线程getInstance,就是读取上面的静态变量,多个线程读取同一个变量,是线程安全的
- 懒汉模式:instance = new SingletonLazy,赋值操作就是修改,而且操作不是原子的,肯定就是线程不安全的
图解:懒汉模式:非原子性操作
- 还可能存在其他执行顺序,t2再次new,可能创建多个实例
- instance只是一个引用,instance中地址指向的那个对象可能就是一个大对象,上述代码会出现覆盖,第二个对象的地址覆盖了第一个,进一步第一个对象没有引用指向了,就会被GC回收(但是这个创建时间的开销,是客观存在的)
4.应用场景
(1)写的服务器,要从硬盘上加载100G的数据到内存(加载到若干个哈希表中),肯定要写一个类,封装上述加载操作,并且获取一些获取、处理数据的业务逻辑
- 代码中的有些对象,本身就不应该是多个实例的,从业务角度就应该是单个实例
一个实例就管理100G的内存数据 - 搞多个实例,就是N*100G的内存数据,机器肯定吃不消,没必要
(2)MySQL的配置文件,专门管理配置,需要加载配置数据到内存中提供其他代码使用,这样的类也是单例的
- 如果是多个实例,就存储了多份数据,如果一样还罢了,如果不一样,以哪个为准?
🔥5.多线程中的单例模式
懒汉模式的线程安全问题如何解决呢?
(1)加锁
Q:这样加锁线程就安全了吗?
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (locker) {
instance = new SingletonLazy();
}
}
return instance;
}
A: 没有,t2拿到锁后,还在直接执行new操作
图解:
应该把if和new操作打包成一个原子操作
public static SingletonLazy getInstance() {
synchronized (locker) {
if(instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
(2)双重if
加锁之后的问题Q:懒汉模式只有最开始调用getInstance会存在线程安全问题,一旦把实例创建好了,后续再调用,就是只读操作,就不存在线程安全问题了,但是上述代码,只要一调用getInstance方法,就需要先加锁,再执行后续操作(后续没有线程安全问题,还要加锁,有开销)
真正的解决A:再加一层判断
连续两次一样的条件判断(单线程中无意义,但是多线程含义就不一样)
第一层if:判定是否要加锁(new之前要加锁,new之后就不用加了)
第二层if:判断是否要创建对象
指令级理解
t2拿到锁,这个时候instance已经被t1修改了
(3)volatie
private static volatile SingletonLazy instance = null;
为了保证第一次线程修改,后续线程一定会读到,加上volatile【避免内存可见性+指令重排序问题】
综上:懒汉模式的线程安全版
//单例模式,懒汉模式的实现
class SingletonLazy {
private static volatile SingletonLazy instance = null;
private static Object locker = new Object();
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (locker) {
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy (){
}
}
6.指令重排序(小概率出现问题)
编译器的优化策略有很多:
把读内存优化到读寄存器,指令重排序,循环展开,条件分支预测…
(1)什么是指令重排序?
编译器会在保证逻辑是等价的情况下,调整二进制指令的执行顺序,从而提高效率
- 正常来说写的代码,最终会编译成为一系列的二进制指令,CPU会按照顺序,一条一条执行
(2)以Instance = new SingletonLazy( );为例分析
- 这行代码可以细分为三个步骤
- 申请内存空间
- 调用构造方法(对内存空间进行优化)
- 把此时内存空间的地址,赋值给Instance引用
- 在指令重排序的策略下,上述执行的过程,不一定是123,也可能是132(但是1一定先执行)
- 132这样的执行顺序,就可能存在线程安全问题
- 指令级理解
- 3一旦执行完,就意味着instance就非null,但是指向的对象其实是一个未初始化的对象(里面的成员都是0)
- 执行到t2的时候,instance已经非null了,这里的条件无法进行,直接返回未初始完毕的instance
- 后续如果t2中还有其他逻辑,就会对未初始完毕的对象进行操作,这样存在严重的问题
(3)解决上述指令重排序问题
- 加上volatile,主要是针对某个对象的读写过程中,不会出现重排序
- 很多地方都能重排序,但是只是针对这一过程中
- 这样t2线程读到的数据,一定是t1已经构造完毕的完整对象了(一定是123执行的对象)
7.延伸:了解
(1)单例模式要确保反射下安全,即使动用反射也无法破坏单例特性
(2)单例模式要确保序列化下安全,即使动用Java标准库的序列化机制,也无法破坏单例特性
- enum类型的实例天然支持序列化和反序列化
- 序列化:把对象转为二进制字符串
🔥8.常见考察
(1)为什么说饿汉式单例天生就是线程安全的?
实例早就有了,每个线程getInstance,就是读取上面的静态变量,多个线程读取同一个变量,是线程安全的
(2)传统的懒汉式单例为什么是非线程安全的?
instance = new SingletonLazy,赋值操作就是修改,而且操作不是原子的,肯定就是线程不安全的
(3)怎么修改传统的懒汉式单例,使其线程变得安全?
1.线程不安全的版本
2.加锁版本
3.加上双重if
4.最后加上volatile
(4)线程安全的单例的实现还有哪些,怎么实现?
静态内部类单例
public class StaticInnerClassSingleton { private StaticInnerClassSingleton() {} private static class SingletonHolder { private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton getInstance() { return SingletonHolder.INSTANCE; } }
静态内部类在外部类加载时不会被加载,只有在调用 getInstance 方法时才会加载类,从而创建实例。类加载过程是线程安全的,所以这种方式实现了线程安全的单例,并且具有延迟加载的特性
(5)双重检查模式、Volatile关键字在单例模式中的应用
1.双重检查模式:
第一层if:判定是否要加锁(new之前要加锁,new之后就不用加了)
第二层if:判断是否要创建对象
2.Volatile关键字:避免内存可见性+指令重排序问题
(6)ThreadLocal在单例模式中的应用
public class ThreadLocalSingleton {
private static final ThreadLocal threadLocalInstance =
new ThreadLocal() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton() {}
public static ThreadLocalSingleton getInstance() {
return threadLocalInstance.get();
}
}
ThreadLocal 会为每个使用该实例的线程都提供一个独立的副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。在单例模式中使用 ThreadLocal,可以保证每个线程都有自己的单例实例,适用于需要在每个线程中维护单例状态的场景
(7)枚举式单例
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("Doing something...");
}
}
枚举式单例是实现单例模式的最佳方式之一。它是线程安全的,因为枚举类型的实例创建是由 JVM 保证线程安全的。而且枚举类型可以防止反序列化和反射攻击,因为 Java 规范中规定,枚举类型的 clone()、readObject()、readResolve() 等方法都不会破坏单例的唯一性。使用时可以直接通过 EnumSingleton.INSTANCE 来获取单例实例
评论(0)