单例模式
保证一个类在任何情况下都绝对只有一个实例,并且提供一个全局访问点
需要隐藏其所有构造方法
优点:
在内存中只有一个实例,减少了内存开销
可以避免对资源的多重占用
设置全局访问点,严格控制访问
缺点:
没有接口,扩展困难
如果要扩展单例对象,只有修改代码,没有别的途径
应用场景
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();
}
}
文中源码地址设计模式