JAVA设计模式(6:单例模式详解)

单例模式作为一种**创建型模式**,在日常开发中用处极广,我们先来看一一段代码: ```java // 构造函数 protected Calendar(TimeZone var1, Locale var2) { this.lenient = true; this.sharedZone = false; this.nextStamp = 2; this.serialVersionOnStream = 1; this.fields = new int[17]; this.isSet = new boolean[17]; this.stamp = new int[17]; this.zone = var1; this.setWeekCountData(var2); } // 提供 Calendar 类实例的方法 public static Calendar getInstance(){ return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT)); } ``` 看过上一篇博客Java设计模式(5:工厂模式详解)的朋友应该熟悉这段来自`JDK`中`Calendar`类的代码,这就是单例模式的一种实现: 1. `Calendar`类的构造函数被`protected`修饰,保证其不能被其他包下的类访问。 2. `getInstance()`方法提供了获得`Calendar`类实例化对象的方法。 从上述代码来看,我们可以认定实现单例模式需要满足两个基本原则: 1. 类的**构造函数**私有化。 2. 该类需要**提供一个获得实例的全局访问点**。 所以可以得出结论:单例模式是指**确保一个类在任何情况下都绝对只有一个实例,并提供一个全局的访问点。** 得出结论后,再来看看实现。在`java`语言当中,两种方式构建单例模式:**饿汉式单例**和**懒汉式单例**。 ### 一、饿汉式单例 ```java // 饿汉式单例 public class HungrySingleton { // 构造函数私有化 private HungrySingleton() {} private static final HungrySingleton singleton = new HungrySingleton(); // 提供一个全局的访问点 public static HungrySingleton getInstance(){ return singleton; } } ``` 饿汉式单例是在**类加载的时候就立即初始化,并且创建了单例对象**。在上述代码中,当`HungrySingleton`类在被类加载器加载时,它的实例对象`singleton`就已经创建完成了;并且根据类的加载机制,我们明白:`singleton`作为`HungrySingleton`类中的一个静态的声明对象,在`HungrySingleton`类第一次被类加载器加载时就已经创建完成,并且只会创建这一次。这就保证了无论`getInstance()`方法被调用多少次,返回的都是同一个`singleton`实例;保证了线程的绝对安全,不会出现访问安全的问题。 但也正式因为`singleton`实例在`HungrySingleton`类第一次被类加载器加载时就已经创建完成,若`getInstance()`方法不被任何地方调用,那么`singleton`实例就会一直占着内存空间,白白浪费了资源。所以引申出了另一种构建单例模式的方式:**懒汉式单例** ### 二、懒汉式单例 懒汉式单例的特点是**只有在类的全局访问点被访问的时候,类的实例化对象才会创建**。 ```java // 懒汉式单例 public class LazySingleton { // 构造函数私有化 private LazySingleton() {} private static LazySingleton lazySingleton = null; // 全局访问点 public static LazySingleton getInstance(){ if (lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; } } ``` 在上述代码中,只有当`getInstance()`方法被调用时,才会去创建`lazySingleton`实例。这样就解决了**饿汉式模式**中的资源占用问题,但同样引申出了另一个问题:**线程安全问题**。 我们先来创建一个属于我们自己的线程类`LazyThread`: ```java // 线程 public class LazyThread implements Runnable { @Override public void run() { LazySingleton instance = LazySingleton.getInstance(); // 打印 线程名字 和 instance实例的内存地址 System.out.println(Thread.currentThread().getName() + ":" +instance); } } ``` 调用: ```java // 创建两个线程 public static void main(String[] args) { Thread thread1 = new Thread(new LazyThread()); Thread thread2 = new Thread(new LazyThread()); thread1.start(); thread2.start(); } ``` 我们采用debug模式调试一下,**先和下图一般,在`LazySingleton`类中打一个断点**。 ![image20210624102816663.png](https://s2.51cto.com/images/20210625/1624582922118119.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) **再用鼠标右键点击断点的位置($\color{#FF0000}{红色圆点的位置}$),打开如下图的框之后,先选择红框中的`Thread`模式,再点击蓝框中的`Done`按钮。** ![image20210624103111658.png](https://s2.51cto.com/images/20210625/1624582949107684.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 做完上述的操作之后,我们来用**debug**模式运行一下**main**方法 ![image20210624103832500.png](https://s2.51cto.com/images/20210625/1624582999398833.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 上图红框中内容就是我们所创建的两个线程,目前是`Thread-0`线程在运行。我们将`Thread-0`线程运行到`lazySingleton = new LazySingleton()`这行代码的位置($\color{#FF0000}{图1}$),然后切换为`Thread-1`线程,并将`Thread-1`线程同样运行到此位置($\color{#FF0000}{图2}$): 图1: ![image20210624104448611.png](https://s2.51cto.com/images/20210625/1624583030520610.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 图2: ![image20210624104518577.png](https://s2.51cto.com/images/20210625/1624583050150473.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 最后:切换回`Thread-0`线程,并全部放开,让代码一直运行下去;并对`Thread-1`做出同样的操作。打印出结果: ![image20210624105027443.png](https://s2.51cto.com/images/20210625/1624583081167590.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 通过结果可以看出,两个线程获得的`lazySingleton`实例所对应的内存地址不相同,显然不符合单例模式中的**只有一个实例**的原则。 那有什么办法可以保证**懒汉式模式**在线程环境下安全呢?有,而且很简单,加锁。我们来给`getInstance()`方法加上锁: ```java // 懒汉式 public class LazySingleton { // 私有化构造函数 private LazySingleton() {} private static LazySingleton lazySingleton = null; // 加锁 public synchronized static LazySingleton getInstance(){ if (lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; } } ``` 我们再用上述的方式来**debug**调试一下: ![image20210624110350785.png](https://s2.51cto.com/images/20210625/1624583112801801.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 在线程`Thread-1`进入`getInstance()`方法内部的时候,线程`Thread-0`处于`MONITOR`锁监控的状态。将线程`Thread-1`运行完后,`Thread-0`进入`getInstance()`方法内部,状态更新为`RUNNING`运行状态。 ![image20210624110740849.png](https://s2.51cto.com/images/20210625/1624583147346709.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 而此时我们可以看出`lazySingleton`已经有值了,所以我们将线程`Thread-0`运行完后,两个线程会打印出一样的结果: ![image20210624110931419.png](https://s2.51cto.com/images/20210625/1624583193126929.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 由结果我们可以看出,在给`getInstance()`方法加上锁之后,线程安全的问题便解决了。但依然可以继续来优化这段**懒汉式单例模式**的代码。 ```java // 懒汉式 public class LazySingleton { // 私有化构造函数 private LazySingleton() {} // volatile 关键字 解决重排序的问题 private volatile static LazySingleton lazySingleton = null; public static LazySingleton getInstance(){ if (lazySingleton == null){ // 锁代码块 synchronized (LazySingleton.class) { if (lazySingleton == null){ lazySingleton = new LazySingleton(); } } } return lazySingleton; } } ``` 这种方式被称为**双重检查锁**,它有着以下两点的好处: 1. 线程由基于`LazySingleton`整个类的阻塞变为在`getInstance()`方法内部的阻塞。锁的颗粒度变得更细,锁的代码块变得更小了。 2. 第一重的`if`判断,直接分流了一部分在`lazySingleton`实例化后在进入`getInstance()`方法的线程,提高了效率。 但是,只要涉及到加锁的问题,对程序的性能或多或少都有影响,那么有没有不加锁的方式呢?当然也是有的,那就是以类的初始化角度来考虑,使用内部类的方式。 ### 三、静态内部类实现单例模式 ```java // 懒汉式模式 和 饿汉式模式 兼顾 public class InnerClassSingleton { // 私有化构造函数 private InnerClassSingleton(){} public static InnerClassSingleton getInstance(){ return SingletonHolder.singleton; } // 静态内部类 private static class SingletonHolder{ private static final InnerClassSingleton singleton = new InnerClassSingleton(); } } ``` 这种方式兼顾了**懒汉式模式**和**饿汉式模式**,根据类的加载机制来说,静态内部类`SingletonHolder`不会随着外部类`InnerClassSingleton`的加载而加载,只会在被调用时才会加载。 这里外部类`InnerClassSingleton`在被类加载器加载后,并不会去进一步加载`SingletonHolder`类,从而也不会去实例化`singleton`,也就避免了资源浪费的情况。而在`getInstance()`方法第一次被调用时,内部类`SingletonHolder`才会加载,`SingletonHolder`类中声明的静态对象`singleton`才会被实例化;后面每一次调用`getInstance()`方法时,返回的都是此`singleton`对象,保证了**只有一个实例化对象**的原则。 ### 四、用反射的方式来破坏单例 讲完单例模式的几种实现方式之后,我们来讲一讲破坏单例的方式;虽然日常开发中不会怎么用到,但对面试来说,可以说是一个必考点。多了解了解,总会有意想不到的用处。 ```java public static void main(String[] args) { try { // 用反射获得 InnerClassSingleton 类的实例 Class clazz = InnerClassSingleton.class; Constructor constructor = clazz.getDeclaredConstructor(null); // 强制访问 constructor.setAccessible(true); InnerClassSingleton instance1 = (InnerClassSingleton)constructor.newInstance(); // 单例模式获取 InnerClassSingleton instance2 = InnerClassSingleton.getInstance(); System.out.println("利用反射得到的实例对象:"+instance1); System.out.println("单例模式的实例对象:"+instance2); }catch (Exception e){ e.printStackTrace(); } } ``` 上述的测试代码,我分别用**反射**的方式和**单例**的方式来获得`InnerClassSingleton`类的实例,最后打印出来,看一看结果: ![image20210624141829954.png](https://s2.51cto.com/images/20210625/1624583268264390.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 可以看出,两次创建的`InnerClassSingleton`类的实例又不相同了。那怎么杜绝这种办法呢?我们可以来优化一下上述的静态内部类的代码: ```java // 懒汉式模式 和 饿汉式模式 兼顾 public class InnerClassSingleton { // 私有化构造函数 private InnerClassSingleton(){ if (SingletonHolder.singleton != null){ throw new RuntimeException("不能以这种方式来获得实例对象......"); } } public static InnerClassSingleton getInstance(){ return SingletonHolder.singleton; } // 静态内部类 private static class SingletonHolder{ private static final InnerClassSingleton singleton = new InnerClassSingleton(); } } ``` 主要看**私有构造函数**中的代码,我们将这里做了限制,当被外界调用时,直接抛出异常!测试的结果也如我们所愿: ![image20210624142422447.png](https://s2.51cto.com/images/20210625/1624583303871890.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) ### 五、用序列化的方式破坏单例 除了反射之外,用序列化的方式也能破坏单例,达到创建不一样的类的实例的效果。 先将`InnerClassSingleton`类实现序列化接口: ```java // 懒汉式模式 和 饿汉式模式 兼顾 public class InnerClassSingleton implements Serializable { // ....... 中间的代码查看上面的代码 } ``` 编写测试代码: ```java public static void main(String[] args) { try { InnerClassSingleton instance1 = InnerClassSingleton.getInstance(); FileOutputStream fos = new FileOutputStream("singleton.obj"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fos); objectOutputStream.writeObject(instance1); objectOutputStream.flush(); objectOutputStream.close(); fos.close(); FileInputStream fis = new FileInputStream("singleton.obj"); ObjectInputStream objectInputStream = new ObjectInputStream(fis); InnerClassSingleton instance2 = (InnerClassSingleton)objectInputStream.readObject(); objectInputStream.close(); fis.close(); System.out.println("利用单例获得实例:"+instance1); System.out.println("利用序列化获取的实例:"+instance2); }catch (Exception e){ e.printStackTrace(); } } ``` 在上面的代码中,我们先获得`InnerClassSingleton`类的实例`instance1`,再将`instance1`写入`singleton.obj`文件当中;然后再从中取出来,转化为实例`instance2`;最后将`instance1`和`instance2`打印出来: ![image20210624143908874.png](https://s2.51cto.com/images/20210625/1624583330363069.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 可以看出,两次创建的`InnerClassSingleton`类的实例又不相同了。那么这种方式的解决方案是什么呢?也不难,只需要加上一个方法就好了: ```java public class InnerClassSingleton implements Serializable { // ....... 代码省略 // 加上 readResolve() 方法 private Object readResolve(){ return SingletonHolder.singleton; } // 静态内部类 private static class SingletonHolder{ private static final InnerClassSingleton singleton = new InnerClassSingleton(); } } ``` 再加上`readResolve()`之后,再来测试一下: ![image20210624145929658.png](https://s2.51cto.com/images/20210625/1624583368373247.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 可以看出,两次创建的实例完全相同,完美的解决了序列化的问题。那么为什么加上`readResolve()`就会解决这个问题呢?这里和`JDK`的源码有关,我这里就不贴源码了,不便于观看,我这里画了一个时序图,大家可以跟着这个时序图来对照`JDK`源码进行查看,了解内情。 1、先从编写的测试代码里面进入`ObjectInputStream`类中的`readObject()`方法 ![image20210624150733739.png](https://s2.51cto.com/images/20210625/1624583422317543.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 2、实序图 ![readResolve.png](https://s2.51cto.com/images/20210625/1624584209366280.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 以实序图来看,其实方法内部还是创建了一次`InnerClassSingleton`类的实例,不过是后面用调用`readResolve()`方法获得的`InnerClassSingleton`类的实例将它替换掉了,所以打印出的结果依旧是相同的。总体来说,还是白白消耗了内存,那么再来看另一种创建单例的方式。 ### 六、注册式单例 注册式单例又被称为**登记式单例**,大体分为**枚举登记**和**容器缓存**两种。 #### 6.1 枚举登记 ```java public enum EnumSingleton { INSTANCE; // 用来测试对象是否相同 private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumSingleton getInstance(){ return INSTANCE; } } ``` ##### 6.1.1 序列化破坏 将上面的测试代码稍微更改一下: ```java public static void main(String[] args) { try { EnumSingleton instance1 = EnumSingleton.getInstance(); instance1.setData(new Object()); // ....... 查看 五、用序列化的方式破坏单例 的测试代码 EnumSingleton instance2 = (EnumSingleton)objectInputStream.readObject(); objectInputStream.close(); fis.close(); System.out.println("利用单例获得实例:"+instance1.getData()); System.out.println("利用序列化获取的实例:"+instance2.getData()); }catch (Exception e){ e.printStackTrace(); } } ``` 结果: ![image20210624160430744.png](https://s2.51cto.com/images/20210625/1624583480512396.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 由结果可以看出是可行的,那么原理是什么呢?通过上述实序图的方式查看源码: 1、`ObjectInputStream`类中的`readObject0()`方法: ```java private Object readObject0(boolean unshared) throws IOException { // ...... 省略代码 // 如果是枚举类 case TC_ENUM: return checkResolve(readEnum(unshared)); // ...... } ``` 2、`readEnum()`方法 ```java private Enum readEnum(boolean unshared) throws IOException { // ...... if (cl != null) { try { // 通过Class对象 c1 和 类名 name 来获得唯一的枚举对象 @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); } } // ...... } ``` 通过查看源码发现,枚举类型其实通过Class 对象类和类名找到一个唯一的枚举对象;因此,枚举对象不可能被类加载器加载多次。 ##### 6.1.2 反射破坏 测试代码: ```java public static void main(String[] args) { try { Class clazz = EnumSingleton.class; Constructor constructor = clazz.getDeclaredConstructor(null); // 强制访问 constructor.setAccessible(true); EnumSingleton instance1 = (EnumSingleton)constructor.newInstance(); EnumSingleton instance2 = EnumSingleton.getInstance(); System.out.println("利用反射得到的实例对象:"+instance1); System.out.println("单例模式的实例对象:"+instance2); }catch (Exception e){ e.printStackTrace(); } } ``` 结果: ![image20210624162535995.png](https://s2.51cto.com/images/20210625/1624583543792128.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 它竟然报出`java.lang.NoSuchMethodException`,意思是没有找到对应的无参的构造函数,这是为什么呢?不急,让我们将`EnumSingleton.class`这个文件反编译一下($\color{#FF0000}{这里使用的是jad反编译工具,不会的同学去网上搜教程,这里不详细讲解了}$),得到一个`EnumSingleton.jad`文件,打开文件后发现这么一段代码: ```java // ..... private EnumSingleton(String s, int i){ super(s, i); } // ..... static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); } ``` 原来`jvm`在编译`EnumSingleton`枚举类时,给它创建了一个有参的构造函数,并再静态代码块里面实例化了`INSTANCE`对象。那这里,我们再将测试代码修改一下,强制传入两个参数会怎么样: ```java public static void main(String[] args) { try { Class clazz = EnumSingleton.class; // 设置两个参数的类型 Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class); // 强制访问 constructor.setAccessible(true); // 传入两个参数 EnumSingleton instance1 = (EnumSingleton)constructor.newInstance("test",111); EnumSingleton instance2 = EnumSingleton.getInstance(); System.out.println("利用反射得到的实例对象:"+instance1); System.out.println("单例模式的实例对象:"+instance2); }catch (Exception e){ e.printStackTrace(); } } ``` 结果: ![image20210624164245873.png](https://s2.51cto.com/images/20210625/1624583602856590.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 还是报错,不过这次的错误换成了`Cannot reflectively create enum objects`,不允许创建枚举类的对象。我们来看看`JDK`的源码: ![image20210624164418863.png](https://s2.51cto.com/images/20210625/1624583640696378.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 从`constructor.newInstance("test",111)`这行代码进入`Constructor`类中的`newInstance()`方法我们发现,这里有个判断,如果是对枚举类进行操作,那么直接报出错误;这么看来,是`JDK`源码帮助我们去拦截了来自反射技术的破坏,那么就可以放宽心了。 #### 6.2 容器缓存 容器缓存最经典的例子就是`Spring`框架中的`IOC`容器,我们来模拟一下: ```java // 容器缓存 public class ContainerSingleton { // 私有化构造函数 private ContainerSingleton(){} private static Map iocMap = new ConcurrentHashMap<>(); // 传入 类名参数 public static Object getBean(String className){ if (className == null || "".equals(className)){ return null; } synchronized (iocMap){ // 判断容器中是否有该属性 if (!iocMap.containsKey(className)){ Object object = null; try { object = Class.forName(className).newInstance(); iocMap.put(className,object); }catch (Exception e){ e.printStackTrace(); } return object; } else { return iocMap.get(className); } } } } ``` `iocMap`中的`key`存的是类名,`value`存的是该类的实例化对象,通过这种方式来保证每次获得的都是一个类的相同实例。 ### 七、ThreadLocal线程单例 以`ThreadLocal`方式创建的单例对象是最为特殊的,因为它是一个伪单例,它只能保证同一个线程内创建的类的实例是相同的,有着天生的线程安全;但不能保证创建的类的实例是全局唯一的;先来看看代码: ```java public class ThreadLocalSingleton { public ThreadLocalSingleton() {} private static final ThreadLocal threadLocal = new ThreadLocal(){ @Override protected Object initialValue() { return new ThreadLocalSingleton(); } }; public static ThreadLocalSingleton getInstance(){ return threadLocal.get(); } } ``` 线程代码: ```java public class LazyThread implements Runnable { @Override public void run() { ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance(); System.out.println(Thread.currentThread().getName() + ":" +instance); } } ``` 测试代码: ```java public static void main(String[] args) { System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance()); System.out.println("————————————————————————————————————————"); Thread thread1 = new Thread(new LazyThread()); Thread thread2 = new Thread(new LazyThread()); thread1.start(); thread2.start(); } ``` 结果: ![image20210624171030320.png](https://s2.51cto.com/images/20210625/1624583692126504.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 从结果可以看出,再主线程`main`中,无论我们调用多少次`getInstance()`方法,获得的`ThreadLocalSingleton`的实例都是相同的。而两个子线程`Thread-0`和`Thread-1`都获得了不同的实例。那么这是怎么做到了呢? 通过查看源码($\color{#FF0000}{别问我为啥不贴源码,问就是看不到,它的底层不是用Java写的【流泪】,感兴趣的小伙伴可以百度,有大神,我也是百度的,yyds!!!}$)我们发现,`ThreadLocal`将`ThreadLocalSingleton`类的实例对象全部放在了`ThreadLocalMap`中,为每一个线程提供了对象,实际上是以空间换时间来实现线程隔离的。这也使`ThreadLocal`技术频繁的使用了于用户登陆时,存储用户的登录信息方面。甚至于`Mybatis`中多个数据源切换的技术也是用它实现的。 ### 最后 --- 如果这篇文章对你有帮助的话,麻烦动动小手,点个赞,万分感谢!!! 如果有小伙伴发现文章里面有错误,欢迎来指正,不胜感激!!!
优秀的个人博客,低调大师

微信关注我们

原文链接:https://blog.51cto.com/caimm/2946568

转载内容版权归作者及来源网站所有!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

相关文章

发表评论

资源下载

更多资源
优质分享Android(本站安卓app)

优质分享Android(本站安卓app)

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Apache Tomcat7、8、9(Java Web服务器)

Apache Tomcat7、8、9(Java Web服务器)

Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。

Eclipse(集成开发环境)

Eclipse(集成开发环境)

Eclipse 是一个开放源代码的、基于Java的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。幸运的是,Eclipse 附带了一个标准的插件集,包括Java开发工具(Java Development Kit,JDK)。

Sublime Text 一个代码编辑器

Sublime Text 一个代码编辑器

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。