关于java:设计模式学习04Java实现单例模式

6次阅读

共计 12849 个字符,预计需要花费 33 分钟才能阅读完成。

写在后面

  • 记录学习设计模式的笔记
  • 进步对设计模式的灵活运用

学习地址

https://www.bilibili.com/vide…

https://www.bilibili.com/vide…

参考文章

http://c.biancheng.net/view/1…

我的项目源码
https://gitee.com/zhuang-kang/DesignPattern

4,创建者模式

创立型模式的次要关注点是“怎么创建对象?”,它的次要特点是“将对象的创立与应用拆散”。

这样能够升高零碎的耦合度,使用者不须要关注对象的创立细节。

创立型模式分为:

  • 单例模式
  • 工厂办法模式
  • 形象工程模式
  • 原型模式
  • 建造者模式

5,单例模式

5.1 单例模式的定义和特点

单例(Singleton)模式的定义: 指一个类只有一个实例,且该类能自行创立这个实例的一种模式。例如,Windows 中只能关上一个工作管理器,这样能够防止因关上多个工作管理器窗口而造成内存资源的节约,或呈现各个窗口显示内容的不统一等谬误。

在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后盾解决服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 利用的配置对象、应用程序中的对话框、零碎中的缓存等经常被设计成单例。

单例模式在现实生活中的利用也十分宽泛,例如公司 CEO、部门经理等都属于单例模型。J2EE 规范中的 ServletgContext 和 ServletContextConfig、Spring 框架利用中的 ApplicationContext、数据库中的连接池等也都是单例模式。

单例模式有 3 个特点:

  1. 单例类只有一个实例对象;
  2. 该单例对象必须由单例类自行创立;
  3. 单例类对外提供一个拜访该单例的全局拜访点。

单例模式的长处

  • 单例模式能够保障内存里只有一个实例,缩小了内存的开销。
  • 能够防止对资源的多重占用。
  • 单例模式设置全局拜访点,能够优化和共享资源的拜访。

单例模式的毛病:

  • 单例模式个别没有接口,扩大艰难。如果要扩大,则除了批改原来的代码,没有第二种路径,违反开闭准则。
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模仿生成一个新的对象。
  • 单例模式的性能代码通常写在一个类中,如果功能设计不合理,则很容易违反繁多职责准则。

5.2 单例模式的构造与实现

5.2.1 单例模式的构造

  1. 单例类:蕴含一个实例且能自行创立这个实例的类。
  2. 拜访类:应用单例的类。

5.2 代码实现

单例设计模式分类两种:

饿汉式:类加载就会导致该单实例对象被创立    

懒汉式:类加载不会导致该单实例对象被创立,而是首次应用该对象时才会创立

饿汉式(动态变量)

package com.zhuang.singleton.type1;

/**
 * @Classname SingletonTest01
 * @Description  饿汉式(动态变量)
 * @Date 2021/3/17 9:26
 * @Created by dell
 */

public class SingletonTest01 {public static void main(String[] args) {Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        // 判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance 的哈希值" + instance.hashCode());
        System.out.println("intstance2 的哈希值" + instance2.hashCode());
    }
}

class Singleton {

    //1. 结构器私有化,内部能 new、private Singleton() {}

    // 本类外部创建对象实例
    private final static Singleton instance = new Singleton();

    // 对外部提供一个私有的静态方法
    public static Singleton getInstance() {return instance;}
}

<font color=’red’> 阐明:</font>

该形式在成员地位申明 Singleton 类型的动态变量,并创立 Singleton 类的对象 instance。instance 对象是随着类的加载而创立的。如果该对象足够大的话,而始终没有应用就会造成内存的节约。

动态代码块

package com.zhuang.singleton.type2;

/**
 * @Classname SingletonTest02
 * @Description  动态代码块
 * @Date 2021/3/17 9:35
 * @Created by dell
 */

public class SingletonTest02 {public static void main(String[] args) {Singleton2 instance = Singleton2.getInstance();
        Singleton2 instance2 = Singleton2.getInstance();
        // 判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance 的哈希值" + instance.hashCode());
        System.out.println("intstance2 的哈希值" + instance2.hashCode());
    }
}

class Singleton2 {

    //1. 结构器私有化,内部能 new、private Singleton2() {}

    // 本类外部创建对象实例
    private static Singleton2 instance;

    /*
    在动态代码块中创建对象
     */
    static {instance = new Singleton2();
    }

    // 对外部提供一个私有的静态方法
    public static Singleton2 getInstance() {return instance;}
}

<font color=’red’> 阐明:</font>

该形式在成员地位申明 Singleton 类型的动态变量,而对象的创立是在动态代码块中,也是对着类的加载而创立。所以和饿汉式的形式 1 基本上一样,** 当然该形式也存在内存节约问题。**

懒汉式 线程不平安

package com.zhuang.singleton.type3;

/**
 * @Classname SingletonTest03
 * @Description  懒汉式 线程不平安
 * @Date 2021/3/17 9:39
 * @Created by dell
 */

public class SingletonTest03 {public static void main(String[] args) {System.out.println("懒汉式,线程不平安!!!");
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        // 判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance 的哈希值" + instance.hashCode());
        System.out.println("intstance2 的哈希值" + instance2.hashCode());
    }
}

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // 提供一个动态的私有办法 当应用到该办法时,才去创立 instance
    public static Singleton getInstance() {if (instance == null) {instance = new Singleton();
        }
        return instance;
    }
}

<font color=’red’> 阐明:</font>

从下面代码咱们能够看出该形式在成员地位申明 Singleton 类型的动态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用 getInstance()办法获取 Singleton 类的对象的时候才创立 Singleton 类的对象,这样就实现了懒加载的成果。然而,如果是多线程环境,会呈现线程平安问题。

懒汉式(线程平安,同步办法)

package com.zhuang.singleton.type4;

/**
 * @Classname SingletonTest04
 * @Description  懒汉式(线程平安,同步办法)
 * @Date 2021/3/17 9:46
 * @Created by dell
 */

public class SingletonTest04 {public static void main(String[] args) {System.out.println("懒汉式,线程平安!!!");
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        // 判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance 的哈希值" + instance.hashCode());
        System.out.println("intstance2 的哈希值" + instance2.hashCode());
    }
}

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // 提供一个动态的私有办法,退出同步解决的代码,解决线程平安问题
    public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();
        }
        return instance;
    }
}

<font color=’red’> 阐明:</font>

该形式也实现了懒加载成果,同时又解决了线程平安问题。然而在 getInstance()办法上增加了 synchronized 关键字,导致该办法的执行成果特地低。从下面代码咱们能够看出,其实就是在初始化 instance 的时候才会呈现线程平安问题,一旦初始化实现就不存在了。

懒汉式(线程平安,同步代码块)

package com.zhuang.singleton.type5;

/**
 * @Classname SingletonTest05
 * @Description  懒汉式(线程平安,同步代码块)
 * @Date 2021/3/17 9:50
 * @Created by dell
 */

public class SingletonTest05 {public static void main(String[] args) {System.out.println("懒汉式,线程平安!, 同步代码块");
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        // 判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance 的哈希值" + instance.hashCode());
        System.out.println("intstance2 的哈希值" + instance2.hashCode());
    }
}

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // 提供一个动态的私有办法,退出同步解决的代码,解决线程平安问题
    public static synchronized Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {instance = new Singleton();
            }
        }
        return instance;
    }
}

双重查看锁模式是一种十分好的单例实现模式,解决了单例、性能、线程平安问题,下面的双重检测锁模式看上去白璧无瑕,其实是存在问题,在多线程的状况下,可能会呈现空指针问题,呈现问题的起因是 JVM 在实例化对象的时候会进行优化和指令重排序操作。

要解决双重查看锁模式带来空指针异样的问题,只须要应用 volatile 关键字, volatile 关键字能够保障可见性和有序性。

package com.zhuang.singleton.type6;

/**
 * @Classname SingletonTest06
 * @Description  双重查看,举荐应用
 * @Date 2021/3/17 9:54
 * @Created by dell
 */

public class SingletonTest06 {public static void main(String[] args) {System.out.println("懒汉式,双重查看,举荐应用");
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        // 判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance 的哈希值" + instance.hashCode());
        System.out.println("intstance2 的哈希值" + instance2.hashCode());
    }
}

class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    // 提供一个动态的私有办法,退出双重查看代码,退出同步解决的代码,解决懒加载的问题
    // 保障效率。举荐应用
    public static synchronized Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {instance = new Singleton();
            }
        }
        return instance;
    }
}

<font color=”red”> 小结:</font>

增加 volatile 关键字之后的双重查看锁模式是一种比拟好的单例实现模式,可能保障在多线程的状况下线程平安也不会有性能问题。

动态外部类实现单例模式!

package com.zhuang.singleton.type7;

/**
 * @Classname SingletonTest07
 * @Description  动态外部类实现单例模式!* @Date 2021/3/17 9:59
 * @Created by dell
 */

public class SingletonTest07 {public static void main(String[] args) {System.out.println("动态外部类实现单例模式");
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        // 判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance 的哈希值" + instance.hashCode());
        System.out.println("intstance2 的哈希值" + instance2.hashCode());
    }
}

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // 写一个动态外部类,该类中有个动态属性,Singleton
    public static class SingletonInstance {private static final Singleton INSTANCE = new Singleton();
    }

    // 提供一个动态的私有办法,间接返回 SingletonInstance.INSTANCE;
    public static synchronized Singleton getInstance() {return SingletonInstance.INSTANCE;}
}

<font color=’red’> 阐明:</font>

第一次加载 Singleton 类时不会去初始化 INSTANCE,只有第一次调用 getInstance,虚拟机加载 SingletonHolder

并初始化 INSTANCE,这样不仅能确保线程平安,也能保障 Singleton 类的唯一性。

<font color=”red”> 小结:</font>

动态外部类单例模式是一种优良的单例模式,是开源我的项目中比拟罕用的一种单例模式。在没有加任何锁的状况下,保障了多线程下的平安,并且没有任何性能影响和空间的节约。

枚举的形式实现单例模式

package com.zhuang.singleton.type8;

/**
 * @Classname SingletonTest08
 * @Description  枚举的形式实现单例模式
 * @Date 2021/3/17 10:06
 * @Created by dell
 */

public class SingletonTest08 {public static void main(String[] args) {System.out.println("枚举的形式实现单例模式,举荐应用");
        Singleton instance = Singleton.INSTANCE;
        Singleton instance2 = Singleton.INSTANCE;
        System.out.println(instance == instance2);
        // 判断是否为单例
        System.out.println(instance == instance2);
        System.out.println("intstance 的哈希值" + instance.hashCode());
        System.out.println("intstance2 的哈希值" + instance2.hashCode());
    }
}

/*
枚举
 */
enum Singleton {
    INSTANCE;// 属性

    public void method() {System.out.println("method()办法被调用...");
    }
}

阐明:

枚举形式属于恶汉式形式。

5.3 单例模式的利用场景

  • 须要频繁创立的一些类,应用单例能够升高零碎的内存压力,缩小 GC。
  • 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
  • 某些类创立实例时占用资源较多,或实例化耗时较长,且常常应用。
  • 某类须要频繁实例化,而创立的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  • 频繁拜访数据库或文件的对象。
  • 对于一些管制硬件级别的操作,或者从零碎上来讲该当是繁多管制逻辑的操作,如果有多个实例,则零碎会齐全乱套。
  • 当对象须要被共享的场合。因为单例模式只容许创立一个对象,共享该对象能够节俭内存,并放慢对象访问速度。如 Web 中的配置对象、数据库的连接池等。

5.4 存在的问题

毁坏单例模式:

使下面定义的单例类(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;}
    }

Test 类:

public class Test {public static void main(String[] args) throws Exception {
        // 往文件中写对象
        //writeObject2File();
        // 从文件中读取对象
        Singleton s1 = readObjectFromFile();
        Singleton s2 = readObjectFromFile();

        // 判断两个反序列化后的对象是否是同一个对象
        System.out.println(s1 == s2);
    }

    private static Singleton readObjectFromFile() throws Exception {
        // 创建对象输出流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\dell\\Desktop\\a.txt"));
        // 第一个读取 Singleton 对象
        Singleton instance = (Singleton) ois.readObject();

        return instance;
    }

    public static void writeObject2File() throws Exception {
        // 获取 Singleton 类的对象
        Singleton instance = Singleton.getInstance();
        // 创建对象输入流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\dell\\Desktop\\a.txt"));
        // 将 instance 对象写出到文件中
        oos.writeObject(instance);
    }
}

下面代码运行后果是false,表明序列化和反序列化曾经毁坏了单例设计模式。

  • 反射

    Singleton 类:

    public class Singleton {
    
        // 公有构造方法
        private Singleton() {}
        
        private static volatile Singleton instance;
    
        // 对外提供静态方法获取该对象
        public static Singleton getInstance() {if(instance != null) {return instance;}
    
            synchronized (Singleton.class) {if(instance != null) {return instance;}
                instance = new Singleton();
                return instance;
            }
        }
    }

Test 类:

public class Test {public static void main(String[] args) throws Exception {
        // 获取 Singleton 类的字节码对象
        Class clazz = Singleton.class;
        // 获取 Singleton 类的公有无参构造方法对象
        Constructor constructor = clazz.getDeclaredConstructor();
        // 勾销拜访查看
        constructor.setAccessible(true);

        // 创立 Singleton 类的对象 s1
        Singleton s1 = (Singleton) constructor.newInstance();
        // 创立 Singleton 类的对象 s2
        Singleton s2 = (Singleton) constructor.newInstance();

        // 判断通过反射创立的两个 Singleton 对象是否是同一个对象
        System.out.println(s1 == s2);
    }
}

下面代码运行后果是false,表明序列化和反序列化曾经毁坏了单例设计模式

<font color=”red”> 留神:</font> 枚举形式不会呈现这两个问题。

问题的解决

  • 序列化、反序列形式毁坏单例模式的解决办法

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

    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;}
        
        /**
         * 上面是为了解决序列化反序列化破解单例模式
         */
        private Object readResolve() {return SingletonHolder.INSTANCE;}
    }

源码解析:

ObjectInputStream 类

public final Object readObject() throws IOException, ClassNotFoundException{
    ...
    // if nested read, passHandle contains handle of enclosing object
    int outerHandle = passHandle;
    try {Object obj = readObject0(false);// 重点查看 readObject0 办法
    .....
}
    
private Object readObject0(boolean unshared) throws IOException {
    ...
    try {switch (tc) {
            ...
            case TC_OBJECT:
                return checkResolve(readOrdinaryObject(unshared));// 重点查看 readOrdinaryObject 办法
            ...
        }
    } finally {
        depth--;
        bin.setBlockDataMode(oldMode);
    }    
}
    
private Object readOrdinaryObject(boolean unshared) throws IOException {
    ...
    //isInstantiable 返回 true,执行 desc.newInstance(),通过反射创立新的单例类,obj = desc.isInstantiable() ? desc.newInstance() : null; 
    ...
    // 在 Singleton 类中增加 readResolve 办法后 desc.hasReadResolveMethod() 办法执行后果为 true
    if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
        // 通过反射调用 Singleton 类中的 readResolve 办法,将返回值赋值给 rep 变量
        // 这样屡次调用 ObjectInputStream 类中的 readObject 办法,继而就会调用咱们定义的 readResolve 办法,所以返回的是同一个对象。Object rep = desc.invokeReadResolve(obj);
         ...
    }
    return obj;
}
  • 反射形式破解单例的解决办法

    public class Singleton {
    
        // 公有构造方法
        private Singleton() {
            /*
               反射破解单例模式须要增加的代码
            */
            if(instance != null) {throw new RuntimeException();
            }
        }
        
        private static volatile Singleton instance;
    
        // 对外提供静态方法获取该对象
        public static Singleton getInstance() {if(instance != null) {return instance;}
    
            synchronized (Singleton.class) {if(instance != null) {return instance;}
                instance = new Singleton();
                return instance;
            }
        }
    }

<font color=”red”> 阐明:</font>

  这种形式比拟好了解。当通过反射形式调用构造方法进行创立创立时,间接抛异样。不运行此中操作。

5.5 JDK 源码解析 -Runtime 类

Runtime 类就是应用的单例设计模式。

  1. 通过源代码查看应用的是哪儿种单例模式

    public class Runtime {private static Runtime currentRuntime = new Runtime();
    
        /**
         * Returns the runtime object associated with the current Java application.
         * Most of the methods of class <code>Runtime</code> are instance
         * methods and must be invoked with respect to the current runtime object.
         *
         * @return  the <code>Runtime</code> object associated with the current
         *          Java application.
         */
        public static Runtime getRuntime() {return currentRuntime;}
    
        /** Don't let anyone else instantiate this class */
        private Runtime() {}
        ...
    }

从下面源代码中能够看出 Runtime 类应用的是恶汉式(动态属性)形式来实现单例模式的。

  1. 应用 Runtime 类中的办法

    public class RuntimeDemo {public static void main(String[] args) throws IOException {
            // 获取 Runtime 类对象
            Runtime runtime = Runtime.getRuntime();
    
            // 返回 Java 虚拟机中的内存总量。System.out.println(runtime.totalMemory());
            // 返回 Java 虚拟机试图应用的最大内存量。System.out.println(runtime.maxMemory());
    
            // 创立一个新的过程执行指定的字符串命令,返回过程对象
            Process process = runtime.exec("ipconfig");
            // 获取命令执行后的后果,通过输出流获取
            InputStream inputStream = process.getInputStream();
            byte[] arr = new byte[1024 * 1024* 100];
            int b = inputStream.read(arr);
            System.out.println(new String(arr,0,b,"gbk"));
        }
    }

写在最初

  • 如果我的文章对你有用,请给我点个👍,感激你😊!
  • 有问题,欢送在评论区指出!💪
正文完
 0