单例模式

单例模式的应用场景就不再叙述了,相信网上一找一大把。主要说一说单例模式的线程安全问题。单例模式主要分为两种实现:饿汉模式和懒汉模式。

饿汉模式

饿汉模式是指,项目一旦被启动,该类就会被加载完成,后续不在需要创建,整个项目的运行过程中就只有一个实例。实现如下所示:

public class Singleton {

    private Singleton () {
        System.out.println("我被创建了!");
    }

    /**
     * 饿汉模式,类加载时就会创建实例
     */
    private static Singleton instance = new Singleton();

    @PostConstruct
    public void init() {
        System.out.println("啥时候");
    }

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

该实例在类被加载时就已经创建,后续使用的都是同一个实例,不会再次创建,所以是线程安全的。也就是说单例模式的饿汉模式天然就不存在线程安全问题。

懒汉模式

懒汉模式与饿汉模式的最大区别就是,懒汉模式是在该类需要被使用的时候才会创建实例对象,而不是系统启动时加载类就创建。实现如下所示:

public class LazySingleton {

    private LazySingleton() {
        //看看是否会多次创建
        System.out.println("我被创建了!");
    }

    /**
     * 懒汉模式,使用时才会创建实例
     */
    private static LazySingleton instance = null;

    //线程安全
    public static LazySingleton getInstance1() {
        try {
            //通过休眠,模拟并发情况
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                    return instance;
                }
            }
        }
        return instance;
    }

    //线程不安全
    public static LazySingleton getInstance2() {
        try {
            //通过休眠,模拟并发情况
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (instance == null) {
            instance = new LazySingleton();
            return instance;
        }
        return instance;
    }
}

如代码中所示,getInstance2 就是正常情况下的懒汉模式实现,我们通过休眠来模拟并发情况,在单元测试时,发现该类被实例化了2次,说明这种实现在单线程是没问题的,但是在多线程时就会发生线程安全问题。

@Test
public void singleTest() {
    for (int i =0; i<100; i++) {
        new Thread(LazySingleton::getInstance2).start();
        new Thread(LazySingleton::getInstance2).start();
        new Thread(LazySingleton::getInstance2).start();
        new Thread(LazySingleton::getInstance2).start();
        new Thread(LazySingleton::getInstance2).start();
        new Thread(LazySingleton::getInstance2).start();
        new Thread(LazySingleton::getInstance2).start();
        new Thread(LazySingleton::getInstance2).start();
        new Thread(LazySingleton::getInstance2).start();
        new Thread(LazySingleton::getInstance2).start();
    }
}

运行结果如下图所示:
image.png
此时,为确保懒汉模式情况下避免发生线程安全问题,我们就需要使用到同步锁,如代码中getInstance1 所示,我们为实例化的操作进行加锁,同时进行double check 操作,以避免多次实例化。当然避免线程安全问题的一种解决方法,还有其他方法,就不在此赘述。在加上同步锁之后,我们再进行单元测试时,发现果然只创建了一次实例,完美地解决了线程安全问题。

@Test
public void singleTest() {
    for (int i =0; i<100; i++) {
        new Thread(LazySingleton::getInstance1).start();
        new Thread(LazySingleton::getInstance1).start();
        new Thread(LazySingleton::getInstance1).start();
        new Thread(LazySingleton::getInstance1).start();
        new Thread(LazySingleton::getInstance1).start();
        new Thread(LazySingleton::getInstance1).start();
        new Thread(LazySingleton::getInstance1).start();
        new Thread(LazySingleton::getInstance1).start();
        new Thread(LazySingleton::getInstance1).start();
        new Thread(LazySingleton::getInstance1).start();
    }
}

运行结果如下图所示:
image.png

总结

饿汉模式:因为在类加载是就已经创建了实例,后续不需要在创建实例,天然地避开了线程安全问题。
懒汉模式:因为是在使用时才进行实例化操作,所以存在着线程安全问题,此时需要通过加同步锁来确保多线程安全,避免多次实例化。


lxKLj
0 声望0 粉丝

学无止境,笨鸟先飞