目录
单例模式: (单例:一个类单个实例)
1.饿汉式:
2.懒汉式
懒汉式优化:双重检查机制
懒汉双重检查机制为什么要加volatile:
枚举饿汉式:
内部懒汉式:
设计模式是对已有问题固定的解决方法的总结。
单例模式(Singleton):保证一个类仅有一个实例,并提供一个访问他的全局访问点。
保证类仅有一个实例最好的办法就是,让类自身负责保存他的唯一实例。这个类可以保证没有其他实例被创建,并且他可以提供一个访问该实例的方法。
1. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
2. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
3. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
4. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
从时间空间上看:以空间换时间
从线程安全上看:安全的
所以又叫“饿汉式”(还没有使用就实例化了)
public class Singleton {
//3、定义一个变量来存储创建好的类实例
//直接在这里创建类实例,会放到静态代码块中执行,静态代码块里面的线程安全由虚拟机来保证
//这个类加载到内存的时候就会创建唯一的一份实例
private static final Singleton INSTANCE = new Singleton();
//1.私有化构造方法,好在内部控制创建实例的数目
private Singleton() {
}
//2、定义一个方法来为客户端提供类实例
//这个方法需要定义成类方法,也就是要加static
public static Singleton getInstance() {
//4、直接使用已经创建好的实例
return INSTANCE ;
}
}
从时间空间上看:以时间换空间
从线程安全上看:不安全的(所以要加锁synchronized)
所以又叫“懒汉式”(也就是在用到的时候才去实例化)
public class Singleton1 {
//3、定义一个变量来存储创建好的类实例,先不new,用到时候再new
//因为这个变量要在静态方法中使用,所以需要加上static修饰
private static Singleton1 INSTANCE = null;
//1.私有化构造方法,好在内部控制创建实例的数目
private Singleton1() {
}
//2、定义一个方法来为客户端提供类实例
//这个方法需要定义成类方法,也就是要加static
public synchronized static Singleton1 getInstance() {
//4、判断这个实例是不是有值
// 这段代码有线程安全问题,两个线程同时判断为null可能会new多个,加锁解决
if (INSTANCE == null) {
//5、如果没有,就创建一个类实例,并把值
//赋给存储类实例的变量
INSTANCE = new Singleton1();
}
returnINSTANCE ;
}
}
懒汉式加锁,每次访问都要加锁,但其实只有INSTANCE为null,也就是第一次创建前才有线程安全问题,这样每次加锁耗费大量的资源所以我们对其进行优化。
public class Singleton3 {
// volatile解决共享变量可见性,有序性(禁止指令重排),但是解决不了原子性
private static volatile Singleton3 INSTANCE;
private Singleton3(){}
public synchronized static Singleton3 getINSTANCE() {
// 双重检查
if(INSTANCE == null) {
synchronized (Singleton3.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton3();
}
}
}
return INSTANCE;
}
}
如果实例存在就没有必要走同步的必要,如果实例不存在才会进入同步块。
这样只会在第一次创建的时候同步一次,其余的时候不需要同步。
第二重检查是因为:当A线程执行 new Singleton()时候,B线程正在执行第一重检查if(instance == null)此时B线程会进入,所以在synchronized中要再做一次检查。
因为instance是volatile修饰的所以A线程new Singleton修改了instance的时候,在B线程中能读取到instance不是null。
volatile:
解决共享变量可见性,有序性(禁止指令重排),但是解决不了原子性
可见性:
在一个线程修改这个变量时,其他的线程也能察觉。正常情况是不能察觉的,因为线程操作的是变量副本。
原子性:
简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。
比如:m = 6; // 这是个原子操作
int n=6;//这不是一个原子操作,对于这个语句,至少有两个操作:
①声明一个变量n ②给n赋值为6。
指令重排:
CPU乱序执行,乱序执行是相对于顺序执行来说的,CPU可能会对指令的执行次序做出优化,如果指令之间没有因果关系,CPU可能调换他们的执行次序。
简单来说,就是指你在程序中写的代码,在执行时并不一定按照写的顺序。
这里用volatile关键字,除了利用他的可见性之外更重要的是利用它禁止指令重排的特性。
我们将上边的代码反编译得到:
先来解读一下:
17条:new出了对象,分配空间,但是没有代用构造方法。
21条:指构造方法,给上边new出的空间放入了东西。
24条:将17条new出的空间地址赋给INSTANCE变量。
可以看出,17条与21条,17条与24条是有逻辑关系的,顺序不能打乱,但是21条与24条没有逻辑关系,顺序可以打乱,也就是没有volatile时可以重排。
如果我们将其重排,在懒汉式双重检查机制多线程下就会出问题:
如图,如果线程1在执行代码 INSRANCE = new Singleton4(); 时,按照17条,24条,21条的顺序执行,在执行完24条,也就是把new出来的对象(有空间但是没经过构造方法)赋给了INSTANCE,这时INSTANCE不为空,此时线程2来进行第一重判断,会直接将这个INSTANCE变量返回。这是我们不允许的。
而volatile通过内存屏障可以禁止指令重排序,按照new、invokespecial、putstatic顺序执行。
就解决了这一问题。
就是利用枚举类实现
枚举天然能防止反射、反序列化破坏单例
public enum Singleton4 {
// Singleton4 INSTANCE;
INSTANCE;
// 枚举构造方法默认就是私有的,去掉也可以
private Singleton4() {
System.out.println("private Singleton4()");
}
public static Singleton4 getInstance() {
return INSTANCE;
}
}
上边的例子中,饿汉式需要提前new出对象,占用空间;懒汉式有线程安全问题,加锁开销大。
有没有一种方法既能不提前new出单例又能不加锁呢,有,那就是内部懒汉式。
利用内部类实现:
// 懒汉式单例 - 内部类
public class Singleton5 {
private Singleton5() {
System.out.println("private Singleton5()");
}
//没有用到这个静态内部类不会触发他的加载、连接、初始化,也就不会初始化对象
private static class Holder {
// 会放到静态代码块中执行,静态代码块里面的线程安全由虚拟机来保证
static Singleton5 INSTANCE = new Singleton5();
}
// 懒汉式,不调用getInstance时候不会触发Holder的加载和初始化
// 既保证了线程安全static静态保证,避免了双检锁的缺点,又保证懒汉特性,第一次访问才用到
public static Singleton5 getInstance() {
return Holder.INSTANCE;
}
}