(二)构建型模式-单例模式

itmahy
itmahy
发布于 2024-01-19 / 28 阅读
0
0

(二)构建型模式-单例模式

单例模式

当某个对象全局只需要一个实例时,就可以使用单例模式。

特点:

  • 能够避免对象的重复创建,节约空间并提升效率。

  • 避免由于操作不同实例从而导致逻辑错误。

单例模式总共有两种: 饿汉式懒汉式

1. 饿汉式

饿汉式是在类加载的时候就会创建一个唯一实例,不管后期是否使用该实例都会创建。

  • 代码

    /**
     * 匿名内部类- 单例模式-饿汉
     *
     */
    public static class Hungry{
    ​
        private static Hungry hungry = new Hungry();
    ​
        private Hungry() {}
    ​
        public static Hungry getInstance(){
            return hungry;
        }
    ​
        public String getAddress(String address){
            return address;
        }
    }
  • 测试代码

    public static void main(String[] args) {
        Hungry instance = Hungry.getInstance();
        String address = instance.getAddress("这是一个测试的地址");
        System.out.println(address);
        for (int i = 0; i < 100; i++){
            System.out.println("对象:" + instance);
        }
    }
  • 输出

这里截取了部分。

可以看出,我们把Hungry的构造方法修饰符修改为private,使得其他类中无法实例化Hungry这个类,必须通过getInstance()这个方法获得唯一的实例。但是饿汉模式有一个弊端,就是即便Hungry这个类不需要使用,也会在类加载的时候被创建出来,占用一块内存,并增加类初始化的时间。他就像一个修理工修理电器一样,不管有些工具用不用的上,他都会拿出所有的工具,就像一个饿汉,所以称之为饿汉式。

2. 懒汉式

先声明一个空变量,需要用时才初始化

2.1 不加锁

  • 代码

    /**
     * 匿名内部类-懒汉式
     */
    public static class Slacker{
        private static Slacker slacker = null;
    ​
        private Slacker(){}
    ​
        public static Slacker getInstance(){
            if (slacker == null){
                slacker = new Slacker();
            }
            return slacker;
        }
        public String getAddress(String address){
            return address;
        }
    }
  • 测试代码

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                Slacker instance = Slacker.getInstance();
                String address = instance.getAddress("懒汉式的地址");
                System.out.println(address);
                for (int i=0; i<100; i++){
                    System.out.println(instance);
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                Slacker instance = Slacker.getInstance();
                String address = instance.getAddress("懒汉式的地址");
                System.out.println(address);
                for (int i=0; i<100; i++){
                    System.out.println(instance);
                }
            }
        });
    ​
        thread.start();
        thread1.start();
    }
  • 输出

从上面的结果可以看出,上面的代码不是线程安全的,如果多个线程同时调用getInstance()方法,就会别实例化很多次,为了线程安全,必须给判空过程加锁。

2.2 加锁

  • 代码

    /**
     * 匿名内部类-懒汉式
     */
    public static class Slacker{
        private static Slacker slacker = null;
    ​
        private Slacker(){}
    ​
        public static Slacker getInstance(){
            synchronized (Slacker.class){
                if (slacker == null){
                    slacker = new Slacker();
                }
            }
            return slacker;
        }
        public String getAddress(String address){
            return address;
        }
    ​
    }

    这样就会在多个线程调用时,一次最多只有一个线程能够执行非空判断并实例化的操作

  • 测试代码同上

  • 输出

从结果可以看出,实例化的对象都是同一个,但是每次都需要执行synchronized()同步化方法,这样会严重影响执行效率,所以在同步化之前,再加一层非空判断。

2.3 再加一层非空判断

  • 代码

    /**
     * 匿名内部类-懒汉式
     */
    public static class Slacker{
        private static Slacker slacker = null;
    ​
        private Slacker(){}
    ​
        public static Slacker getInstance(){
            if (slacker == null){
                synchronized (Slacker.class){
                    if (slacker == null){
                        slacker = new Slacker();
                    }
                }
            }
            return slacker;
        }
        public String getAddress(String address){
            return address;
        }
    ​
    }
  • 测试代码同上

  • 输出

所以2.3 是效率较高的懒汉单例模式,被称之为 双检锁方式

除了双检锁方式外,还有一种比较常见的静态内部类方式保证懒汉式单例的线程安全。

2.4 静态内部类方式

  • 代码

    /**
     * 匿名内部类-懒汉式-静态内部类方式保证懒汉式单例的线程安全
     */
    public static class Slacker1{
    ​
        public static class SlackerHandler{
            public static Slacker1 slacker1 = new Slacker1();
        }
    ​
        private Slacker1(){}
    ​
        public static Slacker1 getInstance(){
            return SlackerHandler.slacker1;
        }
    ​
        public String getAddress(String address){
            return address;
        }
    }
  • 测试代码

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                Slacker1 instance = Slacker1.getInstance();
                String address = instance.getAddress("静态内部类方式懒汉式的地址");
                System.out.println(address);
                for (int i=0; i<100; i++){
                    System.out.println(instance);
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                Slacker1 instance = Slacker1.getInstance();
                String address = instance.getAddress("静态内部类方式懒汉式的地址");
                System.out.println(address);
                for (int i=0; i<100; i++){
                    System.out.println(instance);
                }
            }
        });
    ​
        thread.start();
        thread1.start();
    }
  • 输出结果

虽然我们经常使用这种静态内部类的懒加载方式,但其中的原理不一定每个人都清楚。接下来我们便来分析其原理,搞清楚两个问题:

  • 静态内部类方式是怎么实现懒加载的

    • Java 类的加载过程包括:加载、验证、准备、解析、初始化。初始化阶段即执行类的 clinit方法(clinit= class + initialize),包括为类的静态变量赋初始值和执行静态代码块中的内容。但不会立即加载内部类,内部类会在使用时才加载。,所以当此 Slacker1类加载时,SlackerHandler并不会被立即加载,所以不会像饿汉式那样占用内存。

    • 另外,Java 虚拟机规定,当访问一个类的静态字段时,如果该类尚未初始化,则立即初始化此类。当调用Slacker1getInstance方法时,由于其使用了 SlackerHandler的静态变量 slacker1,所以这时才会去初始化 SlackerHandler,在 SlackerHandler中 new 出 Slacker1对象。这就实现了懒加载。

  • 静态内部类方式是怎么保证线程安全的

    • Java 虚拟机的设计是非常稳定的,早已经考虑到了多线程并发执行的情况。虚拟机在加载类的 clinit方法时,会保证 clinit在多线程中被正确的加锁、同步。即使有多个线程同时去初始化一个类,一次也只有一个线程可以执行 clinit方法,其他线程都需要阻塞等待,从而保证了线程安全。

一般的建议是:对于构建不复杂,加载完成后会立即使用的单例对象,推荐使用饿汉式。对于构建过程耗时较长,并不是所有使用此类都会用到的单例对象,推荐使用懒汉式。


评论