单例设计模式在程序设计里面被广泛使用,单例的实现方式也各种各样。
* 每一种单例,都需要是否懒加载(考虑效率问题);是否被多线程破坏(线程安全),是否被反射获取破坏,
* 是否被反序列化破坏
备注:我们的设计模式,不一定都符合软件设计所有原则;单例模式就不符合开闭原则。
下面把软件设计原则的基本概念贴图:
设计原则 | 解释 |
开闭原则 | 对扩展开放,对修改关闭。 |
依赖倒置原则 | 通过抽象使各个类或者模块不相互影响,实现松耦合。 |
单一职责原则 | 一个类、接口、方法只做一件事。 |
接口隔离原则 | 尽量保证接口的纯洁性,客户端不应该依赖不需要的接口。 |
迪米特法则 | 又叫最少知道原则,一个类对其所依赖的类知道得越少越好。 |
里氏替换原则 | 子类可以扩展父类的功能但不能改变父类原有的功能。 |
合成复用原则 | 尽量使用对象组合、聚合,而不使用继承关系达到代码复用的目的。 |
1.饿汉式单例
对每种情况进行分析:
线程安全:可有效的避免线程安全问题(是类的静态私有全局变量,在JVM层面保证只有一个实例对象在堆中,方法区是静态变量地址指针)。
懒加载问题:非懒加载的模式,对大量的类加载项目会出现效率的问题。
是否能被反射破坏:可被反射破坏类的单实例。解决办法:在下面的的代码块中,在类的构造函数中,对存在的实例对象进行非空判断,如果已经存在实例对象,人为抛出异常,不能创建。
上面是常见处理办法,我认为也可以使用全局的静态变量进行类单例的创建控制,在类全局中定义private static int nun=1; 然后在构造方法中,进行判断,如果是小于1的时候,不允许再次
创建对象,人为抛出异常,因为在第一次创建后程序对变量num-- 操作。
是否被反序列化破坏:会被序列化破坏 ,反序列化的时候,生成的对象完全是另外一个不同的对象,这是违背了单实例的设计。反序列化的解决办法的原理,在代码的注释地方已经加上。
解决办法的JDK源码位置:
package com.venus.ioc.singleton;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 测试饿汉式单例模式
*/
public class SingletonMode implements Serializable {
private static final SingletonMode singletonMode;
static {
singletonMode=new SingletonMode();
}
/**
* 解决反序列化文件时,无法获取到和原始对象一致的对象;
* 造成原因:反序列化时,使用反射再次调用构造方法生成不同的对象。
* 为啥这个方法可以解决问题?
* 解答:在反序列化时,会有判断逻辑,如果对象含有此方法,就会直接获取此方法返回对象,不再构造新实例
*/
private Object readResolve(){
return singletonMode;
}
private SingletonMode(){
/**
* 这是解决反射获取单实例的漏洞,在jvm层面没有解决或者说避免,需要程序避免
*/
if(singletonMode!=null){
throw new RuntimeException("单实例已经存在,不能再次创建");
}
}
public static SingletonMode getSingletonMode(){
return singletonMode;
}
public static void main(String[] args) throws IllegalAccessException, InstantiationException,
NoSuchMethodException, InvocationTargetException {
/* SingletonMode singletonMode1 = SingletonMode.getSingletonMode();
SingletonMode singletonMode2 = SingletonMode.getSingletonMode();
System.out.println(singletonMode1==singletonMode2);
System.out.println(singletonMode1);
System.out.println(singletonMode2);*/
System.out.println("反射获取实例对象-----");
/* SingletonMode singletonMode3 = SingletonMode.class.newInstance();
SingletonMode singletonMode4 = SingletonMode.class.newInstance();*/
Constructor<SingletonMode> declaredConstructor = SingletonMode.class.getDeclaredConstructor(null);
SingletonMode singletonMode3 = declaredConstructor.newInstance(null);
SingletonMode singletonMode4 = declaredConstructor.newInstance(null);
System.out.println(singletonMode3==singletonMode4);
System.out.println(singletonMode3);
System.out.println(singletonMode4);
}
}
下面是另外的一个测试类,测试反序列化的问题;
package com.venus.ioc.singleton;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 测试序列化和反序列化是不同的对象
*/
public class SerializableSingletonMode {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
validateSerializableSingletonMode();
//validateStaticInnerClassSingletonSerializable();
//validateEnumSingletonSerializable();
}
/**
* 验证饿汉式单例被反序列化破坏并提供解决方案
*/
public static void validateSerializableSingletonMode() throws IOException, ClassNotFoundException{
SingletonMode beforeSingleton=SingletonMode.getSingletonMode();
SingletonMode singletonMode;
FileOutputStream fos = new FileOutputStream("C:\\Users\\lj\\Desktop\\serializale.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fos);
objectOutputStream.writeObject(beforeSingleton);
objectOutputStream.close();
fos.close();
System.out.println("把对象写入文件--完成");
FileInputStream fis = new FileInputStream("C:\\Users\\lj\\Desktop\\serializale.txt");
final ObjectInputStream objectInputStream = new ObjectInputStream(fis);
singletonMode=(SingletonMode)objectInputStream.readObject();
objectInputStream.close();
fis.close();
System.out.println("从文件中读取序列化文件");
System.out.println(beforeSingleton==singletonMode);
System.out.println(beforeSingleton.hashCode());
System.out.println(singletonMode.hashCode());
}
}
反序列化测试结果:
在无代码解决前的结果:
把对象写入文件--完成
从文件中读取序列化文件
false
500977346
200006406
代码解决后:在单例类中加载这个方法:在上面的代码里面已经加上。
private Object readResolve(){
return singletonMode;
}
测试结果:
把对象写入文件--完成
从文件中读取序列化文件
true
500977346
500977346
2.懒汉式单例模式
线程安全:此模式只有在double-check双锁检测机制下面才是线程安全的写法,并且建议加上volatile 关键字(阿里代码规范建议);原因是在jvm或者编译器或者内存执行时,
会被选择性的指令重排(有内部算法),一旦出现这种情况(非常少见),会导致线程安全问题。
是否懒加载:是懒加载的
反射是否影响:反射会有影响,解决办法和饿汉式一致
反序列化影响:序列化也有影响,解决办法和饿汉式一致
代码:
package com.venus.ioc.singleton;
/**
* 懒汉式加载单实例
*/
public class LazySongletonMode {
private LazySongletonMode(){}
private static volatile LazySongletonMode lazySongletonMode=null;
public static LazySongletonMode getInstance(){
if(lazySongletonMode==null){
synchronized (LazySongletonMode.class){
lazySongletonMode=new LazySongletonMode();
}
}
return lazySongletonMode;
}
public static void main(String[] args) throws InterruptedException {
final LazySongletonModeThread lazySongletonModeThread = new LazySongletonModeThread();
Thread t1 = new Thread(lazySongletonModeThread,"线程01");
Thread t2 = new Thread(lazySongletonModeThread,"线程02");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("执行完成-----");
}
}
多线程的测试代码:
package com.venus.ioc.singleton;
import lombok.SneakyThrows;
/**
* 使用多线程获取懒加载实例
*/
public class LazySongletonModeThread implements Runnable{
@SneakyThrows
@Override
public void run() {
LazySongletonMode instance = LazySongletonMode.getInstance();
System.out.println(Thread.currentThread().getName()+"--"+instance);
}
}
3.静态内部类的方式:
线程安全:是线程安全的,首先内部类不会被外部类加载时初始化,在被调用时,才初始化内部类的静态变量,使用static 关键字,在jvm层面保证实例独有,和饿汉式一致的原理。
是否懒加载:是懒加载的
反射是否影响:反射会有影响,解决办法和饿汉式一致
反序列化影响:序列化也有影响,解决办法和饿汉式一致
备注:在此案例中,还写了复制对象后是否破坏单例的用法,测试发现,仅浅复制都会变成另外一个实例对象,和原始对象不同,详情见下面的代码和注释。必须实现Cloneable。
序列化和反序列化时,出现了transient关键字的使用,虽然代码有解释,这里再次备注下;只使用在变量前面修饰,这个关键字保证对象的变量不会被序列化到文件或者其他流文件中,
在反序列化时,不会获 取到原始的值,获取的是null;对于静态变量不会起到效果,是因为静态变量是类层面的变量,加上这个关键字,序列化和反序列化不影响。
实例必须实现Serializable接口,不然会抛出异常。
这里需要关注序列化接口的另外一个接口 Externalizable,这不是一个标记接口,需要实现此接口的两个方法,序列化的对象和属性的序列化和反序列化时和transient 无关,和实现的方法写法有关。
代码:
package com.venus.ioc.singleton;
import org.springframework.beans.BeanUtils;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import static org.aspectj.weaver.ResolvedTypeMunger.InnerClass;
/**
* 静态内部类实现单例模式;
*
* 重点理解,执行的时序图
*
* 外部类实例化后并不会实例化内部类,只有当调用时,才会实例化静态内部类,进而实话innerClassSingleton对象,
* innerClassSingleton 对象是包含在内部类的一个对象实例。
*
* 每一种单例,都需要是否懒加载(考虑效率问题);是否被多线程破坏(线程安全),是否被反射获取破坏,
* 是否被反序列化破坏
*
* 静态内部类在这几个维度的考虑如下:
*
* 1.是采用懒加载的方式---效率较好,----属于懒汉式模式扩展
* 2。是否被多线程获取到不同的实例,在下面main方法多次测试后,发现获取都是同一实例,
* 原因:类的静态变量存储在方法区,只有一个实例对象,
*/
public class StaticInnerClassSingleton implements Serializable,Cloneable {
/**
* 反序列化解决方法
*/
private Object readResolve(){
return StaticInnerClassSingleton.InnerClass.innerClassSingleton;
}
private int num=0;
private StaticInnerClassSingleton(){
if(StaticInnerClassSingleton.getInstance()!=null){
throw new RuntimeException("单实例已存在,不允许多次创建");
}
System.out.println("生成实例对象");
}
private static class InnerClass{
private static StaticInnerClassSingleton innerClassSingleton=new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
System.out.println("获取内部单实例");
return StaticInnerClassSingleton.InnerClass.innerClassSingleton;
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, CloneNotSupportedException {
/*System.out.println("验证多线程的获取是否同一实例");
new Thread(() -> {
StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
System.out.println(instance.hashCode());//和验证静态内部类线程2获取的对象hashcode一致
},"验证静态内部类线程1").start();
new Thread(() -> {
StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
System.out.println(instance.hashCode());
},"验证静态内部类线程2").start();
System.out.println("结论==静态内部类方式实现单实例,在多线程情况下是安全的");*/
/*System.out.println("验证反射是否可以破坏单例模式--");
final Class<?> aClass = Class.forName("com.venus.ioc.singleton.StaticInnerClassSingleton");
final Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(null);
final StaticInnerClassSingleton instance01 = (StaticInnerClassSingleton)declaredConstructor.newInstance(null);
final StaticInnerClassSingleton instance02 = (StaticInnerClassSingleton)declaredConstructor.newInstance(null);
System.out.println(instance01==instance02);//false
System.out.println(instance01.hashCode());//960604060
System.out.println(instance02.hashCode());//1349393271
System.out.println("结论:==反射可破坏单实例的创建,两次获取的是不同的对象");*/
//防止反射获取到不同的对象,普遍采用在构造函数执行时,进行异常的抛出。相当于阻止了反射方式的调用。
System.out.println("验证复制方法是否破坏单例");
StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
StaticInnerClassSingleton clone01 = (StaticInnerClassSingleton)instance.clone();
System.out.println(instance==clone01);
clone01.num=1;
System.out.println(instance.hashCode()+"=成员变量复制前="+instance.num);//0
System.out.println(clone01.hashCode()+"=成员变量复制后="+clone01.num);//1
/**
*浅克隆是指对象不一样,非引用的变量是全部复制一份到新的对象;深克隆,引用的变量也复制一份,如果引用层次比较多,使用copy方法会很麻烦
* 如果对象含有引用对象,需要实现深克隆,必须实体实现Cloneable接口,只是标记,不然会抛出异常,引用的对象也必须使用copy再次复制一遍,才会
* 把原始的引用对象进行复制。
*
* 实现克隆技术,主要是三种方式:
* 1.object copy 方法,,
* 2.spring 或者apache common 包提供的BeanUtils.copyProperties(stu2,stu1); PropertyUtils 这两个工具类
* 3.使用序列化方式,序列化后是对原始对象(包含所有引用对象)的复制进去文件,然后再次从文件读取实体,就是两个完全不同的对象。
* 序列化需要使用Serializable标记,不然抛出异常。
*/
System.out.println("证明copy 后的对象是和原始的对象是不同的,这里只是浅克隆,不是深克隆");
}
}
4.枚举实现单例
线程安全:是线程安全的,因为在被定义枚举时,默认生成的字节码文件继承了Enum类,定义的变量instance 在静态代码块已被初始化了,并且编译后是private static String instance;这种形式
所以,在jvm层面保证枚举的实例一定是独有,不可能存在第二个实例。
效率问题:枚举在初始化时,就及时加载对象,不是懒加载。
反射是否破坏:不会被破坏。因为不能被反射获取对象,JDK源码里面限制了如果是枚举类型不能使用反射实例化。
代码:
package com.venus.ioc.singleton;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.CountDownLatch;
/**
* 定义枚举,枚举单例是effect-java 书籍推荐的单例实现
*
* 为啥
*/
public enum EnumSingleton {
instance;
private User user;
private EnumSingleton(){
System.out.println("执行枚举单例");
}
public void setUser(User user) {
this.user = user;
}
public User getUser() {
return user;
}
public static void main(String[] args) throws IllegalAccessException, InstantiationException, InterruptedException, InvocationTargetException, NoSuchMethodException {
/*System.out.println("验证多线程是否破坏枚举单例");
new Thread(() -> {
User user =EnumSingleton.instance.user();
System.out.println(user.hashCode());
},"验证枚举单例线程1").start();
new Thread(() -> {
User user =EnumSingleton.instance.user();
System.out.println(user.hashCode());
},"验证枚举单例线程2").start();
Thread.sleep(100);
System.out.println("ok,多线程多次获取是同一对象");*/
System.out.println("----------反射获取实例");
Constructor<User> declaredConstructor = User.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
final User user01 = declaredConstructor.newInstance(null);
System.out.println(user01.hashCode());
final User user02 =declaredConstructor.newInstance(null);
System.out.println(user01==user02);
System.out.println(user02.hashCode());
System.out.println("测试结果:不能获取到单例对象");
}
static class User implements Serializable {
//Externalizable
public volatile static int count=1;
User(){
if(count<1){
throw new RuntimeException("已经实例化");
}
count--;
}
private String username;
public void setUsername(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
}
枚举序列化测试:
package com.venus.ioc.singleton;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 测试序列化和反序列化是不同的对象
*/
public class SerializableSingletonMode {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//validateSerializableSingletonMode();
//validateStaticInnerClassSingletonSerializable();
validateEnumSingletonSerializable();
}
/***
* 验证枚举的反序列化问题
*/
private static void validateEnumSingletonSerializable() throws IOException, ClassNotFoundException {
EnumSingleton enumSingleton01 = EnumSingleton.instance;
enumSingleton01.setUser(new EnumSingleton.User());
EnumSingleton enumSingleton02;
FileOutputStream fos = new FileOutputStream("C:\\Users\\lj\\Desktop\\serializale.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fos);
objectOutputStream.writeObject(enumSingleton01);
objectOutputStream.close();
fos.close();
System.out.println("把对象写入文件--完成");
FileInputStream fis = new FileInputStream("C:\\Users\\lj\\Desktop\\serializale.txt");
final ObjectInputStream objectInputStream = new ObjectInputStream(fis);
enumSingleton02=(EnumSingleton)objectInputStream.readObject();
objectInputStream.close();
fis.close();
System.out.println("从文件中读取序列化文件");
System.out.println(enumSingleton01.getUser()==enumSingleton02.getUser());
System.out.println(enumSingleton01.getUser());
System.out.println(enumSingleton02.getUser());
}
}
习惯性地想来看看 JDK 源码,进入 Constructor 的 newInstance()方法:会找到如下的判断:
if ((clazz.getModifiers() & Modifier.ENUM) != 0) {
throw new IllegalArgumentException("Cannot reflectively create enum objects");
}
5. Spring ioc 容器的单例
不讨论安全性等问题,只是简化spring初始模式方式:
代码:
package com.venus.ioc.singleton;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 这里模拟Spring ioc 容器的单例
*/
public class ContainerSingleton {
private static Map<String,ContainerSingleton> singeMap=new ConcurrentHashMap<>(2);
private ContainerSingleton(){}
public static ContainerSingleton getInstance(){
if(singeMap.size()==0){
synchronized (ContainerSingleton.class){
if(singeMap.size()==0){
singeMap.put("containerSingleton",new ContainerSingleton());
}
}
}
return singeMap.get("containerSingleton");
}
public static void main(String[] args) {
//测试容器式单例(单线程测试)
final ContainerSingleton instance01 = ContainerSingleton.getInstance();
final ContainerSingleton instance02 = ContainerSingleton.getInstance();
System.out.println(instance01==instance02);
System.out.println(instance01.hashCode());
System.out.println(instance02.hashCode());
System.out.println("多线程测试容器单例-----");
new Thread(() -> {
ContainerSingleton instance03 = ContainerSingleton.getInstance();
System.out.println(instance03.hashCode());
}, "测试容器单例线程1").start();
new Thread(() -> {
ContainerSingleton instance03 = ContainerSingleton.getInstance();
System.out.println(instance03.hashCode());
}, "测试容器单例线程2").start();
}
}
6.线程变量类实现伪单例
代码:
package com.venus.ioc.singleton;
/**
* 线程变量类实现伪单例
*/
public class ThreadLocalSingleton {
public String username;
private ThreadLocalSingleton(){}
private static ThreadLocal<ThreadLocalSingleton> threadLocal=new ThreadLocal<>();
public static ThreadLocalSingleton getInstance(){
ThreadLocalSingleton threadLocalSingleton = threadLocal.get();
if(threadLocalSingleton==null){
synchronized (ThreadLocalSingleton.class){
if(threadLocalSingleton==null){
threadLocal.set(new ThreadLocalSingleton());
}
}
}
return threadLocal.get();
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ThreadLocalSingleton instance03 = ThreadLocalSingleton.getInstance();
instance03.username="test03";
System.out.println(instance03.hashCode());
}, "测试ThreadLocal单例线程1").start();
new Thread(() -> {
ThreadLocalSingleton instance03 = ThreadLocalSingleton.getInstance();
System.out.println(instance03.username);
System.out.println(instance03.hashCode());
}, "测试ThreadLocal单例线程2").start();
Thread.sleep(100);
System.out.println("测试单线程---");
ThreadLocalSingleton instance01 = ThreadLocalSingleton.getInstance();
ThreadLocalSingleton instance02 = ThreadLocalSingleton.getInstance();
System.out.println(instance01.hashCode());
System.out.println(instance02.hashCode());
}
}
以上是根据自己工作学习之余总结的使用经验,便于自己以后复习。转载请注明出处。