【多线程案例】单例模式(懒汉模式和饿汉模式)

1. 什么是单例模式?

提起单例模式,就必须介绍设计模式,而设计模式就是在软件设计中,针对特殊问题提出的解决方案。它是多年来针对一些常见的问题的解决方法,具有良好的可复用性、可扩展性和可维护性。
标准的设计模式有23种,单例模式就是最常见的一种,其目的是确认一个类只有一个实例对象,并且可以提供一个全局访问点来访问该实例。
单例就是指一个类的实例只有一个,即该类的所有对象都指向用同一个实例。
而多数单例模式并没有结合多线程,在多线程环境下运行有时会出现线程安全问题,所以下面不仅介绍如何实现单例模式,还有单例模式结合多线程使用时的相关知识。

2. 立即加载/“饿汉模式”

立即加载一般还被称作饿汉模式,根据立即,饿汉可以看出十分的急,所以在饿汉模式中,这样单例中的唯一实例对象就被创建。

创建MyObject.java代码如下:

//单例模式、饿汉模式
public class MyObject {
    //进行封装,防止创建新的对象
    private static MyObject object = new MyObject();
    private MyObject(){};
    //通过这个方法获得对象
    public static MyObject getObject(){
        return object;
    }
}

创建线程类MyThread.java:

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(MyObject.getObject().hashCode());
    }
}

创建运行类Run.java:

public class Run {
    //测试单例模式对象是同一个
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:
运行1
运行结果相同,说明对象是同一个,成功实现了立即加载型单例设计模式。

3. 延时加载/“懒汉模式”

延时,懒汉可以看出代码并不着急,所以懒汉模式型单例模式中的对象并不像饿汉模式中没有调用前就创建完成,而是在第一调用方法实例时才被创建。
对比饿汉模式:
优点:会减少内存资源浪费。
缺点:多线程环境并发运行,可能会出现线程安全。

3.1 第一版

创建类MyObjectLazy.java,代码如下:

public class MyObjectLazy {
    //单例模式、懒汉模式
    private static MyObjectLazy myObjectLazy = null;
    private static Object lock = new Object();
    private MyObjectLazy(){};
    public static MyObjectLazy getMyObjectLazy(){
        if(myObjectLazy != null){
            return myObjectLazy;
        }else{
            myObjectLazy = new MyObjectLazy();
        }
        return myObjectLazy;
    }
 }

创建线程类MyThreadLazy.java:

public class MyThreadLazy extends Thread{
    @Override
    public void run() {
        System.out.println(MyObjectLazy.getMyObjectLazy().hashCode());
    }
}

创建运行类RunLazy.java:

public class RunLazy {
    //测试对象是不是同一个,是的话就是安全的单例模式
    public static void main(String[] args) {
        MyThreadLazy t1 = new MyThreadLazy();
        MyThreadLazy t2 = new MyThreadLazy();
        MyThreadLazy t3 = new MyThreadLazy();
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:
1
结果不同,所以并不是单例模式,其中有问题,造成线程不安全。

3.2 第二版

第一版生成不同对象,所以造成非线程安全,我们可以做出一点修改,对代码加上锁。
修改后的MyObjectLazy.java:

    public static MyObjectLazy getMyObjectLazy(){
        synchronized (lock){
            if(myObjectLazy == null){
                myObjectLazy = new MyObjectLazy();
            }
        }
        return myObjectLazy;
    }

运行结果:
运行3
说明这个单例模式是正确实现了。

3.3 第三版

但是第二版又暴露一个问题,上面加锁后相当于整个方法都加锁,上面一个线程没有释放锁,下一个线程将无法运行,造成效率低下。
所以我们继续修改,修改后的MyObjectLazy.java:

    public static MyObjectLazy getMyObjectLazy(){
        try{
            if(myObjectLazy == null){
                Thread.sleep(1000);
                synchronized (lock){
                    myObjectLazy = new MyObjectLazy();
                }
            }
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        return myObjectLazy;
    }

运行结果:
3
运行结果又不同了,创建出了三个对象,为什么?这是因为虽然上了锁,但是if已经判断,只是new对象时串行。
虽然效率提高了,但这并不是单例模式。

3.4 第四版

我们可以使用DCL双检查锁机制来实现,进行两次if判断,使线程安全。
修改后MyObjectLazy.java:

    //再一次修改代码,加锁只加一块,并且应用DCL双检查机制来实现多线程环境下的延时加载单例模式,保证线程安全
    public static MyObjectLazy getMyObjectLazy(){
        try{
            if(myObjectLazy == null){
                Thread.sleep(1000);
                synchronized (lock){
                    if(myObjectLazy == null) {
                        myObjectLazy = new MyObjectLazy();
                    }
                }
            }
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        return myObjectLazy;
    }

运行结果:
4
使用双检查锁功能,成功解决了懒汉模式遇到多线程的问题。DCL经常出现在此场景下,我们要学会应用。