前言
想一想,如果公司里面有一个大牛,一个人就能把所有的事情都干了,那该多好!作为一个不喜欢卷来卷去的中国时代打工人,我喜欢这样的同事。作为吹着**春风成长起来的企业家,同样喜欢这样的员工(可以少聘用几个打工人了!!!可以裁掉几个打工人了!!!),可以省下不少成本!!!
如你所愿,单例模式就是这么一个时代楷模,虽然不能包揽所有工作,但是分内之事,一人足矣!接下来,来看一个简单的实例吧。
实例一:
package com.elean.design.singleton;
/**
* @auther Elean
* @date 17/7/2024 下午4:23
* @description
*/
public class SuperMan {
public static SuperMan tom = new SuperMan();
//拒绝招聘
private SuperMan() {
}
//有事找Tom
public static final SuperMan getInstance() {
return tom;
}
public void serveTea() {
System.out.println("端茶,我来");
}
public void pourWater() {
System.out.println("倒水,我来");
}
}
在这个案例里面,我们可以看到单例的几个最基本特点:
1)构造函数私有(拒绝招聘新人,Tom is super man)
2)有一个类变量来承载单一实例
3)给一个静态方法获取实例
当然,如果老板突然有一天喜欢上骑马、烧柴,就需要老实肯干的Tom另外学习喂马、劈柴的技能,如果没涨工资,这就对Tom是很不公平了(也违背了开闭原则:对扩展开放,对修改关闭)。这时候,就需要老板再开放一个岗位主管所有部门的马和柴,为了进一步节约成本,老板决定,招柴买马那天再设此岗位。
实例二:
package com.elean.design.singleton;
/**
* @auther Elean
* @date 17/7/2024 下午4:23
* @description
*/
public class BatMan {
public static BatMan xiaoMing;
private BatMan() {
}
public static final BatMan getInstance() {
if (null == xiaoMing) {
xiaoMing = new BatMan();
}
return xiaoMing;
}
public void horseFeed() {
System.out.println("喂马,我去");
}
public void choppingFirewood() {
System.out.println("劈柴,我去");
}
}
一、单线程下的使用及存在的问题
1.1 饿汉模式和懒汉模式
以上两个示例是单例模式中两种比较简单的写法:饿汉模式和懒汉模式
对比两种写法来看,饿汉模式在类的初始化过程会自动调用它的实例化方法给Tom赋值,而后一直存在于内存中;懒汉模式则是在调用该类的instance()方法时才会进行实例化。从表面上看,懒汉模式对资源的占用相对更小一些。但是我们知道,用户自定义类只有在被使用,或者子类被加载的时候才会被加载,如果我们能够做到让类的加载时间跟使用实例的时间一致,那么这种消耗是可以避免的,这就要求我们熟练掌握jvm的类加载机制,小心使用饿汉模式了。
1.2 反射破坏单例
在单线程中,一般情况下上面两个简单示例是完全可以保证内存中单例了。当然,如果我们使用反射把构造函数设置成true同样可以在内存中创建实例,这种情况,可以在构造函数中像懒汉模式一样先判断内存中是否有实例存在,如下代码。
package com.elean.design.singleton;
import java.lang.reflect.Constructor;
/**
* @auther Elean
* @date 18/7/2024 下午8:40
* @description
*/
public class SingleReflect {
public static void main(String[] args) {
SuperMan instance = SuperMan.getInstance();
try {
Class<?> clazz = Class.forName("com.elean.design.singleton.SuperMan");
Constructor<?> constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object o = constructor.newInstance();
System.out.println(instance);
System.out.println(o);
System.out.println(instance == o); //打印结果:false
} catch (Exception e) {
e.printStackTrace();
}
}
}
在构造函数中判空:饿汉模式和懒汉模式都可以用这种方式解决
public class SuperMan {
...
private SuperMan() {
if (null != tom) {
throw new RuntimeException("请不要破坏单例结构。。。");
}
}
...
}
1.3 序列化破坏单例
也有大佬提供了另外一种破坏单例的方法——序列化,以及解决办法(参考:设计模式之单例模式详解和应用-CSDN博客)
package com.elean.design.singleton;
import java.io.*;
/**
* @auther Elean
* @date 19/7/2024 上午9:33
* @description
*/
public class BreakSingle {
public static void main(String[] args) {
SuperMan instance = SuperMan.getInstance();
try (
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SuperMan.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("SuperMan.txt"))
){
oos.writeObject(instance);
Object o = ois.readObject();
System.out.println(instance);
System.out.println(o);
System.out.println(o == instance); //打印结果:false
} catch (Exception e) {
e.printStackTrace();
}
}
}
解决序列化破环单例:添加readResove()方法
package com.elean.design.singleton;
import java.io.Serializable;
/**
* @auther Elean
* @date 17/7/2024 下午4:23
* @description
*/
public class SuperMan implements Serializable {
public static SuperMan tom = new SuperMan();
private SuperMan() {
if (null != tom) {
throw new RuntimeException("请不要破坏单例结构。。。");
}
}
public static final SuperMan getInstance() {
return tom;
}
private Object readResolve() {
return tom;
}
public void serveTea() {
System.out.println("端茶,我来");
}
public void pourWater() {
System.out.println("倒水,我来");
}
}
在我看来,如果不希望序列化去破坏单例的话,我们实在没必要让这个类实现Serializable接口。当然,如果非要说什么特殊情况,谨慎使用吧!!!
二、多线程下的单例模式
2.1 多线程破坏懒汉模式
饿汉模式是在类加载的时候就初始化了类变量,将实例存入内存,因此在多线程中不会出现单例被破坏的情况。所以这里我们主要讨论懒汉模式,下面看下被破坏的场景:
为了模拟,我们让静态实例方法在判空后先睡一会
package com.elean.design.singleton;
/**
* @auther Elean
* @date 17/7/2024 下午4:23
* @description
*/
public class BatMan {
...
public static final BatMan getInstance() {
if (null == xiaoMing) {
/**************模拟多线程同时进入的情况*******************/
try {
System.out.println(Thread.currentThread().getName() + "调用实例");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/******************************************************/
xiaoMing = new BatMan();
}
return xiaoMing;
}
...
}
测试代码:
package com.elean.design.singleton;
/**
* @auther Elean
* @date 19/7/2024 上午9:33
* @description
*/
public class BreakSingle {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
BatMan instance1 = BatMan.getInstance();
System.out.println(instance1);
});
thread.start();
BatMan instance = BatMan.getInstance();
System.out.println(instance);
}
}
打印结果:
main调用实例
Thread-0调用实例
com.elean.design.singleton.BatMan@3b9a45b3
com.elean.design.singleton.BatMan@22e7a307
从打印结果来看,主线程和线程0调用静态实例方法创建的实例的内存地址是不一样的,也就是创建了不同的实例。当不同的线程同时调用静态实例方法的时候,可能出现线程a还未创建实例时,线程b调用了为空的判断,这时,两个线程都会运行创建实例的代码。因此,在实际项目应用中,通常都会用以下两种方式来防止单例被破坏:
2.2 双重校验锁
package com.elean.design.singleton;
/**
* @auther Elean
* @date 17/7/2024 下午4:23
* @description
*/
public class BatMan {
...
public static final BatMan getInstance() {
if (null == xiaoMing) {
/**************模拟多线程同时进入的情况*******************/
try {
System.out.println(Thread.currentThread().getName() + "调用实例");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/******************************************************/
synchronized (BatMan.class) {
if (null == xiaoMing) {
xiaoMing = new BatMan();
}
}
}
return xiaoMing;
}
...
}
双重校验锁保证了在多线程的情况下只有一个线程能够创建实例。但同一时间进来的线程,因为拿不到锁会被阻塞掉,会影响被阻塞线程的响应速度。
2.3 静态内部类
package com.elean.design.singleton;
/**
* @auther Elean
* @date 17/7/2024 下午4:23
* @description
*/
public class BatMan {
private BatMan() {
}
public static final BatMan getInstance() {
return InstanceInner.xiaoMing;
}
//InstanceInner和外部类会被编译成两个文件,InstanceInner在使用时才会被加载
private static class InstanceInner{
public static final BatMan xiaoMing = new BatMan();
}
public void horseFeed() {
System.out.println("喂马,我去");
}
public void choppingFirewood() {
System.out.println("劈柴,我去");
}
}
打印结果:
main调用实例
Thread-0调用实例
com.elean.design.singleton.BatMan@3b9a45b3
com.elean.design.singleton.BatMan@3b9a45b3
三、单例模式的其他实现方式
3.1 枚举类实现单例
package com.elean.design.singleton;
/**
* @auther Elean
* @date 20/7/2024 下午6:53
* @description
*/
public enum EnumInstance {
XIAO_MING;
public void horseFeed() {
System.out.println("喂马,我去");
}
public void choppingFirewood() {
System.out.println("劈柴,我去");
}
}
测试代码:
package com.elean.design.singleton;
/**
* @auther Elean
* @date 21/7/2024 上午10:48
* @description
*/
public class EnumTest {
public static void main(String[] args) {
EnumInstance.XIAO_MING.horseFeed();
EnumInstance.XIAO_MING.choppingFirewood();
}
}
打印结果:
喂马,我去
劈柴,我去
枚举类之所以能够实现单例,我们对代码进行编译(javac)之后反编译(javap -c && javap -p)的代码如下。在代码中我们看到实例是被static和final修饰的,而其被实例化是在静态代码块中调用了<init>的方法实现的。这和我们的饿汉式单例模式实现如出一辙。
javap -p EnumInstance.class
public final class com.elean.design.singleton.EnumInstance extends java.lang.Enum<com.elean.design.singleton.EnumInstance> {
public static final com.elean.design.singleton.EnumInstance XIAO_MING;
private static final com.elean.design.singleton.EnumInstance[] $VALUES;
public static com.elean.design.singleton.EnumInstance[] values();
public static com.elean.design.singleton.EnumInstance valueOf(java.lang.String);
private com.elean.design.singleton.EnumInstance();
public void horseFeed();
public void choppingFirewood();
static {};
}
javap -c EnumInstance.class
static {};
Code:
0: new #4 // class com/elean/design/singleton/EnumInstance
3: dup
4: ldc #11 // String XIAO_MING
6: iconst_0
7: invokespecial #12 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #13 // Field XIAO_MING:Lcom/elean/design/singleton/EnumInstance;
13: iconst_1
14: anewarray #4 // class com/elean/design/singleton/EnumInstance
17: dup
18: iconst_0
19: getstatic #13 // Field XIAO_MING:Lcom/elean/design/singleton/EnumInstance;
22: aastore
23: putstatic #1 // Field $VALUES:[Lcom/elean/design/singleton/EnumInstance;
26: return
枚举实现饿汉单例模式从jdk中对类实例化的代码实现来看,先天拒绝了通过反射破坏单例。通过 getDeclaredConstructor() 方法,获取枚举类的所有构造函数,枚举类只有一个(String,int) 的构造函数;当通过该构造函数的 newInstance() 方法获取实例时,jdk拒绝了我们的实例化请求,原因无它,jdk在constructor方法中判断了修饰符是否ENUM,如果是ENUM就会抛出异常,代码如下:
@CallerSensitive
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
{
...
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
...
}
同理,在反序列化的代码中同样有对枚举类型的判断,而readEnum(...)方法中用Enum的valueOf方法通过类类型和枚举名称在常量中获取枚举实例,也就是说,获取的是同一个实例。
/**
* readObject0()
*/
...
case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));
...
private Enum<?> readEnum(boolean unshared) throws IOException {
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
}
...
}
...
}
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
hrow new IllegalArgumentException(getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
3.2 注册式单例
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className){
Object instance = null;
if(!ioc.containsKey(className)){
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
}catch (Exception e){
e.printStackTrace();
}
return instance;
}else{
return ioc.get(className);
}
}
}
单例模式在java开源框架中的应用还是比较多的,比如Spring IOC的容器,线程池连接,数据库连接池等的实现,让我们一起掌握好单例的应用,在java开发中大展拳脚吧!!!