首页 科技正文

allbet注册:吐血整理全网最全的单例模式

admin 科技 2020-06-26 4 1

前言

之前文章已经说过了设计模式的七大原则,即接口屏障原则,开闭原则,依赖倒转原则,迪米特原则,里氏替换原则,单一职责原则,合成复用原则,不明白的,可以移至XXXX(代写)。从今天最先我们就要学习一些常见的设计模式,利便我们以后看源码使用,固然,也可以指导我们平时的编码义务。

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

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

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

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

单例模式的观点和作用

观点

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

为什么要用单例模式?

  • 只有一个全局的实例,减少了内存开支,特别是某个工具需要频仍的建立和销毁的时刻,而建立和销毁的历程由jvm执行,我们无法对其举行优化,以是单例模式的优势就显现出来啦。
  • 单例模式可以制止对资源的多重占用,制止泛起多线程的复杂问题

单例模式的写法重点

组织方式私有化

我们需要将组织方式私有化,而默认不写的话,是公有的组织方式,外部可以显式的挪用来建立工具,我们的目的是让外部不能建立工具。

提供获取实例的公有方式

对外只提供一个公有的的方式,用来获取实例,而这个实例是否是唯一的,单例的,由方式决议,外部无需体贴。

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

饿汉式和懒汉式的区别

饿汉式

饿汉式,从名字上也很好明白,就是“对照饿”,迫在眉睫的想用饭,实例在初始化的时刻就已经建好了,不管你有没有用到,都先建好了再说。

懒汉式

饿汉式,从名字上也很好明白,就是“对照懒”,不想用饭,等饿的时刻再吃。在初始化的时刻先不建好工具,若是之后用到了,再建立工具。

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

A类

public class A {
    //私有的组织方式
    private A(){}
    //私有的静态变量
    private final static A a=new A();
    //对外的公有方式
    public static A getInstance(){
        return a;
    }
}

 

测试类

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());
    }
}

 

运行效果

说明

该方式接纳的静态常量的方式来天生对应的实例,其只在类加载的时刻就天生了,后续并不会再天生,以是其为单例的。

优点

在类加载的时刻,就完成实例化,制止线程同步问题。

瑕玷

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

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

A类

public class A { 
    //私有的组织方式
     private A(){}
    //私有的静态变量
     private final static A a; 
    //静态代码块 
    static{ a=new A(); } 
    //对外的公有方式
    public static A getInstance(){
     return a; 
    }
}

 

测试类

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());
    }
}

 

运行效果

说明

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

优点瑕玷

与静态变量饿汉式的优瑕玷类似。

3.懒汉式

A类

public class A {
    //私有的组织方式
    private A(){}
    //私有的静态变量
    private  static A a;
    //对外的公有方式
    public static A getInstance(){
        if(a==null){
            a=new A();
        }
        return a;
    }
}

 

测试类和运行效果

同上。

优点

该方式简直做到了用到即加载,也就是当挪用getInstance的时刻,才判断是否有该工具,若是不为空,则直接放回,若是为空,则新建一个工具并返回,到达了懒加载的效果。

瑕玷

当多线程的时刻,可能会发生多个实例。好比我有两个线程,同时挪用getInstance方式,并都到了if语句,他们都新建了工具,那这里就不是单例的啦。

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

public class A {
    //私有的组织方式
    private A(){}
    //私有的静态变量
    private  static A a;
    //对外的公有方式
    public synchronized static A getInstance(){
        if(a==null){
            a=new A();
        }
        return a;
    }
}

 

测试类和运行效果

同上。

优点

通过synchronize关键字,解决了线程不平安的问题。若是两个线程同时挪用getInstance方式时,那就先执行一个线程,另一个守候,品级一个线程运行竣事了,另一个守候的最先执行。

瑕玷

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

我们想着既然是方式同步导致了性能的问题,我们焦点的代码就是新建工具的历程,也就是new A();的历程,我们能不能只对部门代码举行同步呢?

那就是方式5啦。

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

A类

public class A {
    //私有的组织方式
    private A(){}
    //私有的静态变量
    private  static A a;
    public  static A getInstance(){
        if(a==null){
            synchronized (A.class){
                a=new A();
            }
        }
        return a;
    }
} 

 

测试类和运行效果

如上。

优点

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

瑕玷

这个没有意义,并没有解决多线程的问题。我们可以看到若是两个线程同时挪用getInstance方式,而且都已经进入了if语句,即synchronized的位置,即便同步了,第一个线程先执行,进入synchronized同步的代码块,建立了工具,另一个进入守候状态,品级一个线程执行竣事,第二个线程照样会进入synchronized同步的代码块,建立工具。这个时刻我们可以发现,对这代码块加了synchronized没有任何意义,照样建立了多个工具,并不相符单例。

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

A类

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;
    }
} 

 

测试类和运行效果

同上。

优点

强烈推荐使用,这种写法既制止了在多线程中泛起线程不平安的情形,也能提高性能。

咱详细来说,若是两个线程同时挪用了getInstance方式,而且都已到达了if语句之后,synchronized语句之前,此时第一个线程进入synchronized之中,先判断是否为空,很显然第一次肯定为空,那么则新建了工具。等到第二个线程进入synchronized之中,先判断是否为空,显然第一个已经建立了,以是即不新建工具。下次,不管是一个线程或者多个线程,在第一个if语句那就判断出有工具了,便直接返回啦,基本进不了内里的代码。

瑕玷

就是这么完善,没有瑕玷,哈哈哈。

volatile(插曲)

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

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

2.指令级并行的重排序:现代处理器接纳指令级并行手艺,将多条指令重叠执行。

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

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

// 可以分解为以下三个步骤
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化工具
3 s=memory //设置s指向刚分配的地址
 // 上述三个步骤可能会被重排序为 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类

public class A {
    //私有组织函数
    private A() {
    }

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

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

 

 

优点

B在A装载的时刻并不会装载,而是会在挪用getInstance的时刻装载,这利用了JVM的装载机制。这样一来,优点有两点,其一就是没有A加载的时刻,就装载了a工具,而是在挪用的时刻才装载,制止了资源的虚耗。其二是多线程状态下,没有线程平安性的问题。

瑕玷

没有瑕玷,太完善啦。

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

问题1:私有组织器并不平安

若是不明白反射,可以查看我之前的文章,传送门,万字总结之反射(框架之魂)。

若是我们的工具是通过反射方式invoke出来,这样新建的工具与通过挪用getInstance新建的工具是不一样的,详细咱来看代码。

 

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检查一样,只是多了个接口。

 

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;
    }
} 

 

测试类:

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方式,不管是显式的照样默认的,它都市返回一个新建的实例,这个新建的实例不同于该类初始化时建立的实例。

A类

public enum A {
  a;
  public A getInstance(){
      return a;
  }
}

 

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

public final class  A extends Enum< A> {      
public static final A a;
public static A[] values();
public static AvalueOf(String s);
static {}; }

 

若何解决问题1?

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类型的组织方式,我们再看下是不是组织方式的问题。

我们再用父类的有参组织方式试下,代码如下:

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?

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));
    }
}

 

运行效果;

优点

制止了反射带来的工具不一致问题和反序列问题,简朴来说,就是简朴高效没问题。

结语

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

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

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

若是有说的纰谬的地方,还请列位指正,我好继续学习去。

参考资料

一个单例模式中volatile关键字引发的思索

 

,

Allbet

www.szqygww.com欢迎进入欧博开户平台(Allbet Gaming),欧博开户平台开放欧博(Allbet)开户、欧博(Allbet)代理开户、欧博(Allbet)电脑客户端、欧博(Allbet)APP下载等业务

版权声明

本文仅代表作者观点,
不代表本站Sunbet的立场。
本文系作者授权发表,未经许可,不得转载。

评论

精彩评论