设计模式-单例模式详解

39次阅读

共计 8518 个字符,预计需要花费 22 分钟才能阅读完成。

单例模式
​ 保证一个类在任何情况下都绝对只有一个实例,并且提供一个全局访问点
​ 需要隐藏其所有构造方法
​ 优点:
​ 在内存中只有一个实例,减少了内存开销
​ 可以避免对资源的多重占用
​ 设置全局访问点,严格控制访问
​ 缺点:
​ 没有接口,扩展困难
​ 如果要扩展单例对象,只有修改代码,没有别的途径
应用场景
​ ServletContext
​ ServletConfig
​ ApplicationContext
​ DBPool
常见的单例模式写法
饿汉式单例
​ 饿汉式就是在初始化的时候就初始化实例
​ 两种代码写法如下:
public class HungrySingleton {
private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

private HungrySingleton() {

}

private static HungrySingleton getInstance() {
return HUNGRY_SINGLETON;
}
}
public class HungryStaticSingleton {
private static final HungryStaticSingleton HUNGRY_SINGLETON;

static {
HUNGRY_SINGLETON = new HungryStaticSingleton();
}

private HungryStaticSingleton() {

}

private static HungryStaticSingleton getInstance() {
return HUNGRY_SINGLETON;
}
}
​ 如果没有使用到这个对象,因为一开始就会初始化实例,这种方式会浪费内存空间
懒汉式单例
​ 懒汉式单例为了解决上述问题,则是在用户使用的时候才初始化单例

public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;

private LazySimpleSingleton() {

}

public static LazySimpleSingleton getInstance() {
// 加上空判断保证初只会初始化一次
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();//11 行
}
return lazySimpleSingleton;
}
}
​ 上述方式,线程不安全, 如果两个线程同时进入 11 行,那么会创建两个对象,需要如下,给方法加锁
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;

private LazySimpleSingleton() {

}

public synchronized static LazySimpleSingleton getInstance() {
// 加上空判断保证初只会初始化一次
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}
}
​ 上述方式虽然解决了线程安全问题,但是整个方法都是锁定的,性能比较差,所以我们使用方法内加锁的方式解决提高性能
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;

private LazySimpleSingleton() {

}

public static LazySimpleSingleton getInstance() {
// 加上空判断保证初只会初始化一次
if (lazySimpleSingleton == null) {
synchronized (LazySimpleSingleton.class) {//11 行
lazySimpleSingleton = new LazySimpleSingleton();
}
}
return lazySimpleSingleton;
}
}
​ 上述方式如果两个线程同时进入了 11 行,一个线程 a 持有锁,一个线程 b 等待,当持有锁的 a 线程释放锁之后到 return 的时候,第二个线程 b 进入了 11 行内部,创建了一个新的对象,那么这时候创建了两个线程,对象也并不是单例的。所以我们需要在 12 行位置增加一个对象判空的操作。
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;

private LazySimpleSingleton() {

}

public static LazySimpleSingleton getInstance() {
// 加上空判断保证初只会初始化一次
if (lazySimpleSingleton == null) {
synchronized (LazySimpleSingleton.class) {
if (lazySimpleSingleton != null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
}
}
return lazySimpleSingleton;
}
}
​ 上述方式还是有风险的,因为 CPU 执行时候会转化成 JVM 指令执行:
​ 1. 分配内存给对象
​ 2. 初始化对象
​ 3. 将初始化好的对象和内存地址建立关联,赋值
​ 4. 用户初次访问
​ 这种方式,在 cpu 中 3 步和 4 步有可能进行指令重排序。有可能用户获取的对象是空的。那么我们可以使用 volatile 关键字,作为内存屏障,保证对象的可见性来保证我们对象的单一。
public class LazySimpleSingleton {
private static volatile LazySimpleSingleton lazySimpleSingleton = null;

private LazySimpleSingleton() {

}

public static LazySimpleSingleton getInstance() {
// 加上空判断保证初只会初始化一次
if (lazySimpleSingleton == null) {
synchronized (LazySimpleSingleton.class) {
if (lazySimpleSingleton != null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
}
}
return lazySimpleSingleton;
}
}
静态内部类单例
​ 还有一种懒汉式单例,利用静态内部类在调用的时候等到外部方法调用时才执行,巧妙的利用了内部类的特性,jvm 底层逻辑来完美的避免了线程安全问题
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {

}

public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}

private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
​ 这种方式虽然能够完美单例,但是我们如果使用反射的方式如下所示,则会破坏单例
public class LazyInnerClassTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = LazyInnerClassSingleton.getInstance();
System.out.println(o1 == o2);
}
}
​ 怎么办呢,我们需要一种方式控制访问者的行为,通过异常的方式去限制使用者的行为, 如下所示

public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
throw new RuntimeException(“ 不允许构建多个实例 ”);
}

public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}

private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
​ 还有一种方式会破坏单例,那就是序列化破坏我们的单例,如下所示
序列化破坏单例
​ 我们写一个序列化的方法来尝试一下上述写法是否是满足序列化的。
public class SeriableSingletonTest {
public static void main(String[] args) {
SeriableSingleton seriableSingleton = SeriableSingleton.getInstance();
SeriableSingleton s2;
FileOutputStream fos = null;
FileInputStream fis = null;

try {
fos = new FileOutputStream(“d.o”);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(seriableSingleton);
oos.flush();
oos.close();
fis = new FileInputStream(“d.o”);
ObjectInputStream ois = new ObjectInputStream(fis);
s2 = (SeriableSingleton) ois.readObject();
ois.close();
System.out.println(seriableSingleton);
System.out.println(s2);
System.out.println(s2 == seriableSingleton);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
​ 为什么序列化会破坏单例呢,我们查看 ObjectInputStream 的源码
首先,我们查看 ObjectInputStream 的 readObject 方法

查看 readObject0 方法

查看 checkResolve(readOrdinaryObject(unshared) 方法可以看到

​ 红框内三目运算符内如果 desc.isInstantiable() 为真就创建新对象,不为空就返回空,此时我们查看 desc.isInstantiable() 方法

此处 cons 是
如果有构造方法就会返回 true, 当然我们一个类必然会有构造方法的,所以这就是为什么序列化会破坏我们的单例
那么怎么办呢,我们只需要重写 readResolve 方法就行了
public class SeriableSingleton implements Serializable {
private SeriableSingleton() {
throw new RuntimeException(“ 不允许构建多个实例 ”);
}

public static final SeriableSingleton getInstance() {
return LazyHolder.LAZY;
}

private static class LazyHolder {
private static final SeriableSingleton LAZY = new SeriableSingleton();
}

private Object readResolve() {
return getInstance();
}
}
为什么重写这个 readResolve 的方法就能够避免序列化破坏单例呢
回到上述 readOrdinaryObject 方法,可以看到有一个 hasReadResolveMethod 方法

点进去

可以看到 readResolveMethod 在此处赋值

也就是我们如果类当中有此方法则在 hasReadResolveMethod 当中返回的是 true
那么会进入 readOrdinaryObject 的如下部分

并且如下所示,调用我们的 readResolve 方法获取对象,来保证我们对象是单例的

​ 但是重写 readResolve 方法,只不过是覆盖了反序列化出来的对象,但是还是创建了两次,发生在 JVM 层面,相对来说比较安全,之前反序列化出来的对象会被 GC 回收
注册式单例
枚举单例
​ 枚举式单例属于注册式单例,他把每一个实例都缓存到统一的容器中,使用唯一标识获取实例。也是比较推荐的一种写法,如下所示:
public enum EnumSingleton {
INSTANCE;
private Object data;

public static EnumSingleton getInstance() {
return INSTANCE;
}
}
​ 反编译上述文件,可以看到

​ 那么序列化能不能破坏枚举呢
​ 在 ObjectInputStream 的 readObject 方法中有针对枚举的判断

上述通过一个类名和枚举名字值来确定一个枚举值。从而枚举在序列化上是不会破坏单例的。
我们尝试使用反射来创建一个枚举对象
public enum EnumSingleton {
INSTANCE;
private Object data;

EnumSingleton() {

}

public static EnumSingleton getInstance() {
return INSTANCE;
}

public static void main(String[] args) {
Class clazz = EnumSingleton.class;
try {
Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
c.newInstance(“dd”, 1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
抛出异常

查看 Constructor 源码可以看到

可以看到 jdk 层面如果判断是枚举会抛出异常,所以枚举式单例是一种比较推荐的单例的写法。
容器式单例
这种方式是通过容器的方式来保证我们对象的单例,常见于 Spring 的 IOC 容器
public class ContainerSingleton {
private ContainerSingleton() {

}

private static Map<String, Object> ioc = new ConcurrentHashMap<>();

public static Object getBean(String className) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();//12
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
return ioc.get(className);
}

public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(100);
final CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
Object o = ContainerSingleton.getBean(“com.zzjson.singleton.register.ContainerSingleton”);
System.out.println(o + “”);
countDownLatch.countDown();
}
});
}
countDownLatch.await();
executorService.shutdown();

}

}
这种方式测试可见

出现了几次不同对象的情况因为我们线程在 12 行可能同时进入,这时候我们需要加一个同步锁如下,这样创建对象才是只会创建一个的
public class ContainerSingleton {
private ContainerSingleton() {

}

private static Map<String, Object> ioc = new ConcurrentHashMap<>();

public static Object getBean(String className) {
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
}
return ioc.get(className);
}

public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(100);
final CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
Object o = ContainerSingleton.getBean(“com.zzjson.singleton.register.ContainerSingleton”);
System.out.println(o + “”);
countDownLatch.countDown();
}
});
}
countDownLatch.await();
executorService.shutdown();

}

}
ThreadLocal 单例
这种方式只能够保证在当前线程内的对象是单一的
public class ThreadLocalSingleton {
private ThreadLocalSingleton() {
}

private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}

};

private static ThreadLocalSingleton getInstance() {
return threadLocalInstance.get();
}
}
文中源码地址设计模式

正文完
 0