设计模式(一)之单例模式(Singleton Pattern)深入浅出

单例模式介绍:单例模式是指确保一个类在任何情况下都绝对只有一个实例,并且提供一个全局的访问点。隐藏其所有构造方法,属于创新型模式。

常见的单例有:ServletContext、ServletConfig、ApplicationContext、DBPool

单例模式的优点:

  • 在内存中只有一个实例,减少内存开销。
  • 可以避免对资源的占用
  • 设置全局访问点,严格控制访问

单例模式的缺点:

  • 没有接口,扩展困难
  • 如果要扩展单例对象,只有修改代码,没有其他捷径

以下是单例模式的种类及优缺点分析

饿汉式单例

在单例类首次加载时就创建实例

 第一种写法:

1
2
3
4
5
6
7
8
9
10
11
public class HungrySingleton {
 
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
 
    private HungrySingleton() {
    }
 
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

 第二种写法: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HungryStaticSingleton {
 
    private static final HungryStaticSingleton hungrySingleton;
 
    static {
        hungrySingleton = new HungryStaticSingleton();
    }
 
    private HungryStaticSingleton() {
    }
 
    public static HungryStaticSingleton getInstance() {
        return hungrySingleton;
    }
}

  缺点:单例实例在类装载时就构建,浪费资源空间

懒汉式单例

一、懒汉式第一种:

首先先简单实现以下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LazySimpleSingleton {
 
    private static LazySimpleSingleton lazySingleton = null;
 
    private LazySimpleSingleton() {
    }
 
    public static LazySimpleSingleton getInstance() {
 
        if (lazySingleton == null) {
            lazySingleton = new LazySimpleSingleton();
        }
        return lazySingleton;
    }
}

 我们用线程测一下在多线程场景下会不会出现问题

先创建一个线程类

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

     测试

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LazySimpleSingletonTest {
 
    public static void main(String[] args) {
 
        Thread t1 = new Thread(new ExectorTread());
        Thread t2 = new Thread(new ExectorTread());
 
        t1.start();
        t2.start();
 
        System.out.println("Exector End");
    }
}

  运行结果

 结果发现创建的对象不一样

如果在方法上加入锁会解决问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LazySimpleSingleton {
 
    private static LazySimpleSingleton lazySingleton = null;
 
    private LazySimpleSingleton() {
    }
 
    public synchronized static LazySimpleSingleton getInstance() {
 
        if (lazySingleton == null) {
            lazySingleton = new LazySimpleSingleton();
        }
        return lazySingleton;
    }
}

  虽然jdk1.6之后对synchronized性能优化了不少,但是还是存在一定的性能问题,这种写法会造成整个类被锁住,大大降低了性能

于是我们有了新的写法

 

二、懒汉式第二种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LazyDoubleCheckSingleton {
 
    private volatile static LazyDoubleCheckSingleton lazySingleton = null;
 
    private LazyDoubleCheckSingleton() {
    }
    //    适中方案
    //    双重检查锁
    public static LazyDoubleCheckSingleton getInstance() {
 
        if (lazySingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazySingleton;
    }
}

知识补充:

  1、线程安全性开发遵循三个原则:

  • 原子性:即一个操作或者多个操作要么全部执行,要么都不执行
  • 可见性:多个线程访问同一变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值
  • 有序性:程序执行的顺序按照代码的先后顺序执行

  通过对这段代码线程的debug发现,这里双重检查体现了可见性

  2、JVM:CPU在执行的时候会转换成JVM指令

             lazySingleton = new LazySimpleSingleton(); 这行代码实际进行了如下操作

  •     第一步、分配内存给对象
  •     第二步、初始化对象
  •     第三步、将初始化对象和内存地址关联(赋值)
  •     第四步、用户初次访问

在多线程环境中,第二步和第三步可能会发生颠倒,这就需要指令重排序,于是我们在变量声明上加上volatile关键字就很好的解决了问题

volatile相关博客

 

三、懒汉式第三种

通过内部类的方式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LazyInnerClassSingleton {
 
    private LazyInnerClassSingleton() {
    }
 
    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }
 
    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

  性能上最优的一种写法,全程没有用到synchronized,

  通过懒加载饿汉式写法达到了懒汉式的目的,LazyHolder里面的逻辑要等到外面调用才执行,巧妙地运用了内部类的特性

  有人会问这个不用考虑线程安全吗?其实这是利用了JVM底层的执行逻辑,完美的避开了线程安全性的问题

但是我们会考虑另一个问题,该类构造器虽然私有了,但是还是会被反射攻击,难逃反射法眼

我们来测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LazyInnerClassSingletonTest {
 
    public static void main(String[] args) {
 
        try {
//          调用者装B,不走寻常路,显然搞坏了单例
            Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
            Constructor<LazyInnerClassSingleton> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);//强吻(问)
            LazyInnerClassSingleton instance = constructor.newInstance();
            System.out.println(instance);
//          正常调用
            LazyInnerClassSingleton instance2 = LazyInnerClassSingleton.getInstance();
            System.out.println(instance2);
            System.out.println(instance == instance2);
 
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  运行结果

针对反射问题我们有了以下解决办法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LazyInnerClassSingleton {
 
    private LazyInnerClassSingleton() {
        if (LazyHolder.LAZY != null){
            throw new RuntimeException("不允许构建多个实例");
        }
    }
 
    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }
 
    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

  在私有构造方法上加上判断,如果已经对象被初始化就抛出异常

好的,被反射破坏的问题解决了,还会想到另一个问题,如果被反序列化对象还是单例吗?

  知识点补充:反序列化是将已经持久的的字节码内容,转换为IO流,在转换过程中重新创建对象new。

我们拿饿汉式单例测试一下

单例类:

1
2
3
4
5
6
7
8
9
10
11
public class SeriableSingleton implements Serializable {
 
    private static final SeriableSingleton singleton = new SeriableSingleton();
 
    private SeriableSingleton() {
    }
 
    public static SeriableSingleton getInstance() {
        return singleton;
    }
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SeriableSingletonTest {
 
    public static void main(String[] args) {
 
        FileOutputStream fso = null;
        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();
 
        try {
            fso = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fso);
            oos.writeObject(s2);
            oos.flush();
            oos.close();
 
            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton) ois.readObject();
            ois.close();
 
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
 
 
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  运行结果

显然反序列化破坏了单例

现在我们通过源码寻找答案

进入readObject()方法

方法通过调用readObject0(false)返回结果,再次进入readObject0()方法

 找到object类型,调用了checkResolve(readOrdinaryObject(unshared)),进入readOrdinaryObject方法

在这里我们找到了实例化对象的语句

obj = desc.isInstantiable() ? desc.newInstance() : null;

意思是如果这个对象能被初始化就实例化对象,否则等于null

在这里打个断点调试一下,确实实例化了对象,存在私有构造方法也会实例化对象

接着往下看

 

如果desc.hasReadResolveMethod()返回true,就调用Object rep = desc.invokeReadResolve(obj);返回obj

进入hasReadResolveMethod

 

 

 

 源码分析过后发现这个hasReadResolveMethod()方法是用来判断readResolve方法是否存在,如果存在返回true,不存在返回false

再看invokeReadResolve()方法

 

 返回了readResolve这个方法的返回值,

所以经过这个判断会重新加载对象并返回

 接下来我们得出结论,代码增加重写方法readResolve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SeriableSingleton implements Serializable {
 
    private static final SeriableSingleton singleton = new SeriableSingleton();
 
    private SeriableSingleton() {
    }
 
    public static SeriableSingleton getInstance() {
        return singleton;
    }
 
    protected Object readResolve() {
        return singleton;
    }
}

  再次运行解决了序列化的问题

但是值得我们注意的是,重写readResolve方法只不过是覆盖了反序列化出来的对象,对象还是创建了2次,

由于发生再JVM层面,相对来说比较安全,在之前没有被引用的对象会被GC回收(JVM知识点)

注册式单例

 一、第一种写法

使用枚举类实现单例模式,也是《Effictive 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;
    }
}
复制代码

 1、首先判断线程安全性

  通过反编译工具JAD得到枚举类的源码,附:JAD下载地址

复制代码
public final class EnumSingleton extends Enum
{

    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(com/zc/singleton/register/EnumSingleton, name);
    }

    private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

    public Object getData()
    {
        return data;
    }

    public void setData(Object data)
    {
        this.data = data;
    }

    public static EnumSingleton getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingleton INSTANCE;
    private Object data;
    private static final EnumSingleton $VALUES[];

    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}
复制代码

通过代码发现,静态代码块实例化了单例类,属于饿汉式单列,不存在线程安全性问题,这个实例化过程发生在JVM层面,所以可以认为懒加载

2、测试序列化

复制代码
public class EnumSingletonTest {

public static void main(String[] args) { FileOutputStream fso = null; EnumSingleton s1 = null; EnumSingleton s2 = EnumSingleton.getInstance(); s2.setData(new Object()); try { fso = new FileOutputStream("EnumSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fso); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("EnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (EnumSingleton) ois.readObject(); ois.close(); System.out.println(s1.getData()); System.out.println(s2.getData()); System.out.println(s1.getData() == s2.getData()); } catch (Exception e) { e.printStackTrace(); } } }
复制代码

运行结果 :

 

枚举类是怎样避免不被序列化破坏的呢?我们来查看源码

首先进入枚举类型case

 进入readEnum方法

通过枚举类对象根据注册的类名获取实例然后返回,所以不会创建新的对象

3、测试反射

复制代码
//        反射
        try {
            Class<EnumSingleton> clazz = EnumSingleton.class;
            Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            constructor.newInstance();

        }catch (Exception e){
            e.printStackTrace();
        }
复制代码

运行结果 :

报错:没有找到这样的构造方法

反编译获取的类有这样的一个构造方法

private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

我们用这个构造再实例化一次看看

测试:

复制代码
//        反射
        try {
            Class<EnumSingleton> clazz = EnumSingleton.class;
            Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class,int.class);
            constructor.setAccessible(true);
            EnumSingleton instance = constructor.newInstance("zhou", 666);
            System.out.println(instance);

        }catch (Exception e){
            e.printStackTrace();
        }
复制代码

运行结果:

报错:不能通过反射创建这个枚举对象

查看源码:

 得知如果该类的类型为枚举类,就抛出异常

总结:从JDK层面就为枚举类不被实例化和反射保驾护航

 

二、第二种写法

容器式单例,Spring容器中单例的写法

复制代码
public class ContainerSingleton {

    private ContainerSingleton() {}

    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getBean(String className){

        synchronized (ioc){

            if (!ioc.containsKey(className)){
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className,obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            }
            return ioc.get(className);
        }

    }
}
复制代码

优点:对象方便管理,其实也是属于懒加载

 

ThreadLocal

使用ThreadLocal实现单例模式

复制代码
//伪线程安全
public class ThreadLocalSingleton {

    private ThreadLocalSingleton(){}

    private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };

    public static ThreadLocalSingleton getInstance(){
        return threadLocalSingleton.get();
    }
}
复制代码

测试多线程

复制代码
public class ExectorTread implements Runnable {


    @Override
    public void run() {

        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 ThreadLocalSingletonTest {

    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.currentThread().getName() +  ":" +ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() +  ":" +ThreadLocalSingleton.getInstance());

        Thread t1 = new Thread(new ExectorTread());
        t1.start();
        Thread t2 = new Thread(new ExectorTread());
        t2.start();
    }
}
复制代码

打印结果

 结论:该单例在单个线程中可以保持单例,但是每个其他线程互相都不一样

    原理:每次获取实例会从ThreadLocalMap中取值,而每个单例的key就是线程名

属于注册式单例(容器形式)

应用场景:Spring的orm框架中

 

 

 以上对单例模式的介绍到此结束,欢迎批评指正。 附:源码地址

 

posted @   IT学无止境99  阅读(202)  评论(0编辑  收藏  举报
编辑推荐:
· SQL Server如何跟踪自动统计信息更新?
· AI与.NET技术实操系列:使用Catalyst进行自然语言处理
· 分享一个我遇到过的“量子力学”级别的BUG。
· Linux系列:如何调试 malloc 的底层源码
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
阅读排行:
· 对象命名为何需要避免'-er'和'-or'后缀
· JDK 24 发布,新特性解读!
· C# 中比较实用的关键字,基础高频面试题!
· .NET 10 Preview 2 增强了 Blazor 和.NET MAUI
· SQL Server如何跟踪自动统计信息更新?
点击右上角即可分享
微信分享提示