zoukankan      html  css  js  c++  java
  • 单例模式的演进

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

  • 相关阅读:
    比较器 Comparable 与compartor 的区别及理解
    事务特性、事务隔离级别、spring事务传播特性
    分布式文件上传-FastDFS
    spring-cloud 组件总结以及搭建图示 (六)
    springCloud zuul网关(五)
    hashCode与equals 通过面试题一窥究竟
    【原】那年30岁
    【原】Hyper-V虚拟机设置外部网络访问
    【原】win10 .net framework 3.5安装
    【原】做梦有感
  • 原文地址:https://www.cnblogs.com/tf-Y/p/10248872.html
Copyright © 2011-2022 走看看