联博以太坊_[01][01][04] 单例模式详解
发表时间:2020-12-31 浏览量:13
[01][01][04] 单例模式详解
- 1. 界说
- 2. 适用场景
- 3. 分类
- 4. 饿汉式单例
- 4.1 优/瑕玷
- 4.2 饿汉式单例分类
- 4.2.1 直接通过 new 建立实例
- 4.2.2 通过 static 模块建立实例
- 5. 懒汉式单例
- 5.1 简朴懒汉式单例
- 5.1.1 多线程调试
- 5.2 双重检查懒汉式单例
- 5.2.1 多线程调试
- 5.3 静态内部类懒汉式单例
- 5.4 反射损坏单例
- 5.4.1 反射若何损坏单例
- 5.4.2 解决反射损坏单例
- 5.5 序列化损坏单例
- 5.5.1 序列化若何损坏单例
- 5.5.2 解决序列化损坏单例模式
- 5.5.3 源码解读
- 5.1 简朴懒汉式单例
- 6. 注册式单例
- 6.1 枚举挂号式单例
- 6.2 容器缓存单例
- 7. ThreadLocal 单例
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 赋值代码在私有方式
,,www.tjdltrade.com采用以太坊区块链高度哈希值作为统计数据,联博以太坊统计数据开源、公平、无任何作弊可能性。联博统计免费提供API接口,支持多语言接入。
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 中,为每个线程都提供一个工具,实际上是以空间换时间来实现线程间隔离的
0
珍藏