美文网首页
单例模式-java

单例模式-java

作者: JW2015 | 来源:发表于2021-12-15 22:03 被阅读0次

    单例模式是 Java 中最简单的设计模式之一。

    特点:

    (一)只能有一个实例
    (二)必须自己创建自己的唯一实例
    (三)需要给其他对象使用提供这一实例

    创建方式:

    (1)饿汉式:类加载时就会创建该单例对象。(不使用也会一直占用内存)
    (2)懒汉式:类加载不会创建对象,首次使用该对象时才会创建。(需注意控制线程安全问题)

    具体实现单例模式代码:

    (1-1)饿汉式:静态成员变量

    public class Singleton {
        //构造方法私有化
        private Singleton() {
        }
        private static Singleton instance = new Singleton();
        public static Singleton getInstance() {
            return instance;
        }
    }
    

    (1-2)饿汉式:静态代码块

    public class Singleton {
        private Singleton(){}
        private static Singleton instance;
        static {
            instance = new Singleton();
        }
        public static Singleton getInstance() {
            return instance;
        }
    }
    

    (1-3)饿汉式:枚举

    public enum  Singleton {
        INSTANCE;
    }
    

    饿汉式三种创建方法总结:

    1、静态成员变量、静态代码块两种方式没有太大区别,在类加载时会执行创建对象方法。
    2、枚举是推荐的实现方式,枚举类型是线程安全的,只会装载一次,枚举是所有单例实现中唯一一种不会被破坏的单例实现模式。(下面会介绍破坏单例的方式和解决方法)

    (2-1)懒汉式:(线程不安全)

    public class Singleton {
        private Singleton() {
        }
        private static Singleton instance;
        public static Singleton getInstance() {
            if (instance == null) {
                //线程1执行到此处开始等待
                // 线程2获取到cpu执行权,也会执行到此处
                //多个线程执行到此处,进而会创建多个对象
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    (2-2)懒汉式:(同步方法锁,线程安全)

    public class Singleton {
        private Singleton() {
        }
        private static Singleton instance;
        //单例绝大部分都是读操作,读操作是线程安全的
        //没必要每个线程必须持有锁才能调用该方法,性能低下
        //调整锁的时机,所以有了新的实现模式,双重检查锁(2-3)方式  
        public static synchronized Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    (2-3)懒汉式:(双重检查锁,线程安全)

    public class Singleton {
        private Singleton() {
        }
        //多线程情况下,可能会出现空指针问题
        //原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
        //在变量属性前用volatile关键字修饰,可以保证可见性和有序性
        private static volatile Singleton instance;
        public static Singleton getInstance() {
            //第一次判断 如果!=null 不需要抢占锁 直接返回对象 进而提升效率
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    (2-4)懒汉式:(静态内部类,线程安全)

    /**
    *由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化静态属性
    */
    public class Singleton {
        private Singleton() {
        }
        private static class SingletonHolder {
            //final修饰 防止外部类对它进行修改
            private static final Singleton INSTANCE = new Singleton();
        }
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    

    懒汉式三种创建方法总结:

    1、当不涉及多线程调用时,可以使用(2-1)不加同步锁的方式进行创建
    2、当多线程操作时,可以使用(2-2)、(2-3)的实现方式。推荐使用(2-3)双重检查锁的方式,更加高效。
    3、更加推荐的是(2-4)静态内部类的实现方式,它是一种优秀的单例模式, 是开源项目中比较常用的单例模式。不需要使用任何锁,也保证了多线程安全,并且不影响性能,也不浪费空间。

    破坏单例模式

    可以理解为通过一定的方式,使单例类可以创建多个对象,(枚举方式除外)。有两种方式,分别是反射序列化

    1、反射

    首先我们看一下通过反射创建实例的代码

     //获取字节码对象
     Class clazz = Singleton.class;
     //获取无参的构造方法
     Constructor cons = clazz.getDeclaredConstructor();
     //取消访问检查
     cons.setAccessible(true);
     //创建Singleton对象
     Singleton instance1 = (Singleton) cons.newInstance();
     Singleton instance2 = (Singleton) cons.newInstance();
    

    通过打印instance1instance2地址可以得知这是两个不同的对象。那我可以在无参私有的构造方法中进行处理,在类中添加一个静态变量进行标记是否是第一调用构造方法,在第二次调用时我们抛异常处理。如下代码:(双重检查锁举例)

    public class Singleton {
        private static boolean flag;
        private Singleton() {
            if (flag) {
                throw new RuntimeException("不允许创建多个对象");
            }
            flag = true;
        }
        private static volatile Singleton instance;
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    2、序列化

    这里我们以静态内部类的单例模式进行举例

    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;
        }
        //当进行反序列化时,会自动调用该方法,将该方法的返回值直接返回,解决序列化反序列化单例被破坏问题
      //  public Object readResolve(){
      //      return SingletonHolder.INSTANCE;
      // }
    }
    
    public static  void writeObjToFile() throws Exception {
        //将对象序列化之后 写入到文件中
        Singleton singleton = Singleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("***/a.txt"));
        oos.writeObject(singleton);
        oos.close();
    }
    public static void readObjFromFile() throws Exception{
        //从文件中读取对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("***/a.txt"));
        Singleton singleton = (Singleton) ois.readObject();
        System.out.println(singleton);
        ois.close();
    }
    

    首先我们的单例需实现Serializable接口,然后再调用writeObjToFile方法将单例Singleton对象写入到文件中。然后再多次调用readObjFromFile读取对象,经过打印对象地址发现每次取出的对象不是同一个,所以该单例被破坏
    原因在ObjectInputStream 的readObject()方法,这里jdk源码以截图形式说明:(由于截图不全,大家可以根据代码行数自行查看)

    image.png image.png image.png
    从这几个主要方法中可以得知,我们需要在Singleton类的readResolve方法中,将我们实例返回即可,也就是我上面代码中注释掉的readResolve方法。这时我们再进行读写操作时,就会得到同一个实例。

    总结

    相关文章

      网友评论

          本文标题:单例模式-java

          本文链接:https://www.haomeiwen.com/subject/dmjjfrtx.html