文章目录
1. 定义
指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点
2. 适用场景
- 确保任何情况下都绝对只有一个实例
- ServletContext,ServletConfig,ApplicationContext,DBPool
3. 分类
- 饿汉式单例
- 懒汉式单例
- 注册式单例
- ThreadLocal 单例
4. 饿汉式单例
饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象.绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题
4.1 优/缺点
- 优点:没有加任何的锁,执行效率比较高,在用户体验上来说,比懒汉式更好
- 缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存,有可能占着茅坑不拉屎
4.2 饿汉式单例分类
- 直接通过 new 创建实例
- 通过 static 模块创建实例
4.2.1 直接通过 new 创建实例
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
4.2.2 通过 static 模块创建实例
public class HungryStaticSingleton {
private static final HungryStaticSingleton hungrySingleton;
private HungryStaticSingleton() {}
static {
hungrySingleton = new HungryStaticSingleton();
}
public static HungryStaticSingleton getInstance() {
return hungrySingleton;
}
}
5. 懒汉式单例
当类被外部调用时才创建实例
懒汉式单例分为以下几种:
- 简单懒汉式单例
- 双重检查懒汉式单例
- 静态内部类懒汉式单例
5.1 简单懒汉式单例
- 在并发场景下存在线程安全问题,可以创建出多个对象
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;
private LazySimpleSingleton(){}
public static LazySimpleSingleton getInstance() {
if (null == lazySimpleSingleton) {
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}
}
5.1.1 多线程调试
在 IDEA 中用线程模式调试,手动控制线程的执行顺序来跟踪内存的变化状态,给 LazySimpleSingleton 加上断点,选择 Thread 模式
debug 启动测试类LazySimpleSingletonTest
,线程 0 和 1 进入断点,在 debug 中将线程控制下拉框中切换至线程 0,执行下一行代码后停住
切换至线程 1 执行下一行代码后停住,LazySimpleSingleton
类被实例化了两次,这样就破坏了单例模式
输出结果,LazySimpleSingleton 实例化两个对象
多线程类
public class ExecutorThread implements Runnable {
@Override
public void run() {
LazySimpleSingleton lazySimpleSingleton = LazySimpleSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + lazySimpleSingleton);
}
}
多线程测试类
public class LazySimpleSingletonTest {
public static void main(String[] args) {
Thread thread1 = new Thread(new ExecutorThread());
Thread thread2 = new Thread(new ExecutorThread());
thread1.start();
thread2.start();
System.out.println("执行结束");
}
}
5.2 双重检查懒汉式单例
- 通过 synchronized 和双重检查,解决
简单懒汉式单例
存在的线程安全问题
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance() {
if (null == lazyDoubleCheckSingleton) {
synchronized (LazyDoubleCheckSingleton.class) {
if (null == lazyDoubleCheckSingleton) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
5.2.1 多线程调试
在 IDEA 中用线程模式调试,手动控制线程的执行顺序来跟踪内存的变化状态,给 LazyDoubleCheckSingleton 加上断点,选择 Thread 模式
debug 启动测试类LazyDoubleCheckSingletonTest
,线程 0 和 1 进入断点,此时线程 0 和 1 都是 RUNNING
在 debug 中将线程控制下拉框中切换至线程 0,执行下一行代码后停住,此时线程 0 和 1 都是 RUNNING
线程再切换至线程 1 执行下一行代码,此时线程 0 是 RUNNING,线程 1 是 MONITOR,线程 0 拿到锁的情况下,线程 1 无法进入创建实例代码区域
直到线程 0 执行完释放锁线程 1 才能执行代码,此时线程 0 和 1 都是 RUNNING,lazyDoubleCheckSingleton 对象已经创建,if(null==lazyDoubleCheckSingleton)
因条件不成立而不进行实例创建,这样线程就是安全的
执行结果,LazyDoubleCheckSingleton 类只被实例化了一次
5.3 静态内部类懒汉式单例
- 全程没有用到 synchronized
- 巧妙利用了内部类的特性
- JVM 底层执行逻辑,完美的避免了线程安全问题
- 存在被反射攻击的风险,通过
if(null!=LazyHolder.LAZY)
解决反射问题
/**
* 静态内部类懒汉式单例
*/
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton(){}
// LazyHolder 里面的逻辑需要等到外部方法调用时才执行
// 全程没有用到 synchronized
// 巧妙利用了内部类的特性
// JVM 底层执行逻辑,完美的避免了线程安全问题
public static LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
5.4 反射破坏单例
5.4.1 反射如何破坏单例
之前的饿汉和懒汉单例模式的构造方法除了加上 private,如果我们使用反射来调用其构造方法,然后再调用 getInstance()方法,应该就会两个不同的实例
public class LazyInnerClassSingletonTest {
public static void main(String[] args) throws NoSuchMethodException {
try {
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object object1 = constructor.newInstance();
Object object2 = LazyInnerClassSingleton.getInstance();
System.out.println(object1 == object2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果为 false,object1 是通过反射创建的,object2 是通常正常创建的,这是两个指向不同内存地址的对象,破坏了单例
5.4.2 解决反射破坏单例
在私有构造方法中增加判断能防止反射破坏单例
private LazyInnerClassSingleton(){
/**
* 在私有构造方法中增加判断能防止反射破坏单例
*/
if (null != LazyHolder.LAZY) {
throw new RuntimeException("禁止反射创建实例");
}
}
/**
* 静态内部类懒汉式单例
*/
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton(){
/**
* 在私有构造方法中增加判断能防止反射破坏单例
*/
if (null != LazyHolder.LAZY) {
throw new RuntimeException("禁止反射创建实例");
}
}
// LazyHolder 里面的逻辑需要等到外部方法调用时才执行
// 全程没有用到 synchronized
// 巧妙利用了内部类的特性
// JVM 底层执行逻辑,完美的避免了线程安全问题
public static LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
运行结果
5.5 序列化破坏单例
通过序列化将对象输出到文件,再通过反序列化将文件加载到内存,这样单例实例将在内存中存在两个对象,从而破坏单例模式
5.5.1 序列化如何破坏单例
饿汉模式的单例
public class SerializableSingleton implements Serializable {
private static final SerializableSingleton serializableSingleton = new SerializableSingleton();
private SerializableSingleton() {}
public static SerializableSingleton getInstance() {
return serializableSingleton;
}
}
序列化测试类
public class SerializableSingletonTest {
public static void main(String[] args) {
SerializableSingleton s1 = null;
SerializableSingleton s2 = SerializableSingleton.getInstance();
FileOutputStream fileOutputStream = null;
FileInputStream fileInputStream = null;
ObjectOutputStream objectOutputStream = null;
ObjectInputStream objectInputStream = null;
try {
fileOutputStream = new FileOutputStream("./singleton/SerializableSingleton.obj");
objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(s2);
objectOutputStream.flush();
objectOutputStream.close();
fileInputStream = new FileInputStream("./singleton/SerializableSingleton.obj");
objectInputStream = new ObjectInputStream(fileInputStream);
s1 = (SerializableSingleton) objectInputStream.readObject();
objectInputStream.close();
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(objectInputStream);
IOUtils.closeQuietly(objectOutputStream);
IOUtils.closeQuietly(fileInputStream);
IOUtils.closeQuietly(fileOutputStream);
}
}
}
输出结果为两个对象 s1,s2 不相等,SerializableSingleton 对象实例化了两个对象,分别指向不同的内存地址
5.5.2 解决序列化破坏单例模式
在单例类中重写readResolve()
方法
public class SerializableSingleton implements Serializable {
private static final SerializableSingleton serializableSingleton = new SerializableSingleton();
private SerializableSingleton() {}
public static SerializableSingleton getInstance() {
return serializableSingleton;
}
// 重写 readResolve 方法只不过是覆盖了反序列化出来的对象
// 还是创建了两次,只不过是发生在 JVM 层面,相对来说说比较安全
// 之前反序列化出来的对象会被 GC 回收
private Object readResolve() {
return serializableSingleton;
}
}
5.5.3 源码解读
在序列化测试类的代码中s1=(SerializableSingleton)objectInputStream.readObject()
,查看readObject()
方法源码
public final Object readObject() throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
在readObject()
源码中可以查看到readObject0()
方法,在TC_OBJECT
中调用readOrdinaryObject()
private Object readObject0(boolean unshared) throws IOException {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
...
}
在readOrdinaryObject()
方法中desc.isInstantiable()
判断是否存在构造方法
private Object readOrdinaryObject(boolean unshared) throws IOException
{
...
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
...
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
}
调用了 ObjectStreamClass 的 isInstantiable()方法,而 isInstantiable()里面的代码,判断一下构造方法是否为空,构造方法不为空就返回 true,意味着只要有无参构造方法就会实例化
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
判断无参构造方法是否存在之后,又调用了 hasReadResolveMethod()方法
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
就是判断 readResolveMethod 是否为空,不为空就返回 true,通过全局查找找到了 ObjectStreamClass 中对 readResolveMethod 赋值代码在私有方法
readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
在代码可以看到这样一行代码if(obj!=null&&handles.lookupException(passHandle)==null&&desc.hasReadResolveMethod())
,如果这个判断返回为 true,将能执行desc.invokeReadResolve(obj)
调用序列化对象中的 readResolve 方法
- obj 已经实例化不为空
- handles.lookupException(passHandle)返回结果为空,没有异常
- desc.hasReadResolveMethod()返回 true
private Object readOrdinaryObject(boolean unshared) throws IOException
{
...
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
}
invokeReadResolve()
方法中用反射调用了readResolveMethod
方法
Object invokeReadResolve(Object obj) throws IOException, UnsupportedOperationException
{
requireInitialized();
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ObjectStreamException) {
throw (ObjectStreamException) th;
} else {
throwMiscException(th);
throw new InternalError(th); // never reached
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
通过 JDK 源码分析我们可以看出,虽然增加readResolve()
方法返回实例,解决了单例被破坏的问题.但是我们通过分析源码以及调试,我们可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已.那如果创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,难道真的就没办法从根本上解决问题吗?下面我们来注册式单例也许能帮助到你
6. 注册式单例
注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例
注册式单例有两种写法:
- 容器缓存
- 枚举登记
6.1 枚举登记式单例
枚举单例类
public enum EnumSingleton {
INSTANCE;
private Object data;
public void setData(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
反序列化测试类
public class EnumSingletonTest {
public static void main(String[] args) {
EnumSingleton s1 = null;
EnumSingleton s2 = EnumSingleton.getInstance();
FileOutputStream fileOutputStream = null;
FileInputStream fileInputStream = null;
ObjectOutputStream objectOutputStream = null;
ObjectInputStream objectInputStream = null;
try {
fileOutputStream = new FileOutputStream("./singleton/EnumSingleton.obj");
objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(s2);
objectOutputStream.flush();
objectOutputStream.close();
fileInputStream = new FileInputStream("./singleton/EnumSingleton.obj");
objectInputStream = new ObjectInputStream(fileInputStream);
s1 = (EnumSingleton) objectInputStream.readObject();
objectInputStream.close();
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(objectInputStream);
IOUtils.closeQuietly(objectOutputStream);
IOUtils.closeQuietly(fileInputStream);
IOUtils.closeQuietly(fileOutputStream);
}
}
}
运行结果,两个对象 s1,s2 指向同一块内存地址,是同一个对象
这种方式没有做任何处理却能完美的解决序列化破坏单例,那么枚举式单例如此神奇,通过反编译工具解开神秘面纱
在 IDEA 中找到 EnumSingleton 对应的 class 文件 EnumSingleton.class,复制所在路径
然后切回到命令行,切换到工程所在的 Class 目录,输入命令 jad 后面输入复制好的路径,我们会在 Class 目录下会多一个 EnumSingleton.jad 文件.打开 EnumSingleton.jad 文件我们惊奇又巧妙地发现有如下代码:
static {
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
原来枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例的实现.我们还可以试想,序列化我们能否破坏枚举式单例呢?我们不妨再来看一下 JDK 源码,还是回到 ObjectInputStream 的 readObject0()方法
private Object readObject0(boolean unshared) throws IOException {
...
case TC_ENUM:
return checkResolve(readEnum(unshared));
...
}
在 readObject0()中调用了 readEnum()方法,来看 readEnum()中代码实现
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
发现枚举类型其实通过类名和 Class 对象类找到一个唯一的枚举对象,因此枚举对象不可能被类加载器加载多次
那么反射是否能破坏枚举式单例呢?来看一段测试代码:
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor();
c.newInstance();
}catch (Exception e){
e.printStackTrace();
}
}
运行结果
报的是 java.lang.NoSuchMethodException 异常,意思是没找到无参的构造方法.这时候,我们打开 java.lang.Enum 的源码代码,查看它的构造方法,只有一个 protected 的构造方法,代码如下:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
那我们再来做一个这样的测试:
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
c.setAccessible(true);
EnumSingleton enumSingleton = (EnumSingleton)c.newInstance("Tom",666);
}catch (Exception e){
e.printStackTrace();
}
}
运行结果
这时错误已经非常明显了,告诉我们 Cannotreflectivelycreateenumobjects,不能用反射来创建枚举类型.还是习惯性地想来看看 JDK 源码,进入 Constructor 的 newInstance()方法:
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0) {
throw new IllegalArgumentException("Cannot reflectively create enum objects");
}
ConstructorAccessor ca = constructorAccessor;
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
在 newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型,直接抛出异常.到这为止,我们是不是已经非常清晰明了呢?枚举式单例也是《EffectiveJava》书中推荐的一种单例实现写法.在 JDK 枚举的语法特殊性,以及反射也为枚举保驾护航,让枚举式单例成为一种比较优雅的实现
6.2 容器缓存单例
容器式写法适用于创建实例非常多的情况,便于管理,是非线程安全的
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
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;
} else {
return ioc.get(className);
}
}
}
}
Spring 中的容器式单例的实现代码
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {
/** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */
private final Map<String, BeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<>(16);
...
}
7. ThreadLocal 单例
ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全.下面我们来看代码:
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton() {
}
public static ThreadLocalSingleton getInstance() {
return threadLocalInstance.get();
}
}
多线程执行类
public class ThreadLocalExectorThread implements Runnable {
@Override
public void run() {
ThreadLocalSingleton threadLocalSingleton = ThreadLocalSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + threadLocalSingleton);
}
}
测试类
public class ThreadLocalSingletonTest {
public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
Thread t1 = new Thread(new ThreadLocalExectorThread());
Thread t2 = new Thread(new ThreadLocalExectorThread());
t1.start();
t2.start();
}
}
输出结果
我们发现,在主线程 main 中无论调用多少次,获取到的实例都是同一个,都在两个子线程中分别获取到了不同的实例.那么 ThreadLocal 是如果实现这样的效果的呢?我们知道上面的单例模式为了达到线程安全的目的,给方法上锁,以时间换空间.ThreadLocal 将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的