设计模式五----单例模式

312 阅读5分钟

这是我参与更文挑战的第17天,活动详情查看: 更文挑战

设计模式

单例模式

就是整个软件系统中一个类只有一个实例

  • 饿汉式(静态常量)
  • 饿汉式(静态代码块)
  • 懒汉式(线程不安全)
  • 懒汉式(线程安全,同步方法)
  • 懒汉式(线程安全,同步代码块)
  • 双重检验锁
  • 静态内部类
  • 枚举

饿汉式(静态常量)

package com.wangscaler.singleton;

/**
 * @author wangscaler
 * @date 2021.06.18 11:18
 */

public class Hungryman {
    public static void main(String[] args) {
        StatiConst statiConst1 = StatiConst.getInstance();
        StatiConst statiConst2 = StatiConst.getInstance();
        System.out.println(statiConst2.hashCode());
        System.out.println(statiConst1.hashCode());
    }

    static class StatiConst {
        //私有化之后,外部不可new
        private StatiConst() {
        }

        private final static StatiConst instance = new StatiConst();

        public static StatiConst getInstance() {
            return instance;
        }
    }
}

总结: 构建饿汉式需要注意

  • 1、构造器私有化private StatiConst() { }
  • 2、类的内部创建对象 private final static StatiConst instance = new StatiConst();
  • 3、向外暴露静态的公共方法public static StatiConst getInstance() { return instance;}
  • 4、这种方式会在类装载的时候完成实例化,所以全局通过getInstance拿到的实例对象,永远都是一个,这种方式是线程安全的。
  • 如果项目中不使用这个实例,就会造成内存的浪费。

饿汉式(静态代码块)

package com.wangscaler.singleton;

/**
 * @author wangscaler
 * @date 2021.06.18 11:18
 */

public class Hungryman1 {
    public static void main(String[] args) {
        StatiCodeBlock statiConst1 = StatiCodeBlock.getInstance();
        StatiCodeBlock statiConst2 = StatiCodeBlock.getInstance();
        System.out.println(statiConst2.hashCode());
        System.out.println(statiConst1.hashCode());
    }

    static class StatiCodeBlock {
        //私有化之后,外部不可new
        private StatiCodeBlock() {

        }

        static {
            instance = new StatiCodeBlock();
        }

        private static StatiCodeBlock instance;

        public static StatiCodeBlock getInstance() {
            return instance;
        }
    }
}

这种方式创建和静态常量的方式是一样的。

懒汉式(线程不安全)

package com.wangscaler.singleton;

/**
 * @author wangscaler
 * @date 2021.06.18 14:06
 */
public class Lazyman {
    public static void main(String[] args) {
        Unsafe instance = Unsafe.getInstance();
        Unsafe instance2 = Unsafe.getInstance();
        System.out.println(instance.hashCode());
        System.out.println(instance2.hashCode());
        System.out.println(instance == instance2);
    }

    static class Unsafe {
        private static Unsafe instance;

        private Unsafe() {
        }

        public static Unsafe getInstance() {
            if (instance == null) {
                instance = new Unsafe();
            }
            return instance;
        }
    }
}

在单线程中这种方式只有在第一次用到的时候才会创建这个对象。当已经创建过之后,就会返回之前创建的对象。然而在多线程中就有可能初始化出来多个实例,所以说这种方法是线程不安全的,在实际的开发中,切记不要使用这种方式。

懒汉式(线程安全,同步方法)

package com.wangscaler.singleton;

/**
 * @author wangscaler
 * @date 2021.06.18 14:06
 */
public class Lazyman1 {
    public static void main(String[] args) {
        Synchronizationmethod instance = Synchronizationmethod.getInstance();
        Synchronizationmethod instance2 = Synchronizationmethod.getInstance();
        System.out.println(instance.hashCode());
        System.out.println(instance2.hashCode());
        System.out.println(instance == instance2);
    }

    static class Synchronizationmethod {
        private static Synchronizationmethod instance;

        private Synchronizationmethod() {
        }

        public static synchronized Synchronizationmethod getInstance() {
            if (instance == null) {
                instance = new Synchronizationmethod();
            }
            return instance;
        }
    }
}

将getInstance方法变成同步的方法,每个线程进来就会阻塞,必须等待上一个线程访问结束之后才能继续访问,效率太低。

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

package com.wangscaler.singleton;

import sun.misc.JavaAWTAccess;

/**
 * @author wangscaler
 * @date 2021.06.18 14:06
 */
public class Lazyman2 {
    public static void main(String[] args) {
        Synchronizationcodeblock instance = Synchronizationcodeblock.getInstance();
        Synchronizationcodeblock instance2 = Synchronizationcodeblock.getInstance();
        System.out.println(instance.hashCode());
        System.out.println(instance2.hashCode());
        System.out.println(instance == instance2);
    }

    static class Synchronizationcodeblock {
        private static Synchronizationcodeblock instance;

        private Synchronizationcodeblock() {
        }

        public static Synchronizationcodeblock getInstance() {
            if (instance == null) {
                synchronized (Lazyman2.class) {
                    instance = new Synchronizationcodeblock();
                }
            }
            return instance;
        }
    }
}

将同步机制放到代码块,这种方式其实和懒汉式(线程不安全)是一样的,起不到同步的作用,还是会产生多个实例,所以也是没办法使用。

双重检验锁

package com.wangscaler.singleton;

public class Doublechecklock {
    public static void main(String[] args) {
        Doublecheck doublecheck = Doublecheck.getInstance();
        Doublecheck doublecheck1 = Doublecheck.getInstance();
        System.out.println(doublecheck.hashCode());
        System.out.println(doublecheck1.hashCode());
        System.out.println(doublecheck == doublecheck1);
    }

    static class Doublecheck {
        private static volatile Doublecheck instance;

        private Doublecheck() {
        }

        public static Doublecheck getInstance() {
            if (instance == null) {
                synchronized (Doublecheck.class) {
                    if (instance == null) {
                        instance = new Doublecheck();
                    }
                }
            }
            return instance;
        }
    }
}

此时synchronized (Doublecheck.class)可以认为一个门的锁,第一个if (instance == null) 可以认为门前的检查哨,第二个就是门后的检查哨,这个双重检验锁的名字起的是相当准确啊。假如有三个线程通过第一个检查哨,此时当第一个线程拿到钥匙打开门,剩下的两个只能等待,当第一个线程通过第二个检查哨把对象创建出来,并刷新到内存,剩下的线程进来,就无法通过第二个检查哨

可以看到这里使用了一个关键字volatile,这个关键字可以禁止指令重排,这个关键字在java5之前是有问题的。

在这里instance = new Doublecheck();其实会产生三个操作。

1、给instance分配内存

2、调用构造函数初始化变量

3、将该对象指向为其分配的内存空间

这三个操作会发生指令重排的结果,即1-2-3/1-3-2。

  • 那么什么是指令重排呢?

指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

简言之,代码的运行不一定是按照编码的顺序,但是它可以根据指令之间的数据依赖性来将没有依赖的指令重排保证结果的一致性的同时提高效率。

如果发生指令重排,即先执行3在执行2的情况,此时执行完3之后,新的线程就会进来,此时2还没有执行,那么新线程拿到的对象(执行完3,此时的对象就不是null),就会有问题。

  • 为什么出现这个情况?

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

而使用了volatile之后,不仅会阻止指令重排,还会将修改的值立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

参考资料