Java单例模式研究

前言

在我们创建各种单例的时候,我们需要考虑在多线程下程序是否正常工作,在开始讲解之前,先提一下多线程编程的3个原则:原子性 可见性 有序性

  • 原子性
    原子性是指我们的每一次操作都要一次执行完,不能存在执行到一半就停止执行的情况。在java内存模型中,有一个总的主内存用于保存变量的值,同事每个线程都分配了一块工作内存,用来保存变量值的副本,当我们去改变一个变量的值时,首先从主内存中读取值到线程的工作内存中,在线程内存中将值更改之后再把改变后的值刷新到主内存中。

    比如int i = 0; i++对于i++这个操作来讲,首先从主内存中拿到i的值,拷贝到我们当前线程的工作内存中,在工作内存中执行+1操作后,再刷新i的值到主内存中,这里也可以看到,i++这个操作其实执行了3步,也就是说i++这个操作不满足原子性。
    java中对基本数据类型变量的读取和赋值操作都满足原子性,比如int i = 0就满足原子性,也就是说在对i进行赋值时,会直接改变主内存的值,但是int i = j这种就不满足原子性了,因为它分为两步

    1. 从主内存中读取j的值
    2. 将j的值刷到到主内存中,覆盖i的初始值
  • 可见性
    可见性是指在某个线程中改了一个变量的值,其他线程中能立刻知道这个值已经被改变了,要重新读取变量的值用于各种计算

  • 有序性
    Java中cpu在执行指令时,为了性能优化,可能出现指令重排序的情况,比如

int i = 0;
int j = 0;
i++;

在执行过程中有可能会先执行i++后执行int j = 0,因为不管是否交换了执行顺序,最终结果都是一样的。

单例模式的常见写法

在最初学习java的单例时,可能大多数人都是如下写法(这里以Teacher.class来说明)

public class Teacher {

    public String name;
    public int age;
    public static Teacher teacherInstance = null;
    public static Teacher getInstance(){
        if(teacherInstance == null){
            System.out.println("create instance");
            teacherInstance = new Teacher();
        }
        return teacherInstance;
    }

}

这种写法在单线程中运行时,是没得问题的,但是在多线程中,可能就会创建多个实例,下面写一个测试代码

public class Singleton {

    public static void main(String[] args){
        /*开启100个线程来get Teacher的实例*/
        for(int i = 0;i < 100;i++){
            new Thread(()->{
                Teacher teacher = Teacher.getInstance();
            }).start();
        }
    }
}

最终运行结果如下:

create instance
create instance
create instance
create instance
create instance
create instance
create instance
create instance
create instance
create instance
create instance
create instance
create instance
create instance
create instance

Process finished with exit code 0

我们发现在多线程中,它多次创建了Teacher的实例,why?因为他并没有满足多线程编程的3个原则:原子性 可见性 有序性,首先当线程A在走到if(teacherInstance == null)之后,teacherInstance = new Teacher();之前时,线程B也走到了if(teacherInstance == null)时,由于此时线程A并没有对teacherInstance进行赋值,那么就会存在线程A和线程B都去创建了实例,最终导致拿到的不是相同的实例。

单例写法的改进

为了解决上面的问题,我们需要对单例的获取方法进行一些改进,使用synchronized关键字,同时加上双重检查机制,synchronized关键字修饰代码块后,能使同一时间段内,只有一个线程可以访问这个代码块,,改进代码如下:

public class Teacher {

    public String name;
    public int age;

    public static Teacher teacherInstance = null;
    public static Teacher getInstance(){
        if(teacherInstance == null){

            synchronized (Teacher.class){
                if(teacherInstance == null) {//双重检查锁
                    System.out.println("create instance");
                    teacherInstance = new Teacher();
                }
            }

        }
        return teacherInstance;
    }

}

可能有人会疑惑,为什么我们在synchronized之后还要再加一次非空判断呢?这是由于当线程A和线程B都先执行到了synchronized (Teacher.class)之前时,这时A先进入代码块后,B就会一直等待直到A执行完,如果没加双重判断,当A执行完后,B还是会去重新实例化Teacher,因此我们需要增加双重判断。
那为什么又要在synchronized (Teacher.class)加一次判断呢,去掉之后貌似结果也相同,这主要是由于性能的考虑,因为去掉第一个空判断后,每个线程执行到这时,不管teacherInstance是否为空,都可能都会等待,最优解应该是不为空的话就直接返回了,而不是挂起等待。

单例的最终写法

在我们改进了单例的写法后,虽然看上去没有问题了,但是还有一个问题,那就是syncronized关键字不会保证有序性,那么这又会导致什么问题呢,我们知道synchronized关键字只是保证同一时间段内,有且只有一个线程能执行synchronized修饰的代码块里面的代码,这是Ok的,然而在某些情况下,线程A执行synchronized代码块的代码teacherInstance = new Teacher();时,分为3步

  1. 在堆上申请内存
  2. 初始化内存空间
  3. 将内存空间指向teacherInstance

由于未能够保证有序性,因此第 2 3步有可能交换,当交换后执行第3步后,teacherInstance不为null了,但是还未初始化内存空间时,线程A暂停了,线程B这时候访问teacherInstance发现不为null时,直接访问后,就可能出现问题,因此需要为teacherInstance加上volatile关键字,volatile能禁止指令重排序,最终单例写法如下:

public class Teacher {

    public String name;
    public int age;

    public static volatile Teacher teacherInstance = null;
    public static Teacher getInstance(){
        if(teacherInstance == null){

            synchronized (Teacher.class){
                if(teacherInstance == null) {
                    System.out.println("create instance");
                    teacherInstance = new Teacher();
                }
            }

        }
        return teacherInstance;
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值