大家好,今天给大家介绍一下单例模式。本文是从实际应用开发,结合网络上多篇技术博客,总结其精华、完善其缺陷和优化案例说明角度向大家展示什么叫做单例模式,如何创建单例及其优缺点和什么时候用单例。原创不易,点赞关注支持一下!
什么叫单例模式?
单例模式是设计模式中最简单的形式之一。这一模式的目的是使得类的一个对象成为系统中的唯一实例。
看起来很晦涩,白话一点说就是要具备某各类只能有一个实例、它必须自行创建这个实例和必须自行向整个系统提供这个实例。
如何创建实例?
单例的创建大致分为懒汉模式、饿汉模式、静态内部类、双重加锁等等。我们着重介绍和推荐使用的是双检锁模式,其他模式请自行百度 :-),话不多说直接上代码。
public class SingletonEntity {
private volatile static SingletonEntity singtonEntity = null;
private SingletonEntity(){
}
public void test(String context){
System.out.println(context);
}
public static SingletonEntity getInstance(){
if (singtonEntity == null){
synchronized (SingletonEntity.class){
if (singtonEntity == null){
singtonEntity = new SingletonEntity();
}
}
}
return singtonEntity;
}
}
注意:代码中一定要修改空参构造器为私有权限,防止代用构造器破坏单例原则。
如何使用?
/**
* 正常调用
*/
SingletonEntity instance1 = SingletonEntity.getInstance();
SingletonEntity instance2 = SingletonEntity.getInstance();
instance1.test("正常调用单例方法的测试类");
//测试单利是否同一个对象
System.out.println("正常调用instance1=" + instance1.hashCode() + ",instance2=" + instance2.hashCode());
运行得到结果:
正常调用单例方法的测试类
正常调用instance1=1625635731,instance2=1625635731
发现两次创建获得的对象地址是同一个,这个是在单线程环境下运行那么如果在多线程情况下会如何呢?
/**
* 多线程获取实例
*/
for (int i = 0; i < 50; i++) {
new Thread(()->{
SingletonEntity entity = SingletonEntity.getInstance();
System.out.println(Thread.currentThread().getName() + "获取到的对象地址" + entity.hashCode());
},"线程" + String.valueOf(i)).start();
}
运行结果:
线程0获取到的对象地址1625635731
线程1获取到的对象地址1625635731
线程2获取到的对象地址1625635731
线程6获取到的对象地址1625635731
线程4获取到的对象地址1625635731
线程5获取到的对象地址1625635731
线程3获取到的对象地址1625635731
线程7获取到的对象地址1625635731
线程8获取到的对象地址1625635731
线程9获取到的对象地址1625635731
线程11获取到的对象地址1625635731
线程15获取到的对象地址1625635731
线程10获取到的对象地址1625635731
...
多线程情况下获取到的对象地址也是一个。但是我们在JAVA中获取一个对象除new以外还有序列化/反序列化、反射等手段。那么序列化和反射是否能破坏单例呢?我们先用序列化方式运行看下:
/**
* 序列化方式调用
*/
//序列化
SingletonEntity singletonSerializable = SingletonEntity.getInstance();
FileOutputStream fileOutputStream = new FileOutputStream("temp");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(singletonSerializable);
objectOutputStream.close();
fileOutputStream.close();
//反序列化
FileInputStream fileInputStream = new FileInputStream("temp");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
SingletonEntity readObject = (SingletonEntity)objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
readObject.test("序列化调用的测试方法");
//单例类加了readResolve防止序列化之后地址不一致
System.out.println("序列化之前= " + singletonSerializable.hashCode() + ",序列化调用readObject=" + readObject.hashCode());
注意:此时直接调用会异常,需要类SingletonEntity 实现 Serializable接口
运行结果:
序列化调用的测试方法
序列化之前= 1625635731,序列化调用readObject=793589513
发现单例获得的对象被序列化之后对象的地址和远来的不一样了,这就违反了文章开篇提到的唯一实例原则。别急我们优化一下代码在SingletonEntity类中增加一段代码:
/**
* 防止序列化后的对象不一致
* @return
* @throws ObjectStreamException
*/
private Object readResolve() throws ObjectStreamException {
return singtonEntity;
}
这时候我们运行一下得到结果:
序列化调用的测试方法
序列化之前= 1625635731,序列化调用readObject=1625635731
在这解释一下readResolve方法,readResolve方法可以理解为一种约定。在目标类中定义一个私有的readResolve方法,然后再反序列化的时候会被调用到。readResolve在进行反序列化的时候执行循序在readObject之后,会覆盖readObject方法修改。通过此方式序列化/反序列化都不会破坏单例的唯一原则。 说完了序列化在说说反射是否能破坏呢?上代码:
/**
* 反射调用
*/
Constructor<SingletonEntity> singtonEntityClass = SingletonEntity.class.getDeclaredConstructor();//获取全部构造 包括私有
singtonEntityClass.setAccessible(true);//忽略修饰符检查
SingletonEntity singletonEntity = singtonEntityClass.newInstance();
SingletonEntity singletonEntity2 = singtonEntityClass.newInstance();
singletonEntity.test("反射调用单例方法的测试类");
System.out.println("反射调用singtonEntity=" + singletonEntity.hashCode() + ",singtonEntity2=" + singletonEntity2.hashCode());
运行结果:
反射调用单例方法的测试类
反射调用singtonEntity=1329552164,singtonEntity2=363771819
结果显示反射也是可以破坏单例的,想要解决也是可以的我们在类SingletonEntity中的私有构造器里面增加一小段代码。
private static volatile boolean flag = true;
private SingletonEntity(){
synchronized (SingletonEntity.class){
if(flag){
flag = false;
}else{
throw new RuntimeException("The instance already exists !");
}
}
}
调用反射破坏单例其实就是通过反射的方式拿到私有的构造器,我们对私有构造器进行增加判断在创建第二个对象的时候进行异常抛出。以上方法都是通过添加代码方式进行主观避免,那么是否有一中JAVA提供好的API给我们使用呢?答案是有的,我们引入了枚举方式实现单例。
枚举实现单例
写这篇文章之前查看了众多介绍用枚举实现单例的方式,总结一下代码如下(以下代码为错误枚举实现单例Demo):
public class UserSingletonEntity implements Serializable {
private UserSingletonEntity(){
}
static enum SingletonEnum {
//创建一个实例对象
INSTANCE;
private UserSingletonEntity userSingletonEntity;
private SingletonEnum(){
userSingletonEntity = new UserSingletonEntity();
}
public UserSingletonEntity getInstance(){
return userSingletonEntity;
}
}
public static UserSingletonEntity getInstance(){
return SingletonEnum.INSTANCE.getInstance();
}
}
调用方式和普通类一样,不加赘述。但是这种方式并没有解决序列化和反射破坏单例原则问题。我贴出来一个验证序列化破坏单例原则代码,反射破坏参照上述反射方法。
/**
* 枚举类单例 网上大多数写法
*/
UserSingletonEntity entity = UserSingletonEntity.getInstance();
UserSingletonEntity entity1 = UserSingletonEntity.getInstance();
System.out.println(entity == entity1);
UserSingletonEntity userSingletonEntity = UserSingletonEntity.getInstance();
FileOutputStream fileOutputStream1 = new FileOutputStream("temp1");
ObjectOutputStream objectOutputStream1 = new ObjectOutputStream(fileOutputStream1);
objectOutputStream1.writeObject(userSingletonEntity);
objectOutputStream1.close();
fileOutputStream1.close();
FileInputStream fileInputStream1 = new FileInputStream("temp1");
ObjectInputStream objectInputStream1 = new ObjectInputStream(fileInputStream1);
UserSingletonEntity readObject1 =
(UserSingletonEntity)objectInputStream1.readObject();
objectInputStream1.close();
fileInputStream1.close();
System.out.println("枚举类单例序列化之前= " + userSingletonEntity.hashCode() + ",序列化调用readObject=" + readObject1.hashCode());
运行结果:
true
枚举类单例序列化之前= 793589513,序列化调用readObject=1313922862
正确枚举实现单例写法:
public enum User {
INSTANCE;
private User(){};
public void test(String text){
System.out.println(text);
}
}
调用方式:
/**
* 枚举类正确写法调用
*/
for (int i = 0; i < 10; i++) {
final String a = i+"";
new Thread(()->{
User.INSTANCE.test(a);
},"线程" + i).start();
}
这种方式实现单例是可以避免序列化和反射破坏单例的,有代码为证:
//单例不支持反射,以下写法报错
Constructor<User> declaredConstructor =
User.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
User user = declaredConstructor.newInstance();
user.setName("aaaa");
System.out.println(user.getName());
运行结果:
为什么会报错呢?我们翻阅newInstance源码:
在JDK里规范定义就是不允许的。通过枚举这种方式可以避免反射和序列化方式破坏单例是值得推荐的,但是这种方式的类缺点一是不能作为数据库实体类来使用二是类的属性值设置时候需要注意属性状态,有状态属性在多线程下是不安全的。 枚举类的方式实现单例模式原理是因为枚举的调用方式是User.INSTANCE,这样也就避免调用getInstance方法进行反射调用。使用枚举单例的写法,我们完全不用考虑序列化和反射的问题。枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性。接下来看一下Enum类的valueOf方法:
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
实际上通过调用enumType(Class对象的引用)的enumConstantDirectory方法获取到的是一个Map集合,在该集合中存放了以枚举name为key和以枚举实例变量为value的Key&Value数据,因此通过name的值就可以获取到枚举实例,看看enumConstantDirectory方法源码:
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
//getEnumConstantsShared最终通过反射调用枚举类的values方法
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
//map存放了当前enum类的所有枚举实例变量,以name为key值
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
private volatile transient Map<String, T> enumConstantDirectory = null;
到这里我们也就可以看出枚举序列化和反射都不会重新创建新实例,jvm保证了每个枚举实例变量的唯一性。
总结
一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名 称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
双重检索实现单例方法优点是类的使用比较灵活可以支持高并发的场景,缺点是需要手动添加方法防止序列化和反射破坏单例原则;
枚举方式实现单例好处是利用JDK自带特性避免了反射和序列化破坏单例,坏处是枚举类使用场景有局限性。
优点:
1.在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
2.单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
3.提供了对唯一实例的受控访问。
4.由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
5.允许可变数目的实例。
6.避免对共享资源的多重占用。
缺点:
1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
2.由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
3.单例类的职责过重,在一定程度上违背了“单一职责原则”。
4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。