单例模式是比较简单的设计模式,但是涉及到的知识点还是挺多,比如并发模式下的单例、序列化反序列化情况下保证单例、反射情况下保证单例,下面来看看各种情况下怎么保证单例。
单例模式最基本的组成构造函数私有化、即不能随便创建对象;对外提供一个静态方法来获取对象。
一、饿汉模式
1、饿汉模式实现
public class Single {
/**
* 类成员变量
*/
private static Single single = new Single();
private Single() {
System.out.println("私有构造函数");
}
public static Single getInstance() {
return single;
}
}
2、饿汉模式单例对象创建流程
(1)、invokestatic 指令触发类加载、初始化。
(2)、类加载流程:类加载(Class对象生成)、连接(验证.class 文件、准备、解析)、初始化。
(3)、类成员变量在准备阶段被分配内存空间并赋予初始值,分配的空间是在方法区。
(4)、初始化阶段是执行类构造器
(5)、
(6)、类成员变量初始化:定义变量直接赋值、静态代码块赋值,不管是那种赋值方式都是使用
(7)、虚拟机会保证一个类的
(8)、一个线程执行
(9)、类变量初始化结束
3、线程安全问题
饿汉模式在
二、懒汉模式
1、懒汉模式实现
public class Single {
/**
* 类成员变量
*/
private static Single single ;
private Single() {
System.out.println("私有构造函数");
}
public static Single getInstance() {
if (null == single) {
single = new Single();
}
return single;
}
}
2、单例对象创建时间点
他这个懒汉模式懒在哪里,当调用invokestatic 指令之后进行加载类、连接、初始化;对象的创建不是在类初始化
3、懒汉模式线程安全问题
if(null == single){
Single single= new Single()
}
4、多线程环境下执行存在线程安全问题
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 30; i++) {
service.execute(new Runnable() {
@Override
public void run() {
Single single = getInstance();
System.out.println(single);
}
});
}
5、执行结果
私有构造函数
私有构造函数
com.sb.design.single.Single@2deeefc5
私有构造函数
com.sb.design.single.Single@35655ed5
com.sb.design.single.Single@2e1b63f0
com.sb.design.single.Single@35655ed5
com.sb.design.single.Single@2e1b63f0
多次调用构造函数创建对象而且对象地址也不相同……
唯一性判断在单线程环境没问题,但是在多线程并发执行情况下这个判断条件不能保证原子性,也不能保证复合原子性,所以这样的写法在多线程环境下无法保证单例。
三、静态内部类(懒汉+饿汉)
1、静态内部类实现单例模式
public class Single implements Serializable {
static {
System.out.println("1、外部类加载、链接、初始化");
}
private Single() {
System.out.println("4、调用私有构造函数初始化单例对象");
}
/**
* 获取枚举类对象
*
* @return
*/
private static class InnerSingle {
static {
System.out.println("3、静态内部类加载、链接、初始化");
}
private static final Single SINGLE = new Single();
}
/**
* 对外发布单例对象
*
* @return
*/
public static Single getInstance() {
System.out.println("2、外部类加载、链接、初始化完成");
return InnerSingle.SINGLE;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Single single = Single.getInstance();
Single single1 = Single.getInstance();
System.out.println(single == single1);
}
}
2、静态内部类执行流程
1、外部类加载、链接、初始化
2、外部类加载、链接、初始化完成
3、静态内部类加载、链接、初始化
4、调用私有构造函数初始化单例对象
2、外部类加载、链接、初始化完成
true
3、为何说内部类单例是懒汉+饿汉模式我们分析一下静态内部类的加载顺序
(1)、当有getstatic、setstatic、invokestatic 等指令调用外部类则会触发外部类的加载、链接、初始化过程,但是在这过程不会触发静态内部类的加载(静态内部类、非静态内部类的加载和外部类的加载无关),这里我们调用了外部类静态方法即触发了invokestatic 指令,所以进行了外部类的加载、链接、初始化流程的执行。
(2)、当外部类初始化结束后开始执行外部类静态方法
(3)、外部类静态方法中调用了内部类的静态常量,这样内部类又触发了getstatic 指令
(4)、内部类触发getstatic指令开始执行内部类的类加载、连接、初始化流程
(5)、内部类初始化就是内部类的类构造器执行过程即
方法执行 (6)、按照开始分析的饿汉模式加载流程知道clinit方法中有putstatic 指令即给内部类的静态变量赋值
(7)、putstatic 指令赋值的具体对象就是调用外部类的私有构造函数创建的单例对象
(8)、结束了静态内部类单例的生成流程,按照
方法执行是线程安全的来看,静态内部类也是线程安全的。
4、多线程测试
public static void main(String[] args) throws IOException, ClassNotFoundException {
ExecutorService executor = Executors.newFixedThreadPool(50);
for (int i = 0; i < 100; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
Single single = Single.getInstance();
Single single1 = Single.getInstance();
System.out.println(single == single1);
}
});
}
executor.shutdown();
}
5、多线程测试执行结果
1、外部类加载、链接、初始化
2、外部类加载、链接、初始化完成
2、外部类加载、链接、初始化完成
2、外部类加载、链接、初始化完成
2、外部类加载、链接、初始化完成
2、外部类加载、链接、初始化完成
2、外部类加载、链接、初始化完成
2、外部类加载、链接、初始化完成
3、静态内部类加载、链接、初始化
4、调用私有构造函数初始化单例对象
2、外部类加载、链接、初始化完成
2、外部类加载、链接、初始化完成
true
2、外部类加载、链接、初始化完成
2、外部类加载、链接、初始化完成
true
2、外部类加载、链接、初始化完成
2、外部类加载、链接、初始化完成
true
……
只要看外部类私有构造函数调用次数就能证明线程安全,其实从上面的输出也证明了
四、懒汉模式-多线程环境加类锁
1、懒汉模式线程安全实现
public static Single getInstance() {
synchronized (Single.class) {
if (null == single) {
single = new Single();
}
return single;
}
}
使用synchronized锁类,这样写感觉没问题,但是所有线程都需要获得类锁才能判断是不是对象已创建,这样这个锁竞争很激烈,性能也就更低。
五、懒汉模式-多线程环境双重锁判断
1、代码实现
public static Single getInstance() {
if (null == single) {
synchronized (Single.class) {
if (null == single) {
single = new Single();
}
}
}
return single;
}
双重锁校验相比直接添加类锁,锁竞争没那么激烈,有性能提升但是,这种写法又带来了一个严重的问题即指令重排。
更严格的说这种写法不是线程安全的,分析原因如下:
2、对象创建流程
1、通过new 指令判断所指向的类是否已加载,如果未加载则进行加载。
2、给对象在堆内存分配空间即指定一块对内存给新建对象。
3、对象初始化即
对象构造函数执行。 4、将新建对象地址赋值给引用变量。
但是在编译和运行时会对现有代码进行优化即指令重排序,排序之后对象创建顺序有可能是1-2-3-4 也有可能是1-2-4-3 当出现 1-2-4-3 这种顺序的时候上上面的双重锁校验就有问题!
3、指令重排后的对象创建流程
如果指令重排后是1-2-4-3 这样的顺序,当线程A执行到4即将对象内存地址赋值给引用变量,cpu资源被B线程占用,这时线程B判断不为空则直接返回single对象,但是这个对象没有被初始化;所以这样的写法在1-2-4-3 这样的执行顺序下是有问题的,而引起这个问题的根本原因是指令重排。
六、懒汉模式-指令重排
volatile 关键字是轻量级锁,他能保证变量的可见性和防止指令重排(内存屏障)
private static volatile Single single;
synchronized (Single.class) {
if (null == single) {
single = new Single();
}
}
一个volatile关键字搞定;这样多线程环境下的单例模式就没问题了,可以安全使用!
七、使用序列化、反序列化破坏单例模式
1、序列化、反序列化破坏单例
public class Single implements Serializable {
/**
* 类成员变量
*/
private static volatile Single single;
private Single() {
System.out.println("私有构造函数");
}
/**
* 双重锁校验
*
* @return
*/
public static Single getInstance() {
if (null == single) {
synchronized (Single.class) {
if (null == single) {
single = new Single();
}
}
}
return single;
}
public static void main(String[] args) throws ExecutionException, InterruptedException, NoSuchMethodException, IOException, ClassNotFoundException {
Single single = getInstance();
// 序列化
String path = System.getProperty("user.dir");
File file = new File(path + File.separator + "singles.text");
FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(single);
// 反序列化
FileInputStream fileInputStream = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Single single1 = (Single) objectInputStream.readObject();
System.out.println(single == single1);
}
}
其实看序列化、发序列化的过程你会发现也是调用的反射来创建对象;使用序列化破坏单利很简单,那怎么防止序列化、反序列化破坏单利模式?
2、一步步分析反序列化过程、了解是怎么破坏的单例
(1)、从输入流获取对象
(2)、readObject0获取对象
(3)、获取类序列化描述符类,并使用反射获取反序列化新对象
(4)、使用constructor 构造函数对象创建新对象
(5)、readResolve方法判断,如果序列化类没有readResolve 方法则直接返回构造函数创建的对象,如果存在则获取readResolve 方法返回值
(6)、反射调用readResolve 方法
(7)、用readResolve 方法返回值替换使用constructor 构造函数器对象创建的新对象
从上面流程看到,如果序列化类无readResolve 方法则直接返回通过constructor 构造函数类对象创建的新对象,如果readResolve 方法存在则返回该方法返回值,所以在单例类序列化时添加该方法,可以避免序列化、反序列化破坏单例!
八、懒汉单例模式-反射破坏单利
因为返利能获取类中的任何变量并且能重新设置值,所以不管是添加一个计数器还是判断变量是否为空都是无法避免反射破坏单例的;下面来看看使用计数器和判断是否为null来阻止破坏单例模式是否有效。
public class Single implements Serializable {
private static volatile boolean flag = true;
private static volatile Single single;
/**
* 类成员变量
*/
private Single() throws BindException {
if (!flag) {
throw new BindException("多次创建对象");
}
flag = false;
}
/**
* @return
* @throws BindException
*/
public static Single getSingle() throws BindException {
if (null == single) {
synchronized (Single.class) {
if (null == single) {
single = new Single();
}
}
}
return single;
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, BindException {
Single single = Single.getSingle();
Single single1 = Single.getSingle();
System.out.println(single == single1);
//反射设置属性
Class singleClass = Single.class;
Field field = singleClass.getDeclaredField("flag");
field.setAccessible(true);
//设置属性,满足创建对象条件
field.set(single, true);
Constructor constructor = singleClass.getDeclaredConstructor(null);
Single single2 = constructor.newInstance(null);
System.out.println(single == single2);
}
}
九、枚举类解决序列化、反序列化以及反射等的相关问题
1、代码实现
public class Single implements Serializable {
/**
* 类成员变量
*/
private Single() {
System.out.println("私有构造函数");
}
/**
* 内部类
*/
private enum InnerClass implements Serializable {
MY_SINGLE;
private final Single single;
/**
* 枚举类无惨构造函数
*/
InnerClass() {
System.out.println("内部类构造函数");
single = new Single();
}
/**
* 私有方法
*
* @return
*/
private Single getSingle() {
return single;
}
}
/**
* 获取枚举类对象
*
* @return
*/
public static InnerClass getEnum() {
return InnerClass.MY_SINGLE;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
InnerClass innerClass = getEnum();
Single single = innerClass.getSingle();
String path = System.getProperty("user.dir");
File file = new File(path + File.separator + "single.txt");
FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(innerClass);
FileInputStream fileInputStream = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
InnerClass innerClass1 = (InnerClass) objectInputStream.readObject();
System.out.println(innerClass == innerClass1);
Single single2 = innerClass1.getSingle();
System.out.println(single == single2);
}
}
2、执行结果
内部类构造函数
私有构造函数
true
true