前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >万字总结之单例模式

万字总结之单例模式

作者头像
陈琛
发布2020-06-12 16:17:37
3570
发布2020-06-12 16:17:37
举报
文章被收录于专栏:陈琛的Redis文章

前言

这里不得不先吐槽下,尤其是接手原来的老项目,负责人已经溜了,你不得不上。哎,要是代码写的优雅,注释明了,那就是祖上烧高香了,得此优秀项目。那要是写的一团糟,代码耦合性强,牵一发而动全身,注释根本没有,变量名方法名不规范,甚至用拼音的,那真的分分钟想打人的心都有了。那读代码根本就靠猜好吗,完全考验你和对方的心有灵犀程度。哎,算了,小仙女不生气,要温柔(微笑脸)。

吐槽结束,那我们平常的要多学习下设计模式,写出不让人吐槽的代码。之前文章已经说过了设计模式的七大原则,即接口屏蔽原则,开闭原则,依赖倒转原则,迪米特原则,里氏替换原则,单一职责原则,合成复用原则,不明白的,可以移至XXXX(代写)。从今天开始我们就要学习一些常见的设计模式,方便我们以后看源码使用,当然,也可以指导我们平常的编码任务。

我们常见的设计模式主要有23种,分为3种类型,咱也不全说,只写重要的几个把。

创建型:单例模式,工厂模式,原型模式

结构型:适配器模式,装饰模式,代理模式

行为型:模板模式,观察者模式,状态模式,责任链模式

单例模式的概念和作用

概念

系统中只需要一个全局的实例,比如一些工具类,Converter,SqlSession等。

为什么要用单例模式?

  • 只有一个全局的实例,减少了内存开支,特别是某个对象需要频繁的创建和销毁的时候,而创建和销毁的过程由jvm执行,我们无法对其进行优化,所以单例模式的优势就显现出来啦。
  • 单例模式可以避免对资源的多重占用,避免出现多线程的复杂问题。

单例模式的写法重点

构造方法私有化

我们需要将构造方法私有化,而默认不写的话,是公有的构造方法,外部可以显式的调用来创建对象,我们的目的是让外部不能创建对象。

提供获取实例的公有方法

对外只提供一个公有的的方法,用来获取实例,而这个实例是否是唯一的,单例的,由方法决定,外部无需关心。

单例模式的常见写法(如下,重点)

饿汉式和懒汉式的区别

饿汉式,从名字上也很好理解,就是“比较饿”,迫不及待的想吃饭,实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。

懒汉式,从名字上也很好理解,就是“比较懒”,不想吃饭,等饿的时候再吃。在初始化的时候先不建好对象,如果之后用到了,再创建对象。

1.饿汉式(静态变量)--可以使用

a.测试

A类

代码语言:javascript
复制
public class A {
    //私有的构造方法
    private A(){}
    //私有的静态变量
    private final static A a=new A();
    //对外的公有方法
    public static A getInstance(){
        return a;
    }
}

测试类

代码语言:javascript
复制
public class test {
    public static void main(String[] args){
        A a1=A.getInstance();
        System.out.println(a1.hashCode());

        A a2=A.getInstance();
        System.out.println(a2.hashCode());
    }
}

运行结果

b.说明

该方法采用的静态常量的方法来生成对应的实例,其只在类加载的时候就生成了,后续并不会再生成,所以其为单例的。

c.优点

在类加载的时候,就完成实例化,避免线程同步问题。

d.缺点

没有达到懒加载的效果,如果从始到终都没有用到这个实例,可能会导致内存的浪费。

2.饿汉式(静态代码块)--可以使用

a.测试

A类

代码语言:javascript
复制
public class A {
    //私有的构造方法
     private A(){}
    //私有的静态变量
     private final static A a;
    //静态代码块
    static{ a=new A(); }
    //对外的公有方法
    public static A getInstance(){
     return a;
    }
}

测试类

代码语言:javascript
复制
public class test {
    public static void main(String[] args){
        A a1=A.getInstance();
        System.out.println(a1.hashCode());

        A a2=A.getInstance();
        System.out.println(a2.hashCode());
    }
}

运行结果

b.说明

该静态代码块的饿汉式单例模式与静态变量的饿汉式模式大同小异,只是将初始化过程移到了静态代码块中。

c.优点缺点

与静态变量饿汉式的优缺点类似。

3.懒汉式

a.测试

A类

代码语言:javascript
复制
public class A {
    //私有的构造方法
    private A(){}
    //私有的静态变量
    private  static A a;
    //对外的公有方法
    public static A getInstance(){
        if(a==null){
            a=new A();
        }
        return a;
    }
}

测试类和运行结果同上。

b.优点

该方法的确做到了用到即加载,也就是当调用getInstance的时候,才判断是否有该对象,如果不为空,则直接放回,如果为空,则新建一个对象并返回,达到了懒加载的效果。

c.缺点

当多线程的时候,可能会产生多个实例。比如我有两个线程,同时调用getInstance方法,并都到了if语句,他们都新建了对象,那这里就不是单例的啦。

4.懒汉式(线程安全,同步方法)--可以使用

a.代码

代码语言:javascript
复制
public class A {
    //私有的构造方法
    private A(){}
    //私有的静态变量
    private  static A a;
    //对外的公有方法
    public synchronized static A getInstance(){
        if(a==null){
            a=new A();
        }
        return a;
    }
}

测试类和运行结果同上。

b.优点

通过synchronize关键字,解决了线程不安全的问题。如果两个线程同时调用getInstance方法时,那就先执行一个线程,另一个等待,等第一个线程运行结束了,另一个等待的开始执行。

c.缺点

这种方法是解决了线程不安全的问题,却给性能带来了很大的问题,效率太低了,getInstance经常发生,每一次都要同步这个方法。

我们想着既然是方法同步导致了性能的问题,我们核心的代码就是新建对象的过程,也就是new A();的过程,我们能不能只对部分代码进行同步呢?那就是方法5啦。

5.懒汉式(线程不安全)

a.代码

代码语言:javascript
复制
public class A {
    //私有的构造方法
    private A(){}
    //私有的静态变量
    private  static A a;
    public  static A getInstance(){
        if(a==null){
            synchronized (A.class){
                a=new A();
            }
        }
        return a;
    }
}

测试类和运行结果如上。

b.优点

懒汉式的通用优点,用到才创建,达到懒加载的效果。

c.缺点

这个没有意义,并没有解决多线程的问题。我们可以看到如果两个线程同时调用getInstance方法,并且都已经进入了if语句,即synchronized的位置,即便同步了,第一个线程先执行,进入synchronized同步的代码块,创建了对象,另一个进入等待状态,等第一个线程执行结束,第二个线程还是会进入synchronized同步的代码块,创建对象。这个时候我们可以发现,对这代码块加了synchronized没有任何意义,还是创建了多个对象,并不符合单例。

6.双重检查 --强烈推荐使用

a.代码

代码语言:javascript
复制
public class A {
    //私有的构造方法
    private A() {
    }

    //私有的静态变量
    private volatile static A a;

    //对外的公有方法
    public static A getInstance() {
        if (a == null) {
            synchronized (A.class) {
                if (a == null) {
                    a = new A();
                }
            }
        }
        return a;
    }
}

测试类和运行结果同上。

b.优点

强烈推荐使用,这种写法既避免了在多线程中出现线程不安全的情况,也能提高性能。咱具体来说,如果两个线程同时调用了getInstance方法,并且都已到达了if语句之后,synchronized语句之前,此时第一个线程进入synchronized之中,先判断是否为空,很显然第一次肯定为空,那么则新建了对象。等到第二个线程进入synchronized之中,先判断是否为空,显然第一个已经创建了,所以即不新建对象。下次,不管是一个线程或者多个线程,在第一个if语句那就判断出有对象了,便直接返回啦,根本进不了里面的代码。

c.缺点

就是这么完美,没有缺点,哈哈哈。

c.volatile(插曲)

咱先来看一个概念重排序,也就是语句的执行顺序会被重新安排。其主要分为三种:

1.编译器优化的重排序:可以重新安排语句的执行顺序。

2.指令级并行的重排序:现代处理器采用指令级并行技术,将多条指令重叠执行。

3.内存系统的重排序:由于处理器使用缓存和读写缓冲区,所以看上去可能是乱序的。

上面代码中的a = new A();可能被被JVM分解成如下代码:

代码语言:javascript
复制
// 可以分解为以下三个步骤
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址
代码语言:javascript
复制
 // 上述三个步骤可能会被重排序为 1-3-2,也就是:
1 memory=allocate();// 分配内存 相当于c的malloc
3 s=memory //设置s指向刚分配的地址
2 ctorInstanc(memory) //初始化对象

一旦假设发生了这样的重排序,比如线程A在执行了步骤1和步骤3,但是步骤2还没有执行完。这个时候线程B有进入了第一个if语句,它会判断a不为空,即直接返回了a。其实这是一个未初始化完成的a,即会出现问题。

所以我们会将入volatile关键字,来禁止这样的重排序,即可正常运行。

7.静态内部类 --强烈推荐使用

a.代码

代码语言:javascript
复制
public class A {
    //私有构造函数
    private A() {
    }

    //私有的静态内部类
    private static class B {
        //私有的静态变量
        private static A a = new A();
    }

    //对外的公有方法
    public static A getInstance() {
        return B.a;
    }
}

b.优点

B在A装载的时候并不会装载,而是会在调用getInstance的时候装载,这利用了JVM的装载机制。这样一来,优点有两点,其一就是没有A加载的时候,就装载了a对象,而是在调用的时候才装载,避免了资源的浪费。其二是多线程状态下,没有线程安全性的问题。

c.缺点

没有缺点,太完美啦。

8.枚举 --Java粑粑强烈推荐使用

问题1:私有构造器并不安全

如果不明白反射,可以查看我之前的文章,传送门,万字总结之反射(框架之魂)。如果我们的对象是通过反射方法invoke出来,这样新建的对象与通过调用getInstance新建的对象是不一样的,具体咱来看代码。

代码语言:javascript
复制
public class test {
    public static void main(String[] args) throws Exception {
        A a=A.getInstance();
        A b=A.getInstance();
        System.out.println("a的hash:"+a.hashCode()+",b的hash:"+b.hashCode());

        Constructor<A> constructor=A.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        A c=constructor.newInstance();
        System.out.println("a的hash:"+a.hashCode()+",c的hash:"+c.hashCode());

    }
}

我们来看下运行结果:

我们可以看到c的hashcode是和a,b不一样,因为c是通过构造器反射出来的,由此可以证明私有构造器所组成的单例模式并不是十分安全的。

问题2:序列化问题

我们先将A类实现一个Serializable接口,具体代码如下,跟之前的双重if检查一样,只是多了个接口。

代码语言:javascript
复制
public class A implements Serializable {
    //私有的构造方法
    private A() {
    }

    //私有的静态变量
    private volatile static A a;

    //对外的公有方法
    public static A getInstance() {
        if (a == null) {
            synchronized (A.class) {
                if (a == null) {
                    a = new A();
                }
            }
        }
        return a;
    }
}

测试类:

代码语言:javascript
复制
public class test {
    public static void main(String[] args) throws Exception {
        A s = A.getInstance();

        //写
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("学习Java的小姐姐"));
        oos.writeObject(s);
        oos.flush();
        oos.close();
        //读
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("学习Java的小姐姐"));
        A s1 = (A)ois.readObject();
        ois.close();

        System.out.println(s+"\n"+s1);
        System.out.println("序列化前后两个是否同一个:"+(s==s1));
    }
}

我们来看下运行结果,很显然序列化前后两个对象并不相等。为什么会出现这种问题呢?这个讲起来,又可以写一篇文章了。简单来说,任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。

引出枚举

代码语言:javascript
复制
public enum A {
  a;
  public A getInstance(){
      return a;
  }
}

看着代码量很少,我们将其编译下,代码如下:

代码语言:javascript
复制
public final class  A extends Enum< A> {    
    public static final  A a;    
    public static  A[] values();   
    public static  AvalueOf(String s);  
    static {};
}

如何解决问题1?

代码语言:javascript
复制
public class test {
    public static void main(String[] args) throws Exception {
        A a1 = A.a;
        A a2 = A.a;
        System.out.println("正常情况下,实例化两个实例是否相同:" + (a1 == a2));

        Constructor<A> constructor = null;
        constructor = A.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        A a3 = null;
        a3 = constructor.newInstance();
        System.out.println("a1的hash:" + a1.hashCode() + ",a2的hash:" + a2.hashCode() + ",a3的hash:" + a3.hashCode());
        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (a1 == a3));
    }
}

运行结果:

我们看到报错了,是在寻找构造函数的时候报错的,即没有无参的构造方法,那我们看下他继承的父类ENUM有没有构造函数,看下源码,发现有个两个参数String和int类型的构造方法,我们再看下是不是构造方法的问题。

我们再用父类的有参构造方法试下,代码如下:

代码语言:javascript
复制
public class test {
    public static void main(String[] args) throws Exception {
        A a1 = A.a;
        A a2 = A.a;
        System.out.println("正常情况下,实例化两个实例是否相同:" + (a1 == a2));
        Constructor<A> constructor = null;
        constructor = A.class.getDeclaredConstructor(String.class,int.class);//其父类的构造器
        constructor.setAccessible(true);
        A a3 = null;
        a3 = constructor.newInstance("学习Java的小姐姐",1);
        System.out.println("a1的hash:" + a1.hashCode() + ",a2的hash:" + a2.hashCode() + ",a3的hash:" + a3.hashCode());
        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (a1 == a3));
    }
}

运行结果如下:

我们发现报错信息的位置已经换了,现在是已经有构造方法,而是在newInstance方法的时候报错了,我们跟下源码发现,人家已经明确写明了如果是枚举类型,直接抛出异常,代码如下,所以是无法使用反射来操作枚举类型的数据的。

如何解决问题2?

代码语言:javascript
复制
public class test {
    public static void main(String[] args) throws Exception {
        A s = A.a;

        //写
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("学习Java的小姐姐"));
        oos.writeObject(s);
        oos.flush();
        oos.close();
        //读
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("学习Java的小姐姐"));
        A s1 = (A)ois.readObject();
        ois.close();

        System.out.println(s+"\n"+s1);
        System.out.println("序列化前后两个是否同一个:"+(s==s1));
    }
}

运行结果;

优点

避免了反射带来的对象不一致问题和反序列问题,简单来说,就是简单高效没问题。

结语

看到这里的都是真爱的,在这里先谢谢各位大佬啦。

单例模式是最简单的一种设计模式,主要包括八种形式,分别是饿汉式静态变量,饿汉式静态代码块,懒汉式线程不安全,懒汉式线程安全,懒汉式线程不安全(没啥意义),懒汉式双重否定线程安全,内部静态类,枚举类型。

这几种最优的是枚举类型和内部静态类,其次是懒汉式双重否定,剩下的都差不多啦。

如果有说的不对的地方,还请各位指正,我继续学习去。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-02-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 学习Java的小姐姐 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 单例模式的概念和作用
    • 概念
      • 为什么要用单例模式?
      • 单例模式的写法重点
        • 构造方法私有化
          • 提供获取实例的公有方法
          • 单例模式的常见写法(如下,重点)
          • 饿汉式和懒汉式的区别
            • 饿汉式,从名字上也很好理解,就是“比较饿”,迫不及待的想吃饭,实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。
              • 懒汉式,从名字上也很好理解,就是“比较懒”,不想吃饭,等饿的时候再吃。在初始化的时候先不建好对象,如果之后用到了,再创建对象。
              • 1.饿汉式(静态变量)--可以使用
                • A类
                  • 测试类
                    • 运行结果
                      • b.说明
                        • c.优点
                          • d.缺点
                          • 2.饿汉式(静态代码块)--可以使用
                            • A类
                              • 测试类
                                • 运行结果
                                  • b.说明
                                    • c.优点缺点
                                    • 3.懒汉式
                                      • A类
                                        • 测试类和运行结果同上。
                                          • b.优点
                                            • c.缺点
                                            • 4.懒汉式(线程安全,同步方法)--可以使用
                                              • b.优点
                                                • c.缺点
                                                • 5.懒汉式(线程不安全)
                                                  • a.代码
                                                    • 测试类和运行结果如上。
                                                      • b.优点
                                                        • c.缺点
                                                        • 6.双重检查 --强烈推荐使用
                                                          • a.代码
                                                            • 测试类和运行结果同上。
                                                              • b.优点
                                                                • c.缺点
                                                                  • c.volatile(插曲)
                                                                    • 咱先来看一个概念重排序,也就是语句的执行顺序会被重新安排。其主要分为三种:
                                                                    • 7.静态内部类 --强烈推荐使用
                                                                      • b.优点
                                                                        • c.缺点
                                                                        • 8.枚举 --Java粑粑强烈推荐使用
                                                                          • 问题1:私有构造器并不安全
                                                                            • 问题2:序列化问题
                                                                              • 引出枚举
                                                                                • 如何解决问题1?
                                                                                  • 如何解决问题2?
                                                                                    • 优点
                                                                                    • 结语
                                                                                    领券
                                                                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档