设计模式 | 二、单例模式(懒汉式、饿汉式、注册式、线程单例)[SingletonPattern]

单例模式

源码请参考:https://github.com/GiraffePeng/design-patterns

1、单例模式的应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。例如,windows的回收站、任务管理器等。在 J2EE 标准中,ServletContext、ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接池也都是单例形式。

2、饿汉式

饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线程还没出现以前就已经实例化,不可能存在访问安全问题。

  • 优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
  • 缺点:类加载的时候就初始化,不管用与不用都占着堆内存空间。

Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例。
示例:


/**
 * 饿汉式
 */
public class SingletonHungry {
	//私有化构造函数,使其不能从外部进行实例化。
    private SingletonHungry(){}
	//静态的不可修改的单例实例
    private static final SingletonHungry singletonHungry = new SingletonHungry();
    //全局的静态的访问点,返回实例
    public static SingletonHungry newInstance(){
        return singletonHungry;
    }
}

或者使用静态代码块来实现饿汉式单例

/**
 * 饿汉式
 */
public class SingletonHungry {
	//私有化构造函数,使其不能从外部进行实例化。
    private SingletonHungry(){}
	//静态的不可修改的单例实例
    private static final SingletonHungry singletonHungry;
    //使用静态代码块来实现单例的实例化
    static{
        singletonHungry = new SingletonHungry();
    }
    //全局的静态的访问点,返回实例
    public static SingletonHungry newInstance(){
        return singletonHungry;
    }
}

3、懒汉式

懒汉式单例的特点是:被外部应用类首次调用全局访问接口的时候内部类才会加载,这样对比饿汉式来说,只有被使用时才会占用堆内存空间。
示例:

/**
 * 懒汉式
 */
public class SingletonLazy{

    //声明一个实例但不初始化
    private static SingletonLazy singletonLazy = null;

    //私有化构造函数,防止外部应用类去再次实例化该类
    private SingletonLazy(){}
    //声明一个全局的静态的获取单例的方法
    public static SingletonLazy newInstance(){
        //判断该实例是否为null,如果为null进行创建,用于首次调用该实例
        if(singletonLazy == null){
            singletonLazy = new SingletonLazy();
        }
        return singletonLazy;
    }
}

但是这种懒汉式的单例实现,对于多线程来说不能够保证线程安全,可能在多线程的运行环境下,造成多次实例化了该单例类。
接下来进行验证,首先创建线程类

//实现Runnable接口
public class TheradTest implements Runnable{

	//重写其run方法
	@Override
	public void run() {
		//调用单例的全局访问点
		SingletonLazy newInstance = SingletonLazy.newInstance();
		//打印该类的className以及哈希值
		System.out.println(newInstance);
	}
}

创建测试类

public class LazyTest {

	public static void main(String[] args) {
		//创建线程
		Thread thread = new Thread(new TheradTest());
		
		Thread thread1 = new Thread(new TheradTest());
		
		//开始线程 跑啊跑
		thread.start();
		thread1.start();
	}
}

执行main方法后控制台打印结果如下:

com.peng.lazysingleton.SingletonLazy@7c0acd01
com.peng.lazysingleton.SingletonLazy@1af6fbe7

可以看到两个类的哈希值不同,代表两个类的内存地址不同,单例被创建了两个实例对象。

3.1 线程不安全原因:

让我们开启多线程的debug调试模式(eclipse,IDEA也有相应的debug多线程调试模式,这里不介绍),可以看到线程0和线程1都可以进入到if的判断语句内,从而都调用了实例化的方法导致创建了两个实例。
在这里插入图片描述
在这里插入图片描述

3.2 线程不安全问题的解决

针对线程不安全问题,我们可以通过关键字synchronized来修饰代码,从而解决线程不安全问题。修改懒汉单例类,加入synchronized来修饰method方法。

/**
 * 懒汉式
 */
public class SingletonLazy{

    //声明一个实例但不初始化
    private static SingletonLazy singletonLazy = null;

    //私有化构造函数,防止外部应用类去再次实例化该类
    private SingletonLazy(){}
    //声明一个全局的静态的获取单例的方法,使用synchronized来修饰方法,达到线程安全的目的
    public synchronized static SingletonLazy newInstance(){
        //判断该实例是否为null,如果为null进行创建,用于首次调用该实例
        if(singletonLazy == null){
            singletonLazy = new SingletonLazy();
        }
        return singletonLazy;
    }
}

接下来我们继续调用测试类的main方法来测试线程不安全问题是否解决,开启debug模式。
在这里插入图片描述
可以看到当Thread0进入修饰后的方法时,会有一个钥匙的标志,此时如果Thread1也要进入newInstance方法,会出现Stepping状态,意思为等待其他线程释放资源。
在这里插入图片描述
当我们把Thread0执行完毕时,可以看到Thread1也进入了方法,这时因为被Thread0已经实例化了,这里if判断该实例不为null,则不进行再次实例化该对象。
这时控制台打印的结果为:

com.peng.lazysingleton.SingletonLazy@b4882ae
com.peng.lazysingleton.SingletonLazy@b4882ae

这里看似已经解决了线程不安全问题,但是因为synchronized修饰的为方法,但不是内部的代码块,在so many的线程下,会引起无数次的争取锁资源,释放锁资源从而引起堵塞(synchronized机制问题,这里不展开),我们可以再来优化下我们的代码。

/**
 * 懒汉式
 */
public class SingletonLazy{

    //声明一个实例但不初始化
    private static SingletonLazy singletonLazy = null;

    //私有化构造函数,防止外部应用类去再次实例化该类
    private SingletonLazy(){}
    
    public static SingletonLazy newInstance(){
        //外层判断是为了减少进入synchronized的线程,基本进入外层判断内部的会有首次调用的线程,之后的都会在这步进行阻拦
        if(singletonLazy == null){
        	synchronized (SingletonLazy.class) {
        		//内层判断是为了 多线程下 单例类还没实例化时,外层判断可能会导致有多个线程进入内部逻辑,为了防止多次实例化而加入内层判断
        		if(singletonLazy == null) {
        			singletonLazy = new SingletonLazy();
        		}
			}
        }
        return singletonLazy;
    }
}

这里使用了两层判断来保证线程安全,至于为什么是两层,上述注释中有提及。这种实现方式就是所谓的【双重锁懒汉式单例】。
但是,用到 synchronized 关键字,总归是要上锁,对程序性能还是存在一定影响的。但为了解决懒汉式的线程安全问题,没有更好的方案吗?当然是有的。我们可以从类初始化角度来考虑,看下面的代码,采用静态内部类的方式:

//这种形式兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题
//完美地屏蔽了这两个缺点
public class SingletonLazyInner {
	
	private SingletonLazyInner() {};
	
	//每一个关键字都不是多余的
	//static 是为了使单例的空间共享
	//final 保证这个方法不会被重写,重载
	public static final SingletonLazyInner newInstance(){
		//在返回结果以前,一定会先加载内部类
		return SingletonInner.singletonLazyInner;
	}
	
	//默认不加载
	private static class SingletonInner{
		 static final SingletonLazyInner singletonLazyInner = new SingletonLazyInner();
	}
}

利用内部类的初始化逻辑,静态内部类默认状态下是不被加载的,当被使用SingletonLazyInner时才会被加载。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。

3.3 破坏单例的两种方式以及解决办法

上述代码看似很完美,其实不然,我们还是可以通过野路子来破坏上述的单例模式,野路子有两种:

  • 反射破坏单例
  • 序列化与反序列化破坏单例

反射破坏单例:
利用反射可以通过获取类的无参构造方法从而破坏单例,代码如下:

public class LazyTest {

	public static void main(String[] args) {
		Class<?> clazz = SingletonLazyInner.class;
		try {
			//获取该单例的构造方法
			Constructor c = clazz.getDeclaredConstructor(null);
			//将其私有化的方法进行转为能够访问
			c.setAccessible(true);
			Object object = c.newInstance();
			Object object1 = c.newInstance();
			System.out.println(object);
			System.out.println(object1);
		} catch (NoSuchMethodException | SecurityException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
	}
}

打印结果:

com.peng.lazysingleton.SingletonLazyInner@4fb64261
com.peng.lazysingleton.SingletonLazyInner@42607a4f

可以看到通过反射机制获取类的构造方法能够轻松的破坏单例,我们进行优化,在单例类中的构造方法上加入如下代码:

...
	private SingletonLazyInner() {
		//如果单例已经实例化了,则抛出异常不让你再次实例化
		if(SingletonInner.singletonLazyInner != null) {
			throw new RuntimeException("单例类不允许实例化多次");
		}
	};
...

再次执行main方法,结果如下:

java.lang.reflect.InvocationTargetException
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:488)
	at com.peng.lazysingleton.LazyTest.main(LazyTest.java:24)
Caused by: java.lang.RuntimeException: 单例类不允许实例化多次
	at com.peng.lazysingleton.SingletonLazyInner.<init>(SingletonLazyInner.java:10)
	... 5 more

序列化与反序列化破坏单例:
利用对单例类的序列化输出到磁盘上,然后通过反序列化重新生成单例类如果不做特殊处理,也会生成两个单例实例。

public class LazyTest {

	@SuppressWarnings("resource")
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		SingletonLazyInner newInstance = SingletonLazyInner.newInstance();
		try {
			//序列化
			FileOutputStream fos = new FileOutputStream("singleton.obj");
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(newInstance);
			oos.flush();
			oos.close();
			fos.close();
			//反序列化
			FileInputStream fis = new FileInputStream("singleton.obj");
			ObjectInputStream ois = new ObjectInputStream(fis);
			SingletonLazyInner singletonLazyInner = (SingletonLazyInner)ois.readObject();
			System.out.println(singletonLazyInner);
			System.out.println(newInstance);
			ois.close();
			fis.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
	}
}

打印结果如下:

com.peng.lazysingleton.SingletonLazyInner@233c0b17
com.peng.lazysingleton.SingletonLazyInner@5cbc508c

解决办法:在单例类中加入readResolve方法

...
	public Object readResolve() {
		return SingletonInner.singletonLazyInner;
	}
...

再来测试,控制台打印结果如下:

com.peng.lazysingleton.SingletonLazyInner@5cbc508c
com.peng.lazysingleton.SingletonLazyInner@5cbc508c

可以看到通过序列化和反序列化也能破坏单例,为什么反序列化的过程中能够破坏单例呢,又为何加入readResolve方法就能够防止单例的破坏呢,我们进行查看源码:
我 们 进 入ObjectInputStream 类的 readObject()方法,代码如下

public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        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();
                freeze();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

我们发现在readObject中又调用了readObject0()方法。进入readObject0()方法,关键代码如下:

private Object readObject0(boolean unshared) throws IOException {
	...
		case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));
	...
}

我们看到 TC_OBJECTD 中判断,调用了 ObjectInputStream 的 readOrdinaryObject()方法,我们继续进入看源码

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }
		//这里 调用了 desc.isInstantiable方法,该方法是判断该反序列化的类是否有构造方法,
		//如果有,则进行newInstance()从而创建新的实例。
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);
		//这里调用desc.hasReadResolveMethod()方法,该方法是判断 被反序列化的类是否拥有readResolve方法,
		//如果有则调用desc.invokeReadResolve(obj)方法来获取反序列化的类,而不使用newInstasnce方法新实例化的类。
        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);
            }
        }
        return obj;
    }

通过上述源码我们知道反序列化的逻辑以及如何防止破坏单例的用法。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,下面我们可以用注册式单例来解决这个问题。

4、注册式单例

注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。先来看枚举式单例的写法,来看代码,创建 EnumSingleton 类:

public enum EnumSingleton {

	SINGLETON;
	
	private Object data;

	public Object getData() {
		return data;
	}

	public void setData(Object data) {
		this.data = data;
	}
	
	public static EnumSingleton getInstance(){
		return EnumSingleton.SINGLETON;
	}
}

创建测试类:

public class TestRegister {

	public static void main(String[] args) {
		EnumSingleton instance = EnumSingleton.getInstance();
		instance.setData(new Object());
		System.out.println(instance.getData());
		EnumSingleton instance2 = EnumSingleton.getInstance();
		Object data = instance2.getData();
		System.out.println(data);
	}
}

打印结果:

java.lang.Object@383534aa
java.lang.Object@383534aa

我们来验证通过序列化与反序列能否破坏枚举的单例,测试代码进行调整:

public static void main(String[] args) throws Exception{
		EnumSingleton instance = EnumSingleton.getInstance();
		instance.setData(new Object());
		System.out.println(instance.getData());
		
		FileOutputStream fos = new FileOutputStream("enums.obj");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(instance);
		oos.flush();
		oos.close();
		fos.close();
		//反序列化
		FileInputStream fis = new FileInputStream("enums.obj");
		ObjectInputStream ois = new ObjectInputStream(fis);
		EnumSingleton singletonLazyInner = (EnumSingleton)ois.readObject();
		System.out.println(singletonLazyInner.getData());
		ois.close();
		fis.close();
	}

打印结果:

java.lang.Object@383534aa
java.lang.Object@383534aa

我们通过反编译生成在磁盘上的enums.obj文件,打开反编辑文件可以看到其中有一段这样的代码:

static
{
	//枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例的实现。
	INSTANCE = new EnumSingleton("INSTANCE", 0);
	$VALUES = (new EnumSingleton[] {
		INSTANCE
	});
}

那么通过反序列化不能够破坏枚举式单例,为什么会这样,我们继续查看readObject的源码:

...
private Object readObject0(boolean unshared) throws IOException {
	....
		 case TC_ENUM:
                    return checkResolve(readEnum(unshared));
	....
}
...

我们继续查看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")
                //通过枚举的valueOf方法,cl为反序列化的枚举Class,name为枚举的常量从而得到反序列化的枚举类
                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;
    }

通过源码我们知道,反序列化的枚举类并没有被类加载器加载多次,返回的还是序列化时的对象,所以针对枚举式单例不存在序列化与反序列破坏单例的情况。

那么通过反射能否破坏枚举式单例?我们进行验证,测试代码如下:

public class TestRegister {

	public static void main(String[] args) throws Exception{
		EnumSingleton instance = EnumSingleton.getInstance();
		instance.setData(new Object());
		System.out.println(instance.getData());
		
		Class<EnumSingleton> clazz = EnumSingleton.class;
		Constructor constructor =  clazz.getDeclaredConstructor(null);
		constructor.setAccessible(true);
		EnumSingleton enumSingleton = (EnumSingleton)constructor.newInstance(null);
		
		System.out.println(enumSingleton.getData());
	}
}

执行后控制台打印结果:

java.lang.Object@383534aa
Exception in thread "main" java.lang.NoSuchMethodException: com.peng.register.EnumSingleton.<init>()
	at java.base/java.lang.Class.getConstructor0(Class.java:3302)
	at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2512)
	at com.peng.register.TestRegister.main(TestRegister.java:36)

说明枚举类没有无参的构造方法,我们继续查看反编译生成的枚举类,可以看到枚举的构造方法有String以及int的参数

protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}

我们通过修改测试类,代码如下:

public static void main(String[] args) throws Exception{
		EnumSingleton instance = EnumSingleton.getInstance();
		instance.setData(new Object());
		System.out.println(instance.getData());
		
		Class<EnumSingleton> clazz = EnumSingleton.class;
		Constructor constructor =  clazz.getDeclaredConstructor(String.class,int.class);
		constructor.setAccessible(true);
		EnumSingleton enumSingleton = (EnumSingleton)constructor.newInstance("test",2);
		
		System.out.println(enumSingleton.getData());
	}

再次执行main方法,得到如下结果:

java.lang.Object@383534aa
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:482)
	at com.peng.register.TestRegister.main(TestRegister.java:38)

Cannot reflectively create enum objects(无法通过反射创建枚举对象)
我们打开Constructor.newInstance方法,可以看到其中有如下的判断逻辑:

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, clazz, modifiers);
        }
        //判断通过反射得到的对象是否是枚举类型,如果为枚举类型,抛出异常
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

枚举型的单例通过JDK的底层已经帮助我们解决了反射破坏单例以及序列化、反序列化破坏单例的问题,所以枚举式单例是一种比较好的写法。

注册式单例还有一种写法,容器缓存的写法,创建ContainerSingleton类:

public class ContainerSingleton {
	
	//利用map,将对象进行缓存起来,下次使用时如果map中存在,则从map中获取
	private static Map<String,Object> containerMap = new ConcurrentHashMap<String,Object>();

	private ContainerSingleton() {};
	
	@SuppressWarnings("deprecation")
	public static Object getBean(String className) {
		if(!containerMap.containsKey(className)) {
			try {
				Object newInstance = Class.forName(className).newInstance();
				containerMap.put(className, newInstance);
			} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
				e.printStackTrace();
			}
		}
		return containerMap.get(className);
	}
}

下面创建测试类:

public class TestContainerSingleton {

	public static void main(String[] args) {
		Object bean = ContainerSingleton.getBean("com.peng.register.TestRegister");
		Object bean1 = ContainerSingleton.getBean("com.peng.register.TestRegister");
		System.out.println(bean);
		System.out.println(bean1);
	}
}

打印结果:

com.peng.register.TestRegister@6bc168e5
com.peng.register.TestRegister@6bc168e5

但上述的写法也同样存在线程不安全的问题,我们进行改造,加入synchronized关键字来修饰代码块

@SuppressWarnings("deprecation")
	public static Object getBean(String className) {
		
		if(!containerMap.containsKey(className)) {
			synchronized (containerMap) {
				if(!containerMap.containsKey(className)) {
					try {
						Object newInstance = Class.forName(className).newInstance();
						containerMap.put(className, newInstance);
					} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
						e.printStackTrace();
					}
				}
			}
		}
		return containerMap.get(className);
	}

有点类似于双重锁的写法,容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的,如果要线程安全,需加入同步代码块的操作。

5、线程单例

线程单例的实现 ThreadLocal。ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。创建ThreadLocalSingleton类

public class ThreadLocalSingleton {

	private static ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>() {
		//重写其的初始化方法,将实例化的ThreadLocalSingleton放入到threadLocal中
		@Override
		protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
	};
	
	private ThreadLocalSingleton() {};
	
	public static ThreadLocalSingleton getInstance() {
		return threadLocal.get();
	}
}

为了验证其在同一个线程下为单例的,创建线程类

public class ExectorThread implements Runnable{

	@Override
	public void run() {
		ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
		System.out.println(Thread.currentThread().getName()+instance);
		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.currentThread().getName()+ThreadLocalSingleton.getInstance());
		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.currentThread().getName()+ThreadLocalSingleton.getInstance());
	}

}

测试类:

public class TestThreadLocal {
	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 ExectorThread());
		Thread t2 = new Thread(new ExectorThread());
		t1.start();
		t2.start();
	}
}

控制台打印结果:

com.peng.threadlocal.ThreadLocalSingleton@6bc168e5
com.peng.threadlocal.ThreadLocalSingleton@6bc168e5
com.peng.threadlocal.ThreadLocalSingleton@6bc168e5
com.peng.threadlocal.ThreadLocalSingleton@6bc168e5
com.peng.threadlocal.ThreadLocalSingleton@6bc168e5
Thread-1com.peng.threadlocal.ThreadLocalSingleton@5fbe2c9
Thread-0com.peng.threadlocal.ThreadLocalSingleton@3d5c24
Thread-1com.peng.threadlocal.ThreadLocalSingleton@5fbe2c9
Thread-0com.peng.threadlocal.ThreadLocalSingleton@3d5c24
Thread-1com.peng.threadlocal.ThreadLocalSingleton@5fbe2c9
Thread-0com.peng.threadlocal.ThreadLocalSingleton@3d5c24
Thread-1com.peng.threadlocal.ThreadLocalSingleton@5fbe2c9
Thread-0com.peng.threadlocal.ThreadLocalSingleton@3d5c24
Thread-1com.peng.threadlocal.ThreadLocalSingleton@5fbe2c9
Thread-0com.peng.threadlocal.ThreadLocalSingleton@3d5c24
Thread-1com.peng.threadlocal.ThreadLocalSingleton@5fbe2c9
Thread-0com.peng.threadlocal.ThreadLocalSingleton@3d5c24
Thread-1com.peng.threadlocal.ThreadLocalSingleton@5fbe2c9
Thread-0com.peng.threadlocal.ThreadLocalSingleton@3d5c24
Thread-1com.peng.threadlocal.ThreadLocalSingleton@5fbe2c9
Thread-0com.peng.threadlocal.ThreadLocalSingleton@3d5c24
Thread-1com.peng.threadlocal.ThreadLocalSingleton@5fbe2c9
Thread-0com.peng.threadlocal.ThreadLocalSingleton@3d5c24

可以看到在同一个线程下时,其ThreadLocal中的类为单例状态,那么 ThreadLocal 是如果实现这样的效果的呢?我们知道上面的单例模式为了达到线程安全的目的,给方法上锁,以时间换空间。ThreadLocal将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的

6、总结

单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用.
应用场景:

  • 在应用场景中,某类只要求生成一个对象的时候
  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
  • 当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
发布了21 篇原创文章 · 获赞 2 · 访问量 7499

猜你喜欢

转载自blog.csdn.net/qq_35551089/article/details/100079407