java设计模式--单例模式---线程安全的懒汉式

设计模式是一套被反复使用、多数人知晓的经过分类编目的、代码设计经验的总结。
使用设计模式是为了可重用代码、让代码更容易被他人理解,保证代码可靠性。

在开发过程中,有些对象,我们只需要一个,比如:配置文件、工具类、线程池、缓存、日志对象等。这些对象如果被创造出了多个实例,就会导致许多问题,例如占用过多资源,读写文件不一致等。
怎么保证整个应用中某个实例有且只有一个?—-单例模式
单例模式的饿汉式实现

public class Singleton {
    //1.构造方法私有化,不允许外部直接创建对象
    private Singleton(){}
    //2.创建类的唯一实例---该类只要一加载就会创造一个唯一实例---饿汉式实现
    private static Singleton instance = new Singleton();
    //3.提供一个用于获取实例的方法
    public static Singleton getInstance() {
        return instance;
    }
}

饿汉式在类加载的过程中就会创建一个本类的静态对象,所以它的加载过程比懒汉式慢,但是获得类实例的过程比懒汉式快,并且它在多线程模式下比较安全。缺点在于没有调用方法前就加载了,比较占用内存。
单例模式的懒汉式实现

public class SingletonII {
    private SingletonII() {
    }

    private static SingletonII instance;

    public static SingletonII getInstance() {
        if (instance == null) {//1:读取instance的值
            instance = new SingletonII();//2: 实例化instance
        }
        return instance;
    }
}
测试类:
/**
 * Created by dell on 2018/4/15.
 * 饿汉模式加载类时比较慢,运行时,获取对象的速度比较快。因为加载类时已经创建了类的唯一实例,线程安全。
 * 懒汉式加载类时比较快,运行时,获取对象的速度比较慢,而且有可能线程不安全。
 */
public class SingleTest {

    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        if (s1 == s2) {
            System.out.println("s1和s2指向同一个实例");
        } else {
            System.out.println("s1和s2指向不同的实例");
        }
        SingletonII s3 = SingletonII.getInstance();
        SingletonII s4 = SingletonII.getInstance();
        if (s3 == s4) {
            System.out.println("s3和s4指向同一个实例");
        } else {
            System.out.println("s3和s4指向不同的实例");
        }

        //检查懒汉式线程安全问题
        //newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
        ExecutorService threadPool = Executors.newFixedThreadPool(1000);
        for (int i = 0; i < 1000; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+":"+SingletonII.getInstance());
                }
            });
        }
    }
}

对于SingletonII代码注释部分,如果此时有两个线程,线程A执行到1处,读取了instance为null,然后cpu就被线程B抢去了,此时,线程A还没有对instance进行实例化。因此,线程B读取instance时仍然为null,于是,它对instance进行实例化了。然后,cpu就被线程A抢去了。此时,线程A由于已经读取了instance的值并且认为它为null,所以,再次对instance进行实例化。所以,线程A和线程B返回的不是同一个实例。
引发了单例模式的线程不安全问题。
解决方式:用synchronized修饰获取实例的方法

/**
 * Created by dell on 2018/4/15.
 * 单例模式的懒汉式实现
 */
public class SingletonII {
    private SingletonII() {
    }

    private static volatile SingletonII instance;
    //双重检查加锁机制
    public static synchronized SingletonII getInstance() {
        if (instance == null) {
            synchronized (SingletonII.class) {
                if (instance == null) {
                    instance = new SingletonII();
                }
            }
        }
        return instance;
    }
}

双重加锁机制配合volatile关键字。
instance = new SingletonII()其实可以分为下面的步骤:
1.申请一块内存空间;
2.在这块空间里实例化对象;
3.instance的引用指向这块空间地址;
指令重排序存在的问题是:
对于以上步骤,指令重排序很有可能不是按上面123步骤依次执行的。比如,先执行1申请一块内存空间,然后执行3步骤,instance的引用去指向刚刚申请的内存空间地址,那么,当它再去执行2步骤,判断instance时,由于instance已经指向了某一地址,它就不会再为null了,因此,也就不会实例化对象了。这就是所谓的指令重排序安全问题。那么,如何解决这个问题呢?加上volatile关键字,因为volatile可以禁止指令重排序。

猜你喜欢

转载自blog.csdn.net/ice_eyes/article/details/79950178