设计模式【1】六种单例模式介绍
程序员文章站
2022-06-09 19:11:03
介绍单例模式前先了解一下类加载的顺序类加载顺序1.加载父类静态代码块和静态变量,按代码顺序执行。2.加载子类静态代码块和静态变量。3.加载父类的实例变量初始化。4.加载父类的构造函数。5.加载子类的实例变量参数。6.加载子类的构造函数。(1)饿汉式(可用)public class Singleton { private final static Singleton INSTANCE = new Singleton(); private S.....
介绍单例模式前先了解一下类加载的顺序
类加载顺序
1.加载父类静态代码块和静态变量,按代码顺序执行。
2.加载子类静态代码块和静态变量。
3.加载父类的实例变量初始化。
4.加载父类的构造函数。
5.加载子类的实例变量参数。
6.加载子类的构造函数。
(1)饿汉式(可用)
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
优点:类加载的时候就完成实例化,避免了多线程问题。
缺点:不使用的话就浪费内存。
(2)普通的懒汉式(线程不安全,不可用)
public class Singleton {
private static Singleton instance = null;
private Singeton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
缺点:多线程并发会创建多个实例
(3) 同步方法的懒汉式(可用)
public class Singleton {
private static Singleton instance = null;
private Singeton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点:第一次加载时实例化,不使用则不实例化,节省内存。
缺点:因为修饰的是整个方法,每个线程访问都需要同步,而实际上执行一次实例化就够了,同步锁效率低下。
(4)双重检查懒汉式/DCL模式(可用,推荐)
public class Singleton {
private static volatile Singleton instance = null;
private Singeton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class){
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
第三种方法的改进。
优点:同步的是代码块,效率较高。
DCL失效2个重要原因:
1、编译器优化了程序指令,以加快cpu处理速度
2、多核cpu动态调整指令顺序,以加快并行运算能力。
问题出现顺序:
1、线程A发现对象未实例化,准备开始实例化
2、由于编译器优化了程序指令,允许对象在构造函数未调用完前,将共享变量的引用指向部分构造的对象, 虽然对象未完全实例化, 但已经不为null了.
3、 线程B, 发现部分构造的对象已不是null, 则直接返回了该对象
拓展: 假设某条线程执行一个synchronized代码段,其间对某变量进行操作,JVM会依次执行如下动作:
1、获取同步对象的monitor(lock)
2、从主存复制变量到工作内存(read and load)
3、执行代码,改变共享变量值(use and assign)
4、用工作内存数据刷新主存相关内容(store and write)
5、释放同步对象锁(unlock)
volatile变量的特性
1、保证可见性,不保证原子性
a、当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内去;
b、这个写操作会导致其他线程中的缓存无效。
2、禁止指令重排
重排序是指编译器和处理器味蕾优化程序性能而对指令序列进行排序的一种手段。重排序需遵守一定规则:
a、重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a;这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
b、重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。
比如: a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排 序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:
a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后
面的操作可见;在其后面的操作肯定还没有进行;
b.在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放
到其前面执行。
即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变
量及其后面语句可见。
(5)静态内部类(可用,推荐)
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
优点:线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化
缺点:无法传参
拓展:当getInstance()方法被调用时,SingletonInstance才在Singleton的运行时常量里,把符号引用替换为直接引用,这是静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回回去,这点同饿汉模式。那么INSTANCE在创建过程中如何保证线程安全?
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次),实际应用下,这种阻塞往往是很隐蔽的。
(6)枚举(可用,推荐)
public enum Singleton {
INSTANCE;
}
优点:线程安全。因为Java虚拟机在加载枚举类的时候会用ClassLoader的方法,这个方法使用了同步代码块来保证线程安全。
避免反序列化破坏对象,因为枚举的反序列化不是通过反射实现。
有哪里写的不够严谨或错误的,欢迎评论指出。
本文地址:https://blog.csdn.net/yao799879143/article/details/107612935
上一篇: Win10笔记本电脑如何切换账户?
下一篇: web前端面试题小结