JAVA之实现单例模式

单例模式

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

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。

这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

1.单例模式的结构:

单例模式的主要有以下角色:
1、单例类。只能创建一个实例的类。
2、访问类。使用单例类

2.单例模式的实现:

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

饿汉式:(静态变量方法)

/**
 * 饿汉式:静态变量创建类的方法
 */
public class SingletonDemo1 {
    /**
     * 构造器私有化,避免外部创建对象
     */
    private SingletonDemo1() {
    }
​
    /**
     * 在本类中创建本类对象,static修饰,保障其能够被静态方法访问
     */
    private static SingletonDemo1 instance = new SingletonDemo1();
​
    /**
     * 外部直接调用静态方法实例化对象
     * @return
     */
    public static SingletonDemo1 getInstance() {
        return instance;
    }
}
public class TestDemo1 {
    public static void main(String[] args) {
        //、创建singleton类的创建
        SingletonDemo1 singletonDemo1 = SingletonDemo1.getInstance();
        SingletonDemo1 singletonDemo2 = SingletonDemo1.getInstance();
        //判断获取到的两个是否是同一个对象
        System.out.println(singletonDemo1 == singletonDemo2);
    }
}

结果为true,说明singleton和singleton1在内存储中是同一块内存地址,那么它俩是同一个对象。

该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。

instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

饿汉式(静态代码块方式)

/**
 * 饿汉式:在静态代码块中创建该类对象
 */
public class SingletonDemo2 {
    /**
     * 私有构造方法,避免外部创建对象
     */
    private SingletonDemo2() {
​
    }
​
    /**
     * 成员位置创建该类的对象
     */
    private static SingletonDemo2 instance;
​
    /**
     * 在静态代码块中进行赋值
     */
    static {
        instance = new SingletonDemo2();
    }
​
    /**
     * 对外提供静态方法获取对象
     * @return
     */
    public static SingletonDemo2 getInstance() {
        return instance;
    }
}

结果为true,说明singleton和singleton1在内存储中是同一块内存地址,那么它俩是同一个对象。

该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的静态变量方式基本上一样,当然该方式也存在内存浪费问题。

懒汉式(线程不安全)

/**
 * 懒汉式:线程不安全
 */
public class SingletonDemo3 {
    /**
     * 私有构造方法,避免外部创建对象
     */
    private SingletonDemo3 () {
​
    }
​
    /**
     * 在成员位置创建该类的对象,当前只是声明了一个该类型的变量,并没有赋值
     */
    private static SingletonDemo3 instance;
​
    /**
     * 对外提供静态方法获取该对象
     * @return
     */
    public static SingletonDemo3 getInstance() {
        /**
         * 判断instance是否为null,如果是则说明还没有创建Singleton对象
         * 如果没有创建,创建一个并且返回,如果有直接返回
         */
        if (instance == null ) {
            instance = new SingletonDemo3();
        }
        return instance;
    }
}
​

结果为true,说明singletonsingleton1在内存储中是同一块内存地址,那么它俩是同一个对象。

从上面代码我们可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。

但是,如果是多线程环境,就会出现线程安全问题。

懒汉式(线程安全)

/**
 * 懒汉式:线程安全
 */
public class SingletonDemo4 {
    /**
     * 私有构造方法,避免外部创建对象
     */
    private SingletonDemo4() {
​
    }
​
    /**
     * 在成员位置创建该类的对象,当前只是声明了一个该类型的变量,并没有进行初始化
     */
    private static SingletonDemo4 instance;
​
    /**
     * 对外提供静态方法获取对象
     * @return
     */
    public static synchronized SingletonDemo4 getInstance() {
        //判断instance是否为null,如果为null,说明没有创建Singleton对象
        //如果没有创建,创建一个并返回,如果有就直接返回
        if (instance == null ) {
            instance = new SingletonDemo4();
        }
        return instance;
    }
}
​

该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。

从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

懒汉式(双重检索)

讨论一下懒汉模式中加锁(synchronized)的问题,对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。

由此也产生了一种新的实现模式:双重检查锁模式

public class SingletonDemo5 {
​
    private SingletonDemo5() {
​
    }
​
    private static SingletonDemo5 instance;
​
    public static SingletonDemo5 getInstance() {
        //第一次判断,如果instance不为null,不需要抢占锁,直接返回
        if (instance == null ) {
            synchronized (SingletonDemo5.class) {
                //第二次判断,抢到锁之后再次判断是否为null
                if (instance == null ) {
                    instance = new SingletonDemo5();
                }
            }
        }
        return instance;
    }
}

双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

要解决双重检查锁模式带来空指针异常的问题,只需要使用 **volatile** 关键字, **volatile** 关键字可以保证可见性和有序性。

public class SingletonDemo5 {
​
    private SingletonDemo5() {
​
    }
​
    private static volatile SingletonDemo5 instance;
​
    public static SingletonDemo5 getInstance() {
        //第一次判断,如果instance不为null,不需要抢占锁,直接返回
        if (instance == null ) {
            synchronized (SingletonDemo5.class) {
                //第二次判断,抢到锁之后再次判断是否为null
                if (instance == null ) {
                    instance = new SingletonDemo5();
                }
            }
        }
        return instance;
    }
}
​

懒汉式(静态内部类)

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

public class SingletonDemo6 {
    private SingletonDemo6( ) {};
    //定义一个静态内部类
    private static class SingletonHolder{
        //在内部类中声明并初始化外部类的对象
        private static final SingletonDemo6 INSTANCE = new SingletonDemo6();
    }
    //对外提供静态方法获取该对象
    private static SingletonDemo6 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

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

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

枚举方式

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

public enum Singleton {
​
    INSTANCE;
}
​

存在问题

破坏单例模式:

使上面定义的单例类(Singleton)可以创建多个对象,枚举方式(因为枚举方式是基于JVM底层的一个实现,它已经把所有的问题解决掉了)除外。有两种方式,分别是序列化和反射。

public class SingletonDemo7 implements Serializable {
    private SingletonDemo7( ) {};
    //定义一个静态内部类
    private static class SingletonHolder{
        //在内部类中声明并初始化外部类的对象
        private static final SingletonDemo7 INSTANCE = new SingletonDemo7();
    }
    //对外提供静态方法获取该对象
    public static SingletonDemo7 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
public class TestDemo7 {
    public static void main(String[] args) throws Exception{
        //调用写入操作
        writeObject2File();
        //调用读取操作
        SingletonDemo7 s1 = readObjectFromFile();
        SingletonDemo7 s2 = readObjectFromFile();
        //判断地址是否相同
        System.out.println(s1 == s2);
    }
​
    /**
     * 从文件读取数据(对象)
     * @return
     * @throws Exception
     */
    public static SingletonDemo7 readObjectFromFile() throws Exception {
        //1、创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\test.txt"));
        //2、读取对象
        SingletonDemo7 singletonDemo7 = (SingletonDemo7) ois.readObject();
        //释放资源
        ois.close();
        return singletonDemo7;
    }
​
    /**
     * 向文件中写入数据
     * @throws Exception
     */
    public static void writeObject2File() throws Exception {
        //1.获取Singleton对象
        SingletonDemo7 instance = SingletonDemo7.getInstance();
        //2.创建对象(输出流对象)
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\test.txt"));
        //3.写对象
        oos.writeObject(instance);
        //4.释放资源
        oos.close();
    }
}
​

上面代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式。

反射

public class SingletonDemo7 implements Serializable {
    private SingletonDemo7( ) {};
    //定义一个静态内部类
    private static class SingletonHolder{
        //在内部类中声明并初始化外部类的对象
        private static final SingletonDemo7 INSTANCE = new SingletonDemo7();
    }
    //对外提供静态方法获取该对象
    public static SingletonDemo7 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
​
public class TestDemo7Twice {
    public static void main(String[] args) throws Exception{
        //1、获取Singleton的字节码对象
        Class clazz = SingletonDemo7.class;
        //2、获取无惨构造方法对象
        Constructor cons = clazz.getDeclaredConstructor();
        //3、取消访问检查
        cons.setAccessible(true);
​
        SingletonDemo7 singletonDemo7 = (SingletonDemo7) cons.newInstance();
        SingletonDemo7 singletonDemo71 = (SingletonDemo7) cons.newInstance();
        System.out.println(singletonDemo7 == singletonDemo71);
    }
}
​

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

解决的问题

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

在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

public class SingletonDemo7 implements Serializable {
    private SingletonDemo7( ) {};
    //定义一个静态内部类
    private static class SingletonHolder{
        //在内部类中声明并初始化外部类的对象
        private static final SingletonDemo7 INSTANCE = new SingletonDemo7();
    }
    //对外提供静态方法获取该对象
    public static SingletonDemo7 getInstance() {
        return SingletonHolder.INSTANCE;
    }
​
    public Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}
​

反射方式破解单例的解决方法

public class SingletonDemo7Twice implements Serializable {
    private static boolean flag = false;
    private SingletonDemo7Twice( ) {
        synchronized (SingletonDemo7Twice.class) {
            if (flag) {
                throw new RuntimeException("不能创建多个对象");
            }
            flag = true;
        }
    };
    //定义一个静态内部类
    private static class SingletonHolder{
        //在内部类中声明并初始化外部类的对象
        private static final SingletonDemo7Twice INSTANCE = new SingletonDemo7Twice();
    }
    //对外提供静态方法获取该对象
    public static SingletonDemo7Twice getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
​

这种方式比较好理解。当通过反射方式调用构造方法进行创建时,直接抛异常。不运行此操作。

总结

. 单例中两种饿汉式(静态变量,静态代码)可用,但是存在性能问题。

\2. 单例中两种懒汉式(线程不安全,线程安全)不推荐,存在线程安全问题,线程安全方法的方式解决了线程的问题,但是性能极差。

\3. 最后三种单例模式(双重检查锁,静态内部类,枚举方式)值得推荐。

\4. 注意事项

4.1 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。

4.2 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new。

\5. 应用

5.1 dk源码中Runtime类

5.2 tomcat中ApplicationContext类

5.3 session 工厂

你可能感兴趣的:(单例模式,java,开发语言)