单例模式的演进

本部分介绍单例模式,从懒汉式单例讲起,介绍懒汉式模式遇到的各种问题:多线程、指令重排序,及其在解决问题的基础上的各种演进。之后介绍饿汉式单例,饿汉式单例相对简单,但是失去了延迟加载的优势。还会介绍序列化、反射等对单例模式的破坏与预防;并引申出相对完美的枚举单例。还扩展介绍了容器单例,以及ThreadLocal单例。

一. 概述

1. 定义:保证一个类仅有一个实例,并提供一个全局访问点

2. 类型:创建型

3. 适用场景:确保任何情况下都绝对只有一个实例,如:应用配置、线程池、数据库连接池。

4. 优缺点:

  4.1 优点:在内存里只有一个实例,减少内存开销;避免对资源的多重占用

  4.2 缺点:没有接口,扩展困难

5. 重点:私有构造器、线程安全、延迟加载、指令重排序、序列化和反序列化安全、反射攻击

6. 实用技能:反编译、内存原理、多线程Debug

7. 相关设计模式:工厂模式(一般把工厂类设计为单例模式的)、享元模式(通过享元模式和单例模式的结合,完成单例对象的获取,这种情况下享元模式类似于单例模式的工厂)

二、懒汉式单例:多线程Debug、指令重排序

1. 懒汉式:懒汉式可以理解为这种方式比较懒,有拖延症,其实为延迟加载,实例的创建要到必须的时候才进行。与之相对应的是饿汉式,这种方式比较积极,在类加载的时候就创建实例。这里先介绍懒汉式。

2. 线程不安全的懒汉式单例及其逐步优化

我们对单例模式逐步演化,从最基本的开始:

 1 public class LazySingleton {
 2     private static LazySingleton lazySingleton = null; // 空的实例
 3     private LazySingleton() {};  // 私有构造器
 4     public static LazySingleton getInstance() {  // 公有方法获取实例
 5         if (lazySingleton == null) {
 6             lazySingleton = new LazySingleton();
 7         }
 8         return lazySingleton;
 9     }
10 }

这种懒汉式单例模式,在多线程的情况下是不安全的。考虑这么一种情况,有两个线程0和1,当线程0运行到第6行创建实例但还没赋值时切换到线程1,线程1运行到第5行判断lazySingleton为空,继续运行,这样就创建了两个实例。虽然最后赋值给lazySingleton的是一个单例,但在多个线程的情况下却会创建多个单例,如果单例占用内存较多,则很有可能造成系统故障。下面用多线程Debug的方式模拟两个线程的情况。

多线程安全问题创建线程类和测试类,代码如下:

 1 // 线程类
 2 public class T implements Runnable{
 3 
 4     @Override
 5     public void run() {
 6         LazySingleton lazySingleton = LazySingleton.getInstance();
 7         System.out.println(Thread.currentThread().getName() + " " + lazySingleton);
 8     }
 9 
10 }
// 测试类
public class Test {
    public static void main(String[] args) {
        Thread t0 = new Thread(new T());
        Thread t1 = new Thread(new T());
        t0.start();
        t1.start();
        
        System.out.println("The end...");    
    }
}

上述代码直接运行的话,输出的结果是一样的,但是当我们Debug就会发现创建了多个实例对象。

多线程debug:

1)首先在线程类的run()第6行打个断点(注意:断点的位置一定要正确,run方法或者run以后调用的方法里,否则的话,程序跑完了,debug模式里也只有一个主线程在跑),然后点击debug,程序运行会停在断点位置,观察debug一栏,会看到多个线程。如下图

上图展现了断点的位置和多个线程。

2)切换某个线程时,适用鼠标点击就可以。在此首先执行线程1,进入getInstance方法,执行到单例类的第6行,此时还未给lazySingleton赋值,该变量仍然为null。如下图所示:

3)这时,鼠标点击线程0,切换到0线程,同样执行到单例类第6行,由于lazySingleton为null,所以可以通过if。如下图所示:

4)切换回线程1,执行到单例类第8行,这时可以看到lazySingleton已经赋值,值为:26,如下图所示:

5)再切换回线程0,执行同样的执行到单例类第8行,这时可以看到,lazySingleton的值已经改变为:31,如下图所示。

6)执行程序到最后输出结果如下图所示:

上述过程使用了多线程debug的技能,输出结果一样,这是因为后一个线程重新赋值了,并且是在重新赋值后进行的return,但是创建了两个单例对象。若在第5步不切换回线程0,而是直接让线程1运行结束,再切换回线程0,让其运行结束,那么输出的将是两个不同的结果。

对于上述线程不安全的懒汉式单例模式,采用加sychronized关键字的方式改进:

1     public synchronized static LazySingleton getInstance2() {  // synchronized锁静态类
2         if (lazySingleton == null) {
3             lazySingleton = new LazySingleton();
4         }
5         return lazySingleton;
6     }

synchroized锁静态类使方法变成同步方法,注意synchroized加在静态方法上锁的是类的class文件,synchroized加在非静态方法上锁的是堆中的对象。上述代码还有另一种写法:

1     public static LazySingleton getInstance3() {  
2         synchronized (LazySingleton.class) { // 锁类
3             if (lazySingleton == null) {
4                 lazySingleton = new LazySingleton();
5             }
6         }
7         return lazySingleton;
8     }

上述两种代码效果一样,都是锁class。因为锁的范围过大, 所以会影响性能。下面有更优化的方式:兼顾性能、线程安全,同时是懒加载的。

双重检查式懒汉

 1 public class LazyDoubleCheckSingleton {
 2     private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; // 空的实例
 3     private LazyDoubleCheckSingleton() {};  // 私有构造器
 4     
 5     public static LazyDoubleCheckSingleton getInstance() {  // 公有方法获取实例
 6         if (lazyDoubleCheckSingleton == null) {   // 检查1
 7             synchronized (LazyDoubleCheckSingleton.class) { // 锁类
 8                 if (lazyDoubleCheckSingleton == null) {  // 检查2
 9                     lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
10                 }
11             }
12         }
13         return lazyDoubleCheckSingleton;
14     }
15 }

上述代码看似安全高效,在第一次创建单例对象后,不需要再次进入sychronized。但想象不到的安全隐患却潜藏于第6行和第9行,这涉及到另一个知识点:指令重排序。在第9行看似一个步骤,其实涉及到对象的创建过程,主要有三个步骤:1. 分配内存,2. 初始化对象,3. 指针指向内存地址。这三个步骤中2 3的顺序是可以改变的,即顺序可以为123或132。当初始化顺序为132时,若执行到3,切换另一个线程,该线程执行到第6行,lazyDoubleCheckSingleton不为空,然后第13行返回,因为此时为执行初始化步骤2,则返回的实例对象未初始化,所以会影响接下来的进程。

为了消除指令重排序造成的影响,可以采取禁止指令重排序指令重排序对其他线程不可见的方式。

1)禁止指令重排序可以使用volatile关键字,详情点击链接。上述代码也就改成了

 1 public class LazyDoubleCheckSingleton {
 2     private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; // 加了 volatile
 3     private LazyDoubleCheckSingleton() {};  // 私有构造器
 4     
 5     public static LazyDoubleCheckSingleton getInstance() {  // 公有方法获取实例
 6         if (lazyDoubleCheckSingleton == null) {   // 检查1
 7             synchronized (LazyDoubleCheckSingleton.class) { // 锁类
 8                 if (lazyDoubleCheckSingleton == null) {  // 检查2
 9                     lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
10                 }
11             }
12         }
13         return lazyDoubleCheckSingleton;
14     }
15 }

 2)防止其他线程看到指令重排序的方式可以采用静态内部类的方式,代码如下:

 1 public class StaticInnerClassSingleton {
 2     private StaticInnerClassSingleton() {}; // 私有构造方法
 3     
 4     private static class InnerClass{
 5         private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
 6     }
 7     
 8     public static StaticInnerClassSingleton getInstance() {
 9         return InnerClass.staticInnerClassSingleton;
10     }
11 }

JVM在类的初始化阶段,执行类的初始化时,JVM会获取一个锁,这个锁会同步多个线程对一个类的初始化。上述特性可以实现基于静态内部类的、线程安全的、延迟初始化方案。这样当一个线程执行类的初始化时,其他线程会被锁在外面。

触发类初始化的情况有以下5种:1. 有一个A类型的实例被创建;2. A类中声明的静态方法被调用;3. A类中的静态成员被赋值;4. A类中的静态成员被使用,且该成员不是常量成员;5. A类是顶级类,且该类中有嵌套的断言语句。

假设线程0获取到StaticInnerClassSingleton 对象的初始化锁,这时线程0执行该静态内部类的初始化。这时即使初始化步骤2. 初始化对象,3. 指针指向内存地址,之间存在重排序,但是线程1也是无法看到的。所以这里的关键就在于InnerClass的初始化锁被哪个线程拿到,哪个线程就执行初始化。

总结:对于初始的懒汉式单例,由于存在多线程不安全的情况,所以需要加sycnrhnized关键字;但该关键字会降低效率,所以出现了双重检查机制;对于双重检查机制,存在指令重排序的问题,为防止指令重排序使用了volatile关键字、或使指令重排序对其他线程不可见使用了静态内部类。

在上述叙述中,问题用红色字体标出,解决方案用绿色字体标出。

 二、饿汉式单例:

1. 饿汉式:在类加载的时候,就完成实例化。

1 public class HungrySingleton {
2     private final static HungrySingleton HUNGRY_SINGLETON = new HungrySingleton(); // 类加载时就初始化
3     private HungrySingleton() {} // 私有构造方法
4     
5     public static HungrySingleton getInstance() {
6         return HUNGRY_SINGLETON;
7     }
8 }

上述为饿汉式的基本模式,优点为:写法简单、类加载时就完成初始化避免了线程同步问题。缺点是没有延迟加载的效果,单例类一般比较大,如果这个类从始至终没有被用过,会造成内存的浪费。总体来说,饿汉式是最简单的,如果资源浪费少的话,这种模式非常方便。上述代码还有另外一种实现方式:

 1 public class HungrySingleton {
 2     private final static HungrySingleton HUNGRY_SINGLETON; 
 3     static {
 4         HUNGRY_SINGLETON = new HungrySingleton(); // 类加载时就初始化
 5     }
 6     
 7     private HungrySingleton() {} // 私有构造方法
 8     
 9     public static HungrySingleton getInstance() {
10         return HUNGRY_SINGLETON;
11     }
12 }

三、序列化破坏单例解决方案与 原理分析

    可以思考这样一个问题:当把单例对象序列化到一个文件中,然后再把它反序列化出来,这样生成的对象和原来的对象还是同一个吗?

    下面使用饿汉式测试,测试前饿汉单例类先实现Serializable接口,然后编写测试类如下:

 1 public class Test {
 2     public static void main(String[] args) throws Exception {
 3         // 序列化写
 4         HungrySingleton instance = HungrySingleton.getInstance();
 5         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
 6         oos.writeObject(instance);   
 7         
 8         // 序列化读
 9         File file = new File("singleton_file");
10         ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); 
11         HungrySingleton newInstance = (HungrySingleton)ois.readObject(); 
12         
13         // 比较
14         System.out.println(instance);
15         System.out.println(newInstance);
16         System.out.println(instance == newInstance);
17     }
18 }

运行测试类输出发现,instance和newInstance并不一样,序列化破坏了单例模式生成了不同的对象。为了解决上述问题并理解其原理,需要探究ObjectInputStream.readObject()的源码。在readObject()方法中调用了readObject0(),在readObject0()方法中的switch语句中调用了readOrdinaryObject方法()。下面是该方法的源码,关键处已注释。

 1 private Object readOrdinaryObject(boolean unshared)
 2         throws IOException
 3     {
 4         if (bin.readByte() != TC_OBJECT) {
 5             throw new InternalError();
 6         }
 7 
 8         ObjectStreamClass desc = readClassDesc(false);
 9         desc.checkDeserialize();
10 
11         Class<?> cl = desc.forClass();
12         if (cl == String.class || cl == Class.class
13                 || cl == ObjectStreamClass.class) {
14             throw new InvalidClassException("invalid class descriptor");
15         }
16 
17         Object obj;
18         try {
19             obj = desc.isInstantiable() ? desc.newInstance() : null; // 反射创建对象
20         } catch (Exception ex) {
21             throw (IOException) new InvalidClassException(
22                 desc.forClass().getName(),
23                 "unable to create instance").initCause(ex);
24         }
25 
26         passHandle = handles.assign(unshared ? unsharedMarker : obj);
27         ClassNotFoundException resolveEx = desc.getResolveException();
28         if (resolveEx != null) {
29             handles.markException(passHandle, resolveEx);
30         }
31 
32         if (desc.isExternalizable()) {
33             readExternalData((Externalizable) obj, desc);
34         } else {
35             readSerialData(obj, desc);
36         }
37 
38         handles.finish(passHandle);
39 
40         if (obj != null &&
41             handles.lookupException(passHandle) == null &&
42             desc.hasReadResolveMethod())  // 判断是否有readResolve方法
43         {
44             Object rep = desc.invokeReadResolve(obj); // 反射调用readResolve方法
45             if (unshared && rep.getClass().isArray()) {
46                 rep = cloneArray(rep);
47             }
48             if (rep != obj) {
49                 // Filter the replacement object
50                 if (rep != null) {
51                     if (rep.getClass().isArray()) {
52                         filterCheck(rep.getClass(), Array.getLength(rep));
53                     } else {
54                         filterCheck(rep.getClass(), -1);
55                     }
56                 }
57                 handles.setObject(passHandle, obj = rep);
58             }
59         }
60 
61         return obj;
62     }

在第19行,通过反射创建单例对象,此时反射创建的单例对象与getInstance()获得的对象不同,所以测试类中输出false。为使其相同,我们继续往下看。最后返回的是obj,在第57行有将rep赋值给obj的操作。为满足其条件,首先看到第42行,点进去看源码判断是否有readResolve()方法,在第44行反射调用readResolve()方法将其结果赋值给rep。因此我们在此处这样改写单例类:

 1 public class HungrySingleton implements Serializable{
 2     private final static HungrySingleton HUNGRY_SINGLETON; 
 3     static {
 4         HUNGRY_SINGLETON = new HungrySingleton(); // 类加载时就初始化
 5     }
 6     
 7     private HungrySingleton() {} // 私有构造方法
 8     
 9     public static HungrySingleton getInstance() {
10         return HUNGRY_SINGLETON;
11     }
12     
13     // readResolve方法
14     private Object readResolve() {
15         return HUNGRY_SINGLETON;
16     }
17 }

再次运行测试类,两个对象比较,返回ture。为更详细的了解,可以debug看其具体执行过程。同时注意,在上述序列化和反序列化的过程中,已经实例化对象了,只是没有返回。

 四、反射攻击解决方案及 原理分析

同样用简单的饿汉模式进行演示,反射攻击的测试类如下:

 1 public class Test {
 2     
 3     public static void main(String[] args) throws Exception {
 4         Class objCla = HungrySingleton.class;
 5         Constructor constructor = objCla.getDeclaredConstructor();
 6         constructor.setAccessible(true); // 把权限置为ture,放开权限
 7         
 8         HungrySingleton instance = HungrySingleton.getInstance();
 9         HungrySingleton newInstance = (HungrySingleton)constructor.newInstance();
10         
11         System.out.println(instance);
12         System.out.println(newInstance);
13         System.out.println(instance == newInstance);
14     }
15 }

上述代码输出结果为false,对于饿汉式单例,由于它在类加载时就已经生成了对象,因此我们可以通过改动构造方法来防止在类加载后再次创建对象。具体代码如下:

public class HungrySingleton implements Serializable{
    private final static HungrySingleton HUNGRY_SINGLETON; 
    static {
        HUNGRY_SINGLETON = new HungrySingleton(); // 类加载时就初始化
    }
    
    private HungrySingleton() { // 私有构造方法
        if (HUNGRY_SINGLETON != null) { // 抛出异常禁止反射调用
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    } 
    
    public static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON;
    }
}    

运行后发现,反射调用构造方法时会抛出异常。但是上述方式仅适用于静态内部类和饿汉式的方式。

对于不是在类加载时就创建单例的情况,不可以使用上述方式。

五、枚举单例、原理源码及反编译

 对于枚举单例,将主要关注它在序列化和反射攻击中的表现。枚举单例代码如下:

 1 public enum EnumInstance {
 2     INSTANCE;
 3     private Object data; // 测试的主要为枚举类持有的对象data
 4 
 5     public Object getData() {
 6         return data;
 7     }
 8 
 9     public void setData(Object data) {
10         this.data = data;
11     }
12     
13     public static EnumInstance getInstance() {
14         return INSTANCE;
15     }
16 }

1)序列化测试类的代码如下:

 1 public class Test {
 2     public static void main(String[] args) throws Exception {
 3         EnumInstance instance = EnumInstance.getInstance();
 4         instance.setData(new Object());
 5         
 6         // 枚举单例类测试序列化
 7         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
 8         oos.writeObject(instance);  
 9         File file = new File("singleton_file");
10         ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); 
11         EnumInstance newInstance = (EnumInstance)ois.readObject(); 
12         
13         System.out.println(instance.getData());
14         System.out.println(newInstance.getData());
15         System.out.println(instance.getData() == newInstance.getData());
16     }
17 }

运行测试类后,两个instance输出结果一样。接下来通过源码了解枚举不受序列化影响的原因:打开ObjectInputStream.readObject()的源码。在readObject()方法中调用了readObject0(),在readObject0()方法中的switch语句中调用了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); // 获取枚举常量,因为枚举中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;
    }

2)反射的测试类代码如下:

 1 public class Test {
 2     public static void main(String[] args) throws Exception {
 3         Class objCla = EnumInstance.class;
 4         Constructor constructor = objCla.getDeclaredConstructor(String.class, int.class); // 枚举类的构造方法带有两个参数
 5         constructor.setAccessible(true); // 把权限置为ture,放开权限
 6         
 7         EnumInstance instance = EnumInstance.getInstance();
 8         EnumInstance newInstance = (EnumInstance)constructor.newInstance("haha", 666);
 9         
10         System.out.println(instance);
11         System.out.println(newInstance);
12         System.out.println(instance == newInstance);
13     }
14 }

上述代码为枚举反射的测试代码,运行上述代码可以发现运行不到10行,因为在第8行会报错:“java.lang.IllegalArgumentException: Cannot reflectively create enum objects”。具体的可以在运行时打开Constructor类查看报错处的源码。

3)枚举类本身的优势

了解枚举类本身,要把它反编译。这里反编译枚举类使用的时JAD,可以到这里下载。下载完成后,解压,配置环境变量即可使用,具体可百度。这里对EnumInstance类进行反编译,命令为:

jad EnumInstance.class

反编译结果的后缀名为.jad,此处使用Notepad++打开反编译后的结果如下,关键处已注释:

 1 public final class EnumInstance extends Enum // final --> 类不能被继承
 2 {
 3 
 4     private EnumInstance(String s, int i)  // 私有构造器
 5     {
 6         super(s, i);
 7     }
 8 
 9     public Object getData()
10     {
11         return data;
12     }
13 
14     public void setData(Object data)
15     {
16         this.data = data;
17     }
18 
19     public static EnumInstance getInstance()
20     {
21         return INSTANCE;
22     }
23 
24     public static EnumInstance[] values()
25     {
26         EnumInstance aenuminstance[];
27         int i;
28         EnumInstance aenuminstance1[];
29         System.arraycopy(aenuminstance = ENUM$VALUES, 0, aenuminstance1 = new EnumInstance[i = aenuminstance.length], 0, i);
30         return aenuminstance1;
31     }
32 
33     public static EnumInstance valueOf(String s)
34     {
35         return (EnumInstance)Enum.valueOf(pattern/creational/singletion/EnumInstance, s);
36     }
37 
38     public static final EnumInstance INSTANCE;   // 静态的类变量
39     private Object data;
40     private static final EnumInstance ENUM$VALUES[];
41 
42     static    // 静态块加载
43     {
44         INSTANCE = new EnumInstance("INSTANCE", 0);
45         ENUM$VALUES = (new EnumInstance[] {
46             INSTANCE
47         });
48     }
49 }

从反编译结果能看出,枚举类的构造器是私有的,并且类变量是static final,且在静态块中加载。并且有序列化和反射方面的优势,所以枚举类在创建单例对象上具备原生优势。

六、基于容器的单例模式

 基于容器的单例模式,类似于享元模式。代码如下:

 1 public class ContainerSingleton {
 2     private ContainerSingleton() {}
 3     
 4     private static Map<String, Object> singletonMap = new HashMap<String, Object>(); // 用map存储单例
 5                                                                                      // hashmap不是线程安全的
 6     public static void putInstance(String key, Object instance) {
 7         if (key != null && key.length() != 0 && instance != null) {
 8             if (!singletonMap.containsKey(key)) {
 9                 singletonMap.put(key, instance);
10             }
11         }
12     }
13     
14     public static Object getInstance(String key) {
15         return singletonMap.get(key);
16     }
17 }

此处使用map存储单例,HashMap不是线程安全的,但是在类加载时直接加载HashMap这样用也可以,但要考虑具体情况。考虑下数情况,有两个线程,线程0先put进kv,然后get数据;线程2再put进kv,然后get数据。这事两个线程使用同样的key不同的value那么获得的结果是不一样的;若线程0先put,然后线程1put,然后再get,那么获得的结果是一样的。

若将HashMap改成HashTable会变成线程安全的,但是会影响性能;若是改成ConcurrentHashMap,在此场景中,使用了静态的ConcurrentHashMap并且直接操作了map,ConcurrentHashMap并不是绝对的线程安全。综上,不考虑反射、序列化等情况,容器单例模式也是有一定适用场景。

容器可以统一管理单例对象,节省资源,但线程并不安全。

 七、ThreadLocal线程单例(可保证线程唯一,不能保证全局唯一)

使用ThreadLocal类创建在线程内唯一的单例,代码如下:

 1 public class ThreadLocalInstance {
 2     private static final ThreadLocal<ThreadLocalInstance> THREAD_LOCAL 
 3         = new ThreadLocal<ThreadLocalInstance>() { // 匿名内部类
 4         protected ThreadLocalInstance initialValue() {  // 重写方法
 5             return new ThreadLocalInstance();
 6         }
 7     };
 8     
 9     private ThreadLocalInstance() {}
10     
11     public static ThreadLocalInstance getInstance() {
12         return THREAD_LOCAL.get();
13     } 
14 }

修改T类如下:

1 public class T implements Runnable{
2 
3     @Override
4     public void run() {
5         ThreadLocalInstance instance = ThreadLocalInstance.getInstance();
6         System.out.println(Thread.currentThread().getName() + " " + instance);
7     }
8 
9 }

测试:

 1 public class Test {
 2     public static void main(String[] args) {
 3         ThreadLocalInstance instance = ThreadLocalInstance.getInstance();
 4         System.out.println(instance);
 5         System.out.println(instance);
 6         System.out.println(instance);
 7         System.out.println(instance);
 8         System.out.println(instance);
 9         
10         Thread t0 = new Thread(new T());
11         Thread t1 = new Thread(new T());
12         t0.start();
13         t1.start();
14         
15         System.out.println("The end...");    
16     }
17 }

运行上述代码可以发现main线程中的输出结果是一样的,main,t0,t1的输出结果各不相同。ThreadLocal隔离了多个线程对资源的访问冲突,对于多线程资源共享的问题,使用同步锁是时间换空间的,使用ThreadLocal是空间换时间。

八、源码中的应用

1)java.lang.Runtime类,属于饿汉式单例。

2)java.awt.Desktop类的getDesktop属于容器单例,但是加了各种sychronized进行同步控制。

posted @ 2019-01-10 11:24  木易·月  阅读(369)  评论(0编辑  收藏  举报