程序员该如何管理后宫:朕只爱一个皇后!(单例模式)

157 阅读6分钟

单例模式的应用场景

单例模式 重点在保证 一个类在任何情况下都只有一个 实例。例举生活中的例子,如公司 的CEO 只有一个,我的❤只有一个,spring容器只有一个,朕的后宫只有一个皇后。

饿汉模式

​第一种写法:

package com.singleton.pattern.singleton.demo;

/**
 * @Classname HungrySingleton
 * @Description 饿汉模式
 * @Date 2019/11/20 0020 15:23
 * @Created by 埔枘
 */
public class HungrySingleton {

    private static HungrySingleton hungrySingleton = new HungrySingleton();

    /**
     * 私有化 空构造
     */
    private HungrySingleton(){}

    public static HungrySingleton getSingleton(){
        return hungrySingleton;
    }
}

第二种写法:

package com.singleton.pattern.singleton.demo;

/**
 * @Classname HungrySingleton
 * @Description 饿汉模式(静态加载)
 * @Date 2019/11/20 0020 15:23
 * @Created by 埔枘
 */
public class HungrySingleton2 {

    private static final HungrySingleton2 hungrySingleton;

    static{
        hungrySingleton = new HungrySingleton2();
    }
    /**
     * 私有化 空构造
     */
    private HungrySingleton2(){}

    public static HungrySingleton2 getSingleton(){
        return hungrySingleton;
    }
}

以上两种写法 在容器加载时 创了对象。

饿汉式适用在单例对象较少的情况。(单例对象多的情况下,在初始化时加载过多的对象,影响性能。)

懒汉模式

单线程情况下以下代码没什么问题,但是换成多线程试试?

package com.singleton.pattern.singleton.demo;

/**
 * @Classname LazySingleton
 * @Description 懒汉模式(即懒加载)
 * @Date 2019/11/20 0020 15:30
 * @Created by 埔枘
 */
public class LazySingleton {

    private LazySingleton(){}

    public static LazySingleton lazySingleton = null;

    /**
     * 外部调用时 才会加载
     * @return
     */
    public static LazySingleton getSingleton(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

新建一个线程类,并在里面获取了 懒加载的单例,打印当前线程及获取的单例对象。

package com.singleton.pattern.singleton.demo;

/**
 * @Classname ExectorThread
 * @Description TODO
 * @Date 2019/11/20 0020 15:36
 * @Created by 埔枘
 */
public class ExectorThread implements Runnable{
    @Override
    public void run() {
        LazySingleton singleton = LazySingleton.getSingleton();
        System.out.println(Thread.currentThread().getId()+"---"+Thread.currentThread().getName()+"----"+singleton);
    }
}

情况如下 获取到了不一样的对象,意味着 这里存在线程的安全问题

在通过 synchronized 关键字同步,解决线程安全问题

我们来 多线程 debug 下

设置多线程 debug

两个线程同时进入 getSingleton 方法 发现如下 Thread-1 进入了 MONITOR (监视状态)状态

当 Thread-0 执行完后 发现 Thread-1 进入了 RUNNING 状态

通过 synchronized 保证了多线程情况下 只有一个线程可以进入 被synchronized 修饰的方法中,保证了 懒汉单例 在多线程情况下的安全。

但是???但是???但是???但是???但是???但是???但是???但是???但是???但是???但是?

在线程数量过多的情况下 synchronized 会使 CPU 的压力增大,那么有没有更好的办法来解决这个问题呢?

双重检查锁单例

**​双重检查,减少触发 synchronized 同步的操作,提高性能。

既节约了空间又保证了线程的安全性,但是 由于jvm 的 存在乱序执行功能,还是会导致 线程安全性问题,造成不可靠的原因是编译器为了提高执行效率的指令重排。只要认为在单线程下是没问题的,它就可以进行乱序写入!以保证不要让cpu指令流水线中断,这个就是著名的DCL失效问题。**

​先说一个概念:

new LazyDoubleCheckSingleton()

new 一个对象,

1.第一步会先在堆内存中开辟一个空间

2.初始化对象 在 堆空间

3.把栈内存中的引用指向 第一步开辟的空间地址

以上步骤(只要认为在单线程下是没问题的) jvm 乱序执行 会导致 步骤变为 1,3,2

举例:

线程A,线程B

线程A jvm 乱序执行导致 对象还没初始化 就把引用指向了 堆中的地址,然后线程B 进来判断

A:标识 处(看下面代码) 不为空 就直接返回了 就会导致异常。

ps:在JDK1.5之后,官方也发现了这个问题,故而具体化了volatile

package com.singleton.pattern.singleton.demo;

/**
 * @Classname LazySingleton
 * @Description 双重检查锁单例
 * @Date 2019/11/20 0020 15:30
 * @Created by 埔枘
 */
public class LazyDoubleCheckSingleton {

    private LazyDoubleCheckSingleton(){}

    public static LazyDoubleCheckSingleton lazySingleton = null;

    /**
     * 外部调用时 才会加载
     * @return
     */
    public static synchronized LazyDoubleCheckSingleton getSingleton(){
        if(lazySingleton == null){    A:标识
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazySingleton == null){
                    lazySingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazySingleton;
    }
}

那么怎么解决 jvm 乱序带来的问题呢?

方案:

1.使用volatile 关键字 禁止jvm 重排序

package com.singleton.pattern.singleton.demo;

/**
 * @Classname LazySingleton
 * @Description 双重检查锁单例
 * @Date 2019/11/20 0020 15:30
 * @Created by 埔枘
 */
public class LazyDoubleCheckSingleton {

    private LazyDoubleCheckSingleton(){}

    public static volatile LazyDoubleCheckSingleton lazySingleton = null;

    /**
     * 外部调用时 才会加载
     * @return
     */
    public static LazyDoubleCheckSingleton getSingleton(){
        if(lazySingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazySingleton == null){
                    lazySingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazySingleton;
    }
}

2.屏蔽 多线程之间的 乱序可视化( 使线程A 的乱序 在线程B 中看不到)

   从下面的ClassLoader中可以看出 只有 jvm 加载类 时才会是 一个线程 去加载类  
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

所以最好的方式是 在jvm 加载时去加载这个类,但是不能使用 饿汉模式。

ps:只要用到了 synchronized 锁 则一定会影响到性能。

往下看

单例-静态内部类

静态内部类的加载时机在 getSingleton() 第一次执行时

内部类会在 方法调用之钱初始化 巧妙的避开了 线程安全问题。

ps:此模式兼顾了 饿汉的内存浪费 和 synchronized 的性能问题

package com.singleton.pattern.singleton.demo;

/**
 * @Classname LazyInnerClassSingleton
 * @Description 单例 静态内部类实现
 * @Date 2019/11/20 0020 17:03
 * @Created by 埔枘
 */
public class LazyInnerClassSingleton {

    private LazyInnerClassSingleton(){}

    public static LazyInnerClassSingleton lazySingleton = null;
    /**
        * final 关键字 保证方法不会被 重写和重载
        * @return
    */
    public static final LazyInnerClassSingleton getSingleton(){
        return LazyHolder.LAZY;
    }

    //默认不加载
    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

注册式单例

注册式单例:

描述:注册式单例 又叫登记式的单例,就是把实例登记到 一个地方,然后通过 一个唯一标识 获取。实现方式分为如下两种:

1.枚举单例

2.容器单例

枚举单例

package com.singleton.pattern.singleton.demo;
public enum EnumSingleton {
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
}

    public void setData(Object data) {
        this.data = data;
}

    public static EnumSingleton getInstance(){
        return INSTANCE;
}
}

容器式单例

ps:容器式写法适用于 实力非常多的情况。
package com.singleton.pattern.singleton.demo;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 *  容器式写法适用于 实例非常多的情况
*/
public class ContainerSingleton {
    /**
     *  私有化构造
*/
private ContainerSingleton(){}

    /**
     *  容器
*/
private static Map<String,Object> ioc = new ConcurrentHashMap<>();
    public static Object getBean(String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        //锁 ioc
synchronized (ioc){
            if(!ioc.containsKey(className)){
                Class<?> aClass = Class.forName(className);
Object newInstance = aClass.newInstance();
ioc.put(className,newInstance);
                return newInstance;
}else{
                return ioc.get(className);
}
        }
    }
}

ThreadLocal 单例

这是一个伪单例,它并不能实现全局的实例唯一,只能实现在某个线程中的实例唯一,并且天生式线程安全的。
package com.singleton.pattern.singleton.demo;
/**
 *  在 某个线程中的 单例 (伪单例)
 *  并且 天生是线程安全的
 */
public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstanceThreadLocal
            = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };
    private ThreadLocalSingleton(){

    }
    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstanceThreadLocal.get();
    }
}

总结

饿汉单例(预先加载):

1.静态属性实现

2.静态代码块实现

懒汉单例(懒加载):

1.正常懒汉单例( 需要加synchronized,但影响性能 )

2.双重检查单例( 第一层非 null 判断 减少 synchronized 锁的使用,提高性能,JVM乱序问题)

3.静态内部类

注册式单例:

1.枚举单例(推荐使用)

2.容器单例

伪单例:

1.ThreadLocal单例

参考:

www.jianshu.com/p/3aefe866b…

blog.csdn.net/mnb65482/ar…

下期介绍如何破坏单例