快速导航
单例模式的介绍
一. 1.1 懒汉模式(线程不安全)
1.2 懒汉模式(线程安全)
二、DoubleCheck 模式(懒汉式)(线程安全)
三、静态内部类创建方式(懒汉式)
四、饿汉式单例模式(线程安全)
五、序列化和反序列化破坏单例模式及解决方式
六、通过反射破坏单例模式及解决方式
七、枚举类单例模式(推荐用法,既能解决序列化、反序列化,也能解决 反射攻击)
八、容器单例模式
九、基于 ThreadLocal 的“单例模式”
单例模式的介绍
定义:保证一个类只有一个实例,并提供一个全局访问点。
类型:创建型。
优点:
在内存中只有一个实例,减少了内存开销(尤其是创建和销毁比较麻烦的时候)
可以避免对资源的多重占用
设置全局访问点,严格限制访问
缺点:
没有接口,扩展困难
重点:
私有构造器
线程安全(非常重要)
延迟加载
序列化和反序列化安全(破坏单例模式)
反射(防止反射攻击)
对比:
单例模式--工厂模式
单例模式--享元模式
一.懒汉模式
1.1 懒汉模式Demo(线程不安全):
/**
* @program: adpn-pattern->LazySingleton
* @description: 懒汉式
* @author: Jstar
* @create: 2019-11-22 09:01
**/
public class LazySingleton {
private static LazySingleton lazySingleton =null;
//私有构造器
private LazySingleton(){}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
/**
* @program: adpn-pattern->LazyThread
* @description: 多线程
* @author: Jstar
* @create: 2019-11-22 09:04
**/
public class LazyThread extends Thread {
@Override
public void run() {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName()+" "+lazySingleton);
}
}
/**
* @program: adpn-pattern->Test
* @description: 测试类
* @author: Jstar
* @create: 2019-11-22 09:08
**/
public class Test {
public static void main(String[] args) {
LazyThread lazyThread1 = new LazyThread();
LazyThread lazyThread2 = new LazyThread();
lazyThread1.start();
lazyThread2.start();
System.out.println("test over");
}
}
执行结果:

常规模式下,可能不容易看出执行结果是否存在线程安全,我们用多线程的Debug方式来看一下,
首先我们让两个线程都进入 单例模式:

这时Thread0 获取的 lazySingleton 是 552,

这时Thread1 获取的 lazySingleton 是 564,

从Control ,看两个线程获取的 singleton ,也不是一个实例。

可见,这种模式下,获取的 单例模式是非线程安全的。
1.2 下面我们来改造成线程安全的 懒汉模式:

我们只在 获取单例模式的getInstance()方法上,添加了 synchronized 关键字,添加后,因方法上有 static 关键字,所以这种同步方法锁住的是整个类。
然后我们再来Debug一下:

我们可以看出,当一个线程获取锁以后,另一个线程是 处于监视 Monitor 状态。
当Thread0 释放锁后,Thread1获取锁并执行程序。

因Thread0已经创建实例,所以Thread1,直接取 lazySingleton。

最后Control 两个线程返回的实例是同一个。

二、DoubleCheck 模式 (懒汉式)
上代码,其实主要是在单例模式上做了双重检验:
/**
* @program: adpn-pattern->DoubleCheckSingleton
* @description: doublecheck单例模式
* @author: Jstar
* @create: 2019-11-22 11:42
**/
public class DoubleCheckSingleton {
// volatile 防止 DoubleCheckSingleton 创建的时候 重排序
private static volatile DoubleCheckSingleton doubleCheckSingleton=null;
private DoubleCheckSingleton(){ }
public static DoubleCheckSingleton getInstance(){
if(doubleCheckSingleton==null){
synchronized (DoubleCheckSingleton.class){
if(doubleCheckSingleton==null){
/** 这里的new 操作执行了以下3个步骤:
* 1、分配内存给这个对象
* 2、初始化对象
* 3、设置 doubleCheckSingleton 指向 新分配的内存地址
* 存在的问题:
* 有可能会有重排序 的 现象,创建顺序就变成了 1/3/2
**/
doubleCheckSingleton = new DoubleCheckSingleton();
}
}
}
return doubleCheckSingleton;
}
}
有的小伙伴可能会问, doubleCheckSingleton 为什么要设置成 volatile 类型,因为创建对象有可能存在重排序的问题,上述代码已提示,多线程情况下,有可能因为重排序而导致程序报错。

所以在 doubleCheckSingleton 加上volatile 使 多线程情况下,禁止 创建 doubleCheckSingleton 时候重排序。volatile 修饰的变量会存入系统/共享内存,会使其它cpu缓存该变量的地址无效,无效后,其它cpu缓存又会从共享内存同步数据,多线程情况下都是可见的,
// 多线程模式创建 DoubleCheckSingleton
public class DoubleCheckThread extends Thread {
@Override
public void run() {
DoubleCheckSingleton doubleCheckSingleton = DoubleCheckSingleton.getInstance();
System.out.println(doubleCheckSingleton);
}
}
//测试类
public class DoubleCheckTest {
public static void main(String[] args) {
DoubleCheckThread doubleCheckThread1 = new DoubleCheckThread();
DoubleCheckThread doubleCheckThread2 = new DoubleCheckThread();
doubleCheckThread1.start();
doubleCheckThread2.start();
System.out.println("test over");
}
}
现在我们让两个线程同时进入, if(doubleCheckSingleton==null){} 内,

当Thread1 释放锁后,Thread0 获取锁:

我们来看执行结果:

结果:两个线程获取的是同一个对象,DoubleCheck 此模式是线程安全的。
三、静态内部类创建方式(懒汉式)
上述的DoubleCheck 方式主要是使用,控制创建类时,禁止重排序,来实现创建单例模式时,实现的线程安全。
下面我们来用另一种方式,来解决重排序的问题,思想是:即使有重排序问题,但是我们使 会产生重排序的类,对其它线程不可见。
下面我们来coding,来演示:
/**
* @program: adpn-pattern->StaticInnerClassSingleton
* @description: 单例静态内部类
* @author: Jstar
* @create: 2019-11-23 09:59
**/
public class StaticInnerClassSingleton {
// 防止被 new,构造器必须设成 私有属性
private StaticInnerClassSingleton(){}
/**
**静态内部类:jvm在创建静态内部类的时候,会给类加锁,这样其它线程就无法看到静态内部类 创建过程
* 在本例中:在创建 InnerClass 时,InnerClass 内 StaticInnerClassSingleton 的创建 不会被其它线程看到,
* 即使创建 StaticInnerClassSingleton 有重排序的问题,StaticInnerClassSingleton 对其它线程来说也是一个完整的过程
* 所以这种创建方式是线程安全的
**/
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
// 此处会创建静态内部类 InnerClass
return InnerClass.staticInnerClassSingleton;
//多线程获取单例对象
public class StaticInnerThread implements Runnable{
@Override
public void run() {
//
StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
System.out.println(Thread.currentThread().getName()+" "+instance);
}
}
//测试类
public class StaticInnerTest {
public static void main(String[] args) {
StaticInnerThread staticInnerThread = new StaticInnerThread();
Thread t1 = new Thread(staticInnerThread);
Thread t2 = new Thread(staticInnerThread);
t1.start();
t2.start();
}
}
如上述代码,StaticInnerClassSingleton 使用静态内部类实现线程安全,已在 StaticInnerClassSingleton 类中做说明,我们来看一下运行结果:

此种方式创建的是一个实例。
四、饿汉式单例模式(线程安全)
/**
* @program: adpn-pattern->HungrySinglon
* @description: 饿汉式单例模式
* @author: Jstar
* @create: 2019-11-23 11:12
**/
public class HungrySinglon {
private final static HungrySinglon hungrySinglon=new HungrySinglon();
private HungrySinglon(){ }
public HungrySinglon getInstance(){
return hungrySinglon;
}
}
说明:饿汉式单例模式在类初始化的时候就创建好了,所以在调用的时候是线程安全的。
经过测试,结果是同一个对象:

五、序列化和反序列化破坏单例模式及解决方式
本次测试我们使用了 静态内部类的创建方式测试:
/**
* @program: adpn-pattern->SerializeDestroyTest
* @description: 序列化和反序列化方式破坏单例模式
* @author: Jstar
* @create: 2019-11-23 11:41
**/
public class SerializeDestroyTest implements Serializable {
public static void main(String[] args) throws IOException, ClassNotFoundException {
StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("staticInner_singleton"));
// 将StaticInnerClassSingleton 写出去
outputStream.writeObject(instance);
File file=new File("staticInner_singleton");
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
// 将StaticInnerClassSingleton 重新读进来
StaticInnerClassSingleton newInstance = (StaticInnerClassSingleton) inputStream.readObject();
//看两个对象是否是同一个
System.out.println("原对象 instance:"+instance);
System.out.println("新对象 newInstance:"+newInstance);
}
}
来看测试结果:

可以看出,序列化前后两个类不是同一个。序列化和反序列化破坏了 单例类的问题。
我们来看一下,为什么,序列化破坏了单例模式,
按图示方向进入:





因为类 序列化实现了 serializable,所以 desc.isInstantiable() 返回的true

在这里 obj 被赋值的是 反射的一个新类,所以,序列化后单例模式 得到了两个不同的实例。
那我们有办法来解决这个问题吗,答案是有的:
我们只需在单例类加入readResolve()方法即可,如图:

加入readResolve()方法,我们来看执行结果:

两个线程获取的是同一个对象,神奇不神奇。那为什么 inputStream.readObject() 时,实现内 desc.newInstance() 新建了实例,返回的两个对象相同。
其实是因为,我们单例类加入了 readResolve()方法 ,被 inputStream.readObject() 实现内调用执行,虽然内部 newInstance() ,但最后返回的是 return InnerClass.staticInnerClassSingleton; 所以还是一个实例。
具体可看:


到这里就一目了然了,实现里 因单例类 实现了serializable,所以执行了 readResolve()方法,将 方法内的InnerClass.staticInnerClassSingleton 返回,还是原实例。
六、通过反射破坏单例模式及解决方式
6.1、首先 我们来用最近的饿汉模式来演示
/**
* @program: adpn-pattern->DeclareInstance
* @description: 通过反射破坏单例对象
* @author: Jstar
* @create: 2019-11-23 16:10
**/
public class DeclareInstance {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
HungrySinglon instance = HungrySinglon.getInstance();
Class<HungrySinglon> hungrySinglonClass = HungrySinglon.class;
Constructor c = hungrySinglonClass.getDeclaredConstructor();
c.setAccessible(true);
HungrySinglon newInstance= (HungrySinglon) c.newInstance();
System.out.println("正常获取单例对象:"+instance);
System.out.println("反射获取单例对象:"+ newInstance);
System.out.println(instance==newInstance);
}
}
结果: 不是同一个实例对象

那我们来解决:就是在HungrySinglon 的私有构造器中加条件判断,如果这个实例已经被创建了,不不允许调用私有构造器。
改造如下:
/**
* @program: adpn-pattern->HungrySinglon
* @description: 饿汉式单例模式
* @author: Jstar
* @create: 2019-11-23 11:12
**/
public class HungrySinglon implements Serializable {
private final static HungrySinglon hungrySinglon=new HungrySinglon();
private HungrySinglon(){
if(hungrySinglon!=null){
throw new RuntimeException("不允许使用私有构造器创建对象");
}
}
public static HungrySinglon getInstance(){
return hungrySinglon;
}
public Object readResolve(){
return hungrySinglon;
}
}
不论正常获取单例先执行还是后执行,都是 不允许使用私有构造器创建对象,因为类加载的时候,HungrySinglon 实例已经创建了。

6.2 匿名内部类方式的解决方式和 饿汉模式相同,因为在类初始化的时候已经创建了类实例,在这里就不演示了,读者可以自行尝试一下
6.3、懒汉模式
懒汉模式是无法解决反射攻击的,因为,类加载的时候,没有被实例化,不论在私有构造器加如何复杂的判断还是加全局属性作为 Flag 做标记判断,都是可以被反射获取并更改属性的,所以不存在解决的问题。如果在懒汉模式有解决 反射攻击的方法,你可以告诉小编呀,,
七、枚举类单例模式(推荐用法,既能解决序列化、反序列化,也能解决 反射攻击)
/**
* @program: adpn-pattern->EnumInstance
* @description: 枚举类单例模式
* @author: Jstar
* @create: 2019-11-23 17:22
**/
public enum EnumInstance {
INSTANCE;
public Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public EnumInstance getInstance(){
return INSTANCE;
}
}
写个序列化测试类:
/**
* @program: adpn-pattern->EnumTest
* @description: 枚举类单例测试
* @author: Jstar
* @create: 2019-11-23 17:39
**/
public class EnumTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//测试序列化和反序列化
EnumInstance instance = EnumInstance.INSTANCE;
instance.setData(new Object());
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("enum_singleton"));
// 将 EnumInstance 写出去
outputStream.writeObject(instance);
File file=new File("enum_singleton");
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
// 将 EnumInstance 重新读进来
EnumInstance newInstance = (EnumInstance) inputStream.readObject();
//看两个对象是否是同一个
System.out.println(" 原对象 instance:"+ instance);
System.out.println("新对象 newInstance:"+newInstance);
System.out.println(" 原对象 instance data:"+ instance.getData());
System.out.println("新对象 newInstance data:"+newInstance.getData());
}
}
来看执行结果:

序列化和反序列对 枚举单例模式没影响。
再来看反射对枚举单例模式的影响:
运行测试类时,报 NoSuchMethodException,execuse me ??

那我们反编译来看一下,生成的 EnumInstance.class,我在反编译的结果上加了注释:
package com.jstar.adpnpattern.design.factory.creation.singleton;
public final class EnumInstance extends Enum
{
public static EnumInstance[] values()
{
return (EnumInstance[])$VALUES.clone();
}
public static EnumInstance valueOf(String name)
{
return (EnumInstance)Enum.valueOf(com/jstar/adpnpattern/design/factory/creation/singleton/EnumInstance, name);
}
// 只有一个构造器,且是有参的,那我们待会反射的时候就传 two 个参数,嗯
private EnumInstance(String s, int i)
{
super(s, i);
}
public Object getData()
{
return data;
}
public void setData(Object data)
{
this.data = data;
}
public EnumInstance getInstance()
{
return INSTANCE;
}
// EnumInstance 已声明为 static final 类型
public static final EnumInstance INSTANCE;
public Object data;
private static final EnumInstance $VALUES[];
// 构造代码块,类初始化的时候即执行,类似饿汉式
static
{
INSTANCE = new EnumInstance("INSTANCE", 0);
$VALUES = (new EnumInstance[] {
INSTANCE
});
}
}
构造器传参,运行之后,返回 IllegalArgumentException ,小编是没办法了。
看来反射也没破坏枚举的单例模式。

另外,我们在 INSTANCE 中添加以下代码,反编译看class文件到底做了什么:

反编译后:

INSTANCE 中的testPrint() 在类初始化时即构建了。
八、容器单例模式
介绍:容器类单例模式就是把多种单例模式放到同一个容器中,统一管理。
下面我们用代码来演示:
/**
* @program: adpn-pattern->ContainerSingleton
* @description: 容器单例类
* @author: Jstar
* @create: 2019-11-23 20:36
**/
public class ContainerSingleton {
private ContainerSingleton(){ }
private static Map<String,Object> singletonMap=new HashMap<String,Object>();
public static void putInstance(String key,Object value){
if(StringUtils.isNotBlank(key) && value!=null){
if(!singletonMap.containsKey(key)){
singletonMap.put(key, value);
}
}
}
public static Object getInstance(String key){
return singletonMap.get(key);
}
}
/**
* @program: adpn-pattern->ContainerThread
* @description: 线程类
* @author: Jstar
* @create: 2019-11-23 20:42
**/
public class ContainerThread implements Runnable {
@Override
public void run() {
ContainerSingleton.putInstance("key",new Object());
Object obj= ContainerSingleton.getInstance("key");
System.out.println(Thread.currentThread().getName()+" "+obj);
}
}
/**
* @program: adpn-pattern->ContainerTest
* @description: 测试类
* @author: Jstar
* @create: 2019-11-23 20:45
**/
public class ContainerTest {
public static void main(String[] args) {
ContainerThread containerThread = new ContainerThread();
Thread t1 = new Thread(containerThread);
Thread t2 = new Thread(containerThread);
t1.start();
t2.start();
System.out.println("test over");
}
}
我们用Debug 模式来看一下,这种模式是否是线程安全的。
Thread0 现在地址值 是 553

切换到 Thread1,Thread1的地址值是 557

至少从目前来看,容器单例模式使用的不是一个对象。最后我们来看执行结果。

从执行结果来看,容器类单例模式确实不是线程安全的。那我们可以改造一下。
把 容器 HashMap 换成 Hashtable。 我们再按刚才的方式测试一下。
Thread0 :

Thread1:

我们来看最后的执行结果:



由此可见,此种方式,即使 Hashtable 的containsKey() 和 put() 方法都被 synchronized 修饰了,但在执行put()方法时,containsKey 此时的锁已经释放,导致 put 了两个对象。
所以,小编认为,容器类单例模式也是非线程安全的,我们要根据实际业务中的场景使用单例模式。
可以选择 HashMap、ConcurrentHashMap,不建议使用 Hashtable 。
九、基于 ThreadLocal 的“单例模式”
上代码
/**
* @program: adpn-pattern->ThreadLocalSingleton
* @description: 基于ThreadLocal单例模式
* @author: Jstar
* @create: 2019-11-23 21:33
**/
public class ThreadLocalSingleton {
private ThreadLocalSingleton() {
}
private static ThreadLocal<ThreadLocalSingleton> threadLocalSingleton=new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance(){
return threadLocalSingleton.get();
}
}
//多线程
public class ThreadLocalThread implements Runnable {
@Override
public void run() {
ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
System.out.println(Thread.currentThread().getName()+" "+instance);
}
}
//测试类
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocalThread threadLocalThread = new ThreadLocalThread();
Thread t1 = new Thread(threadLocalThread);
Thread t2 = new Thread(threadLocalThread);
t1.start();
t2.start();
System.out.println("主线程创建的单例对象为:"+ThreadLocalSingleton.getInstance());
System.out.println("主线程创建的单例对象为:"+ThreadLocalSingleton.getInstance());
System.out.println("主线程创建的单例对象为:"+ThreadLocalSingleton.getInstance());
System.out.println("test over");
}
}
我们直接来看运行结果吧

我们可以看出 主线程创建的都是一个对象,每个线程创建的单例对象都不同。其实,这主要是基于 ThreadLocal 对象,ThreadLocal 会把每个线程隔离开来,每个线程各自创建一类对象,互不影响,这是一种以空间换时间的做法。
好了,单例模式,我们暂时就讲这么多了,多谢大家看到这,希望大家还是多总结,好记性不如烂笔头。实际业务中应用哪种单例模式,还需要理解每一种应用,使用相应的创建单例的方式。

1961

被折叠的 条评论
为什么被折叠?



