当前位置:   article > 正文

设计模式之单例模式(七种方法超详细)

单例模式

简介

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象,也就是说不能使用new关键字来创建对象。

1.单例模式的实现

单例设计模式分类两种:
​ 饿汉式:类加载就会导致该单实例对象被创建。
​ 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。

1.1 饿汉式
饿汉式-方式1(静态变量方式)

这种方法通过将对象的实例设置为静态的方式,保证了该对象的实例,永远只有一份,且该对象的创建在类加载的时候就会立即创建在jvm内存中的方法区,在程序运行期间永久存在,所以当我们的对象太大的时候就会造成一种资源的浪费。

代码示例:

/**
* 饿汉式
* 静态变量创建类的对象
*/
public class Singleton {
    //私有构造方法
    private Singleton() {}
    //在成员位置创建该类的对象
    private static Singleton instance = new Singleton();
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
饿汉式-方式2(静态代码块方式)

在方式2中,对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样。

/**
* 饿汉式-方法2
* 在静态代码块中创建该类对象
*/
public class Singleton {
    //私有构造方法
    private Singleton() {}
    //在成员位置创建该类的对象
    private static Singleton instance;
    static {
        instance = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
1.2 懒汉式
懒汉式-方式1(线程不安全)

从名字就可以看出二者的区别,饿汉就是一直处于饿的状态,需要不断有食物给你,也就是对象一直存在。

而懒汉式,就比较懒惰,只有真正饿的时候才会寻找食物,也就是请求对象实例.

所以,当在以下代码中只要调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。

但是,如果是多线程环境下,每个线程抢占Singleton类的对象资源,但是可能会发生对个线程同时请求对象实例的问题,这个时候就有可能创建多个对象,从而导致数据不一致,就会出现线程安全问题。

/**
 * 懒汉式
 * 线程不安全
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}
    //在成员位置创建该类的对象
    private static Singleton instance;
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
懒汉式-方式2(线程安全)

所以对方式1的线程不安全,我们在方式2中进行了优化,通过加同步锁的机制,保证了每次只有一个线程可以操作我们当前的对象,确保了线程安全,

但是由于加锁就会导致该代码执行效率特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了,所以基于此,我们做进一步优化。

/**
 * 懒汉式
 * 线程安全
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}
    //在成员位置创建该类的对象
    private static Singleton instance;
    //对外提供静态方法获取该对象,并且加锁
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
懒汉式-方式3(双重检查锁)

再来讨论一下懒汉模式中加锁的问题,对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必要让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式;

/**
 * 双重检查方式
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}
    private static Singleton instance;
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance == null) {
            synchronized (Singleton.class) {
//抢到锁之后再次判断是否为null
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在双重检查锁模式下,为什么要进行两次的判断呢?

现在我们假设有两个线程a,b。两个线程都去请求我们单例模式下类的实例,

当第一个判断的时候,两个线程都会进入判断代码块中进行锁的抢占,最终a抢占到了锁,那么b只能在加锁的代码块外部进行等候,这个时候a创建了对象的实例,完成功能后归还了锁,这个时候线程b马上抢占到了锁,然后进入内部代码块

假设在这里没有第二次判断的话,线程a就会再次创建一个新的对象,所以,要在这里再加一次判断。

双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,但是呢,JVM在实例化对象的时候会进行优化和指令重排序操作,在多线程的情况下,就可能会出现空指针问题

具体的细节不再叙述,大概进行一些讲解;
对于上述对象的创建主要分为三个部分:

  1. 分配对象的内存空间。
  2. 初始化对象。
  3. 设置instance指针指向对象所在的内存空间。
    为了提高性能在JVM在实例化对象的时候会进行优化和指令重排序操作,也就是说有可能将我们上述的第七行和第九行代码进行顺序的交换。

也就是说当我们上面a线程得到锁以后,这时候还没有初始化我们的对象,就先设置了instance指针指向了我们的对象所在的内存空间,

a线程在设置了instance指针指向了我们的对象所在的内存空间以后就归还了锁,线程b这个时候拿到锁以后,检查到对象不为空,直接返回了线程a创建的对象,但是这个时候线程a还没有完成对象的初始化,所以就导致了线程b拿到的对象是一个空对象,就会出现空指针的问题。

那么如何解决上述的问题呢,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性,这个关键字禁止了对当前修饰的变量上下文重排序。保证了方法的可靠性。

代码示例

/**
 * 双重检查方式
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}
    //使用volatile修饰,禁止重排序
    private static volatile Singleton instance;
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
        if(instance == null) {
            synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
懒汉式-方式4(静态内部类方式)

静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

/**
 * 静态内部类方式
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

说明:
​ 第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder
并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

所以静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

补充:饿汉式-方式3 (枚举方式)

枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚
举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

/**
 * 枚举方式
 */
public enum Singleton {
    INSTANCE;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

没错,正如你所看到,枚举正是你所看到的这样就是这么简单,实际上枚举类型是在Java的语法糖。

扩展:

在计算机科学中,语法糖(syntactic sugar)是指编程语言中可以更容易的表达一个操作的语法,它可以使程序员更加容易去使用这门语言:操作可以变得更加清晰、方便,或者更加符合程序员的编程习惯,总的来说语法糖是为了我们更加方便的简单的书写代码,有没有语法糖我们都可以实现类似的功能。

在此我们可以对枚举方式实现的实例进行反编译,可以得到如下的代码:

package com.qgn.mianshi.main;
public final class Singleton extends Enum
{
    public static Singleton[] values()
    {
        return (Singleton[])$VALUES.clone();
    }
    public static Singleton valueOf(String name)
    {
        return (Singleton)Enum.valueOf(com/qgn/mianshi/main/Singleton, name);
    }
    private Singleton(String s, int i)
    {
        super(s, i);
    }
    public static final Singleton INSTANCE;
    private static final Singleton $VALUES[];
    static 
    {
        INSTANCE = new Singleton("INSTANCE", 0);
        $VALUES = (new Singleton[] {
            INSTANCE
        });
    }
}
  • 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

可以看到我们的一个枚举类实际上是继承了一个Enum方法,并重写将我们在枚举类中定义的变量进行了一些封装,基本上我们的这个类中的元素都是通过static修饰的。所以一个枚举类他所有的资源都是静态的。

而我们知道,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。

也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。
所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。

2.单例模式存在的问题

2.1 单例模式被破坏

在上面定义的单例类(Singleton)中正常的使用的情况下只可以同时只有一个对象存在,但是存在着一些操作可以破坏这种现象,使得上述单例模式可以创建多个对象(枚举方式除外)。

这两种方式,分别是序列化和反射。
下面进行演示:

● 序列化反序列化破坏单例模式

在这里我们使用性能较好的内部类方式的单例模式,来检验一下,序列化与反序列化是否会创建不同的对象。

代码示例

Singleton类:

public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Test类:

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singleton singleton = Singleton.getInstance();
        writeObject(singleton);
        Singleton s1 = readObject();
        Singleton s2 = readObject();
        System.out.println(singleton == s1);
        System.out.println(s1 == s2);
    }
    private static Singleton readObject() throws IOException, ClassNotFoundException {
            //创建对象输入流对象
            ObjectInputStream input = new ObjectInputStream(new FileInputStream("E:\\ziyuan\\object.txt"));
            //读取创建的对象
            Singleton obj = (Singleton) input.readObject();
            return obj;
    }
    private static void writeObject(Singleton singleton) throws IOException {
        //创建对象输出流对象
        ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("E:\\ziyuan\\object.txt"));
        //将单例模式创建的对象写入文件
        output.writeObject(singleton);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

上面代码运行结果都是是 false ,表明序列化和反序列化已经破坏了单例设计模式只有一个对象存在的原则。

● 反射破坏单例模式

Singleton类还是使用内部类的方式实现单例模式。

  public static void main(String[] args) throws Exception {
        //获取Singletion类的字节码;
        Class<Singleton> singletonClass = Singleton.class;
        //获取无参构造方法,用来创建对象。
        Constructor con = singletonClass.getDeclaredConstructor();
        //由于是private修饰,所以使用暴力破解;
        con.setAccessible(true);
        //创建对象
        Singleton s1 = (Singleton) con.newInstance();
        Singleton s2 = (Singleton) con.newInstance();
        System.out.println(s1 == s2);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

上面代码运行结果也是 false ,表明反射也破坏了单例设计模式。

2.2 原因分析

实际上对于序列化与反序列化破坏单例模式的问题,主要是通过readObject()方法,出现了破坏单例模式的现象,主要是因为这个方法最后会通过反射调用无参数的构造方法创建一个新的对象,从而每次返回的对象都不一致。

对于反射破坏单例模式是因为单例模式通过setAccessible(true)指示反射的对象在使用时,取消了 Java 语言访问检查,使得私有的构造函数能够被访问,

而单例模式的设计在于只保留一个公有静态函数来获取唯一的实例,其他方法(构造函数)或字段为私有,外界不能访问。

而反射破坏了这一原则,它突破了构造函数私有的限制,可以获取单例类的私有构造函数并使用其创建多个对象。

2.3 问题解决
● 序列化、反序列方式破坏单例模式的解决方法

在readObject()方法的调用栈的底层方法中有这么两个方法:
hasReadResolveMethod:
表示如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true

==invokeReadResolve:==通过反射的方式调用要被反序列化的类的readResolve方法。

所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

代码示例

public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
● 反射方式破解单例的解决方法

因为反射是一种暴力获取对象实例的方法,因为他可以直接访问private修饰的构造函数,所以在对于反射方式破坏单例模式的问题上我们只能采取被动的防御,

既然你能访问我的构造函数,我就在我的构造函数中建立防御机制,不让你通过我的构造函数创建多个实例对象。

代码示例:

public class Singleton  {
    private static volatile boolean flag = false;
    //私有构造方法
    private Singleton() {
        synchronized (Singleton.class){
        if (flag){
                throw new RuntimeException();
        }
         flag = true;
    }}
    private static class SingletonHolder {

        private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

关于枚举类单例模式为什么不会被破坏

其实是因为java的底层做了很多的方法来防止我们的枚举类单例模式被破坏,其中一点就是我们无法创建枚举类的实例。

枚举类拆掉语法糖之后的类和其他类有一点不同----是没有默认的无参构造器的,而是一个有参的构造器.

private EnumMode(String s, int i){

        super(s, i);

}
  • 1
  • 2
  • 3
  • 4
  • 5

其中String 类型代表的是枚举的描述值,默认为枚举字段名值,int类型代表当前枚举在枚举类中的位置。

关于更多关于枚举的知识,大家可以自行搜索,这里不再叙述。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/729600?site
推荐阅读
相关标签
  

闽ICP备14008679号