Java多线程案例——单例模式(恶汉模式和懒汉模式)

导读:本篇文章讲解 Java多线程案例——单例模式(恶汉模式和懒汉模式),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一,什么是单例模式

单例顾名思义指的是单个实例对象(所以单例模式要求构造方法私有化才能保证在类外不能创建该类的实例化对象);在有的场景中,不应该创建多个对象时就应该使用单例模式,一旦使用了单例模式,此时想创建多个实例都很困难(单例模式就是巧用了Java的语法达成了某个类只能被创建出一个实例这样的效果,当程序员不小心创建了多个实例就会编译报错)。

二,单例模式的分类

单例模式分为饿汉模式和懒汉模式两种

1.饿汉模式


/**
 * 饿汉模式版本的单例模式
 */
public class Singleton {
    //定义一个static属性的Singleton类实例对象instance
    //在类加载的时候就创建出来了,”显得很急切“,所以称之为恶汉模式
    private static Singleton instance = new Singleton();

    //将构造方法私有化,可以保证单例(即在类外不能new Singleton这个类来创建多个对象)
    private Singleton() {

    }

    //定义一个public属性的接口可以在类外接受instance实例
    public static Singleton getInstance() {
        return instance;
    }
}

1,单例模式如何保证的单例?

答:第一是该实例化对象instance是static定义的(属于类属性,类对象是唯一实例的),在类加载的时候创建;第二个是该类的构造方法被private修饰,在类外无法通过new这个关键字来创建实例化对象;这两点保证了实例对象的唯一性。

2,单例模式是否存在线程安全问题?

答:单例模式不存在线程安全问题,单例模式的实例化对象在类加载的阶段(仅此一份),此时该类中只提供了一个getInstance方法来读取这里的instance,只涉及读操作就不存在线程安全问题。

3,单例模式没有线程安全问题,那他的不足在哪里?

答:虽然单例模式不存在线程安全问题,但是由于单例模式无论如何都会在类加载的时候创建实例化对象,如果不需要没有人使用该类的方法的话,那么就没必要创建这个实例化对象,此时这种模式就势必会造成资源的浪费,所以就引入了懒汉模式。

2.懒汉模式

代码一:

public class SingletonLazy {
    //定义一个static属性的Singleton类实例对象instance
    //但是此时初始化该实例化为null
    private static SingletonLazy instance = null;

    //将构造方法私有化,可以保证单例(即在类外不能new Singleton这个类来创建多个对象)
    private SingletonLazy() {

    }

    //定义一个public属性的接口可以在类外接受instance实例
    //该方法会先判断此时的instance对象引用是否为空,如果为空进行实例化操作
    //此时才真正实例化了对象
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

此处的代码是为了区分开饿汉模式的类加载阶段创建实例化对象,此处实例化对象是在getInstance方法中创建的,如果instance为空(即未被创建的时候),就会用new关键字进行实例化对象;这里的代码还存在问题需要进行优化。

1,这里保证单例的方法同恶汉模式一样,不作赘述。

2,现在的代码是否存在线程安全问题?

答:该代码存在线程安全问题,因为if语句块的代码是new实例化对象涉及到了写操作,写操作在多线程下存在线程安全问题,所以需要用synchronized给该代码块加锁,加锁对象是类对象(因为该实实例属于类属性下的对象)。

代码二(加锁优化):

public class SingletonLazy {
    //定义一个static属性的Singleton类实例对象instance
    //但是此时初始化该实例化为null
    private static SingletonLazy instance = null;

    //将构造方法私有化,可以保证单例(即在类外不能new Singleton这个类来创建多个对象)
    private SingletonLazy() {

    }

    //定义一个public属性的接口可以在类外接受instance实例
    //该方法会先判断此时的instance对象引用是否为空,如果为空进行实例化操作
    //此时才真正实例化了对象
    public static SingletonLazy getInstance() {
        synchronized (Singleton.class) {
            //加锁操作,保证写操作安全
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
}

再来观察此时的代码是否存在问题?每次调用getInstance方法时都会进行加锁,但是不难发现实例化instance对象只有第一次为空时才会创建(只需创建一份),一旦instance对象创建之后就不需要再创建了,此时调用getInstance方法就不需要再重复加锁了(直接执行return instance语句即可),重复加锁操作势必会造成资源的开销,所以我们需要在外面再进行一次判断,判断该实例对象是否已经创建(其实就是判断一下此时的对象是否有加锁的资格)。

代码三(判断此时对象是否有加锁的资格):

public class SingletonLazy {
    //定义一个static属性的Singleton类实例对象instance
    //但是此时初始化该实例化为null
    private static SingletonLazy instance = null;

    //将构造方法私有化,可以保证单例(即在类外不能new Singleton这个类来创建多个对象)
    private SingletonLazy() {

    }

    //定义一个public属性的接口可以在类外接受instance实例
    //该方法会先判断此时的instance对象引用是否为空,如果为空进行实例化操作
    //此时才真正实例化了对象
    public static SingletonLazy getInstance() {
        //判断此时的对象是否具有加锁资格
        if(instance == null) {
            synchronized (Singleton.class) {
                //加锁操作,保证写操作安全
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

现在的代码还剩最后一个问题,但是这个问题不是很容易发现,发现这个问题我们需要了解一下new这个操作后面所做的事情(new操作并不是一个原子性的操作,存在指令重排序的可能从而导致发生线程不安全的问题):

  1. 申请内存空间

  1. 调用构造方法,把这个内存空间初始化成一个合理的对象

  1. 把内存空间的地址赋值给instance引用

正常情况下,按照123的顺序来执行的,但是由于编译器的优化可能会出现指令重排序的情况,调整为132的执行顺序,该顺序在单线程的环境下不会有问题,而在多线程的情况下可能存在线程安全问题(假设t1是按照132的顺序执行的,t1执行到13之后,再执行2的时候被切出CPU让t2来执行,站在t2的角度,此处的instance引用就非空了,就会直接返回instance引用并且可能会尝试使用其中的属性,但是由于t1中的2操作还没执行完成,t2拿到的是非法的对象,还没构造成完整的对象),此时就需要使用volatile关键字来修饰instance禁止指令重排序。

代码四(使用volatile关键字禁止指令重排序):

public class SingletonLazy {
    //定义一个static属性的Singleton类实例对象instance
    //但是此时初始化该实例化为null
    private static volatile SingletonLazy instance = null;//使用volatile关键字保证内存可见性

    //将构造方法私有化,可以保证单例(即在类外不能new Singleton这个类来创建多个对象)
    private SingletonLazy() {

    }

    //定义一个public属性的接口可以在类外接受instance实例
    //该方法会先判断此时的instance对象引用是否为空,如果为空进行实例化操作
    //此时才真正实例化了对象
    public static SingletonLazy getInstance() {
        //判断此时的对象是否具有加锁资格
        if(instance == null) {
            synchronized (Singleton.class) {
                //加锁操作,保证写操作安全
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

以上就是单例模式懒汉模式的最终代码版本!!!

总结:

  1. 如何理解这里的双重if的操作?

答:里面的if是判断是否满足条件创建实例化对象,外面的if是判断此时的对象是否具有加锁的资格,减少反复加锁的开销(因为只有第一个new instance对象的时候才需要加锁)。

  1. 使用volatile关键字的作用是什么?

答:因为new操作不是一个原子性的操作,可能出现指令重排序的问题(可以举前面t1,t2线程的例子来说明),造成线程不安全,通过用volatile关键字来禁止指令重排序。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/89443.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!