多线程中的单例模式

第一种写法:饿汉式
//饿汉式
public class SingleObject1 {
    private static final SingleObject1 instance = new SingleObject1();

    private SingleObject1(){

    }

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

第二种写法:懒汉式

1、迭代一:

//懒汉式: 在多线程环境下不安全,会产生多个实例!
public class SingleObject2 {

    private static SingleObject2 instance;

    private SingleObject2(){
        System.out.println("SingleObject2()...");
    }

    public static SingleObject2 getInstance(){
       if(instance == null){
            //t1,t2两个线程同时异步来到这里!
            instance = new SingleObject2();
        }
        return instance;
    }

    public static void main(String[] args) {
        //0~4, 循环5次
        IntStream.range(0,5).forEach(i ->{
            new Thread(() -> {
                System.out.println(SingleObject2.getInstance());;
            }).start();
        });
    }
}
  • 控制台
SingleObject2()...
SingleObject2()...
com.wzj.线程.单例模式.SingleObject2@7ea6e10f
SingleObject2()...
SingleObject2()...
SingleObject2()...
com.wzj.线程.单例模式.SingleObject2@7fdaab5b
com.wzj.线程.单例模式.SingleObject2@3bbb38b7
com.wzj.线程.单例模式.SingleObject2@1dc727cc
com.wzj.线程.单例模式.SingleObject2@7e611cce

结果: 一看就产生了多个实例,因为构造方法中的输出语句执行了多次,而且对象的地址值也都不一样

分析:是什么原因导致的呢?因为多线程是异步执行的,在某个时间点多个线程可能会同时执行到代码中的①处,然后继续往下执行,可能就会导致该类产生多个实例。既然分析出了原因是因为某个时间点同时进来的多个线程导致,那么我就加锁让他们一个一个同步进来。

2、迭代二:加锁

  • 将getInstance方法改造如下
public static SingleObject2 getInstance(){
    synchronized (SingleObject2.class){//类锁
        if(instance == null){
            instance = new SingleObject2();
        }
    }
    return instance;
}
  • 控制台
SingleObject2()...
com.wzj.线程.单例模式.SingleObject2@7ea6e10f
com.wzj.线程.单例模式.SingleObject2@7ea6e10f
com.wzj.线程.单例模式.SingleObject2@7ea6e10f
com.wzj.线程.单例模式.SingleObject2@7ea6e10f
com.wzj.线程.单例模式.SingleObject2@7ea6e10f

加完锁之后,确实变成单例了。但是你有没有发现每次调用该方法执行里面的代码的时候都加锁,这会使得程序的执行效率降低。

3、迭代三:使用双重检查机制

  • 将getInstance方法改造如下
public static SingleObject2 getInstance(){
    if(instance == null){ //③处
        //①
        synchronized (SingleObject2.class){
            if(instance == null){//④
                instance = new SingleObject2();
            }
        }
        //②
    }
    return instance;
}

分析:假设两个线程t1,t2执行到了①处,假设t1线程先抢到cpu的执行权,然后获取了锁,这时t2是进不来的,等t1执行到了②处,假设t2线程抢到了cpu的执行权,这时它进去执行if判断结果是不满足条件,因为t1线程已经创建了该类的对象,然后t2也执行完了,这时其他线程进来直接执行③处,由于不为空所以直接返回实例了,要是不加③处的代码,就每个线程进来都得抢锁,然后在④处判断,因为耗时而且没必要,所以在③处加该判断是很有必要的!

4、迭代四:volatile

你天真的以为用了双重检查机制就完事了,小伙子还是太年轻。

双重检查机制可能会出现空指针的问题。这是由于编译器为了优化可能会帮你做的指令重排序!

  • 将成员变量instance,使用volatile关键字修饰!,它可以禁止重排序!
private static volatile SingleObject2 instance;

5、说清楚什么情况下双重检查机制会导致空指针的问题!

  • 题外话
我为什么会问这个呢?因为不是我问的,哈哈哈,因为今天我在写单例的时候,由于是多线程的访问,我就将它写成了这种双重检查+volatile的形式,然后提交后,不料就被function leader给看到了,就连环炮来问我,问我这样写的好处,我就一一跟它说了,他笑着说没见过加类锁这种形式,说我写的不规范,我:额,哪里不规范了,还说应该拿instance作为锁,我说第一次进来instance不是空的吗,怎么可以作为锁呢?这不是导致空指针吗,他沉默了,然后他要问为什么要加两次判断,这不是多余吗,我还是跟他解释,他又沉默了。然后他又问为什么要加volatile,我刚开始也不是特别清楚,我就和他说只知道是编译器为了优化帮我们做了重排序,可能会导致空指针。所以我加上volatile关键字禁止重排序,这样就可以避免这种情况的发生,然后他说这怎么会空指针啊,就算重排序也没问题啊,他理解的重排序是编写代码的时候互换位置,然后又说不要在网上看了一下就直接用,突显你会新技术啊,这可是要投产的,所以你看你加这个volatile根本什么用都没有,写的多余,耗费性能,我:额,耗费个屁的性能。要说性能这项目中好多代码得重构。当然这是我心里想的,我表明就笑笑不说话,你是大哥,你爱咋样咋样!。
  • 所以下面我就要分析TMD不加volatile可能会发生空指针!
public class SingleObject2 {

    private static SingleObject2 instance;
    
    private String name;
    
    public String getName(){
        return this.name;
    }

    private SingleObject2(){
        System.out.println("SingleObject2()...");
        this.name = "wzj";
    }

    public static SingleObject2 getInstance(){
        if(instance == null){
            synchronized (SingleObject2.class){
                if(instance == null){
                    instance = new SingleObject2(); //①
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        //0~4, 循环5次
        IntStream.range(0,5).forEach(i ->{
            new Thread(() -> {
                System.out.println(SingleObject2.getInstance().getName());;
            }).start();
        });
    }
}

在①处其实 new SingleObject2(),其实不是一个原子性的操作,它可以分为三步

1、分配内存空间

2、初始化对象

3、将对象指向刚分配的内存空间

但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:

1、分配内存空间

2、将对象指向刚分配的内存空间

3、初始化对象

  • 于是可能就发生如下表格情况,但我运行了好多次,始终模拟不到不加volatile会发生空指针的现象,这也是跟线程具有随机的特性有关。

在这里插入图片描述

  • 这里加volatile是为了禁止指令重排序,保证有序性!
第三种写法:使用静态内部类(推荐)
//使用静态内部类的方式
public class SingleObject3 {

    private SingleObject3(){}

    private static class InstanceHolder{
        private final static SingleObject3 instance = new SingleObject3();
    }

    public static SingleObject3 getInstance(){
        return InstanceHolder.instance;
    }
}
第四种写法:使用枚举(推荐)
//使用枚举来实现单例,很好,但是个人感觉不太优雅,可能是我太菜了!
public class SingleObject4 {

    private SingleObject4(){}

    private enum Singleton{
        INSTANCE;

        private final SingleObject4 instance;

        Singleton(){
            instance = new SingleObject4();
        }

        public SingleObject4 getInstance(){
            return instance;
        }
    }

    public static SingleObject4 getInstance(){
        return Singleton.INSTANCE.getInstance();
    }

    public static void main(String[] args) {
       IntStream.rangeClosed(0,100)
                .forEach(i -> new Thread(String.valueOf(i)){
                    @Override
                    public void run() {
                        System.out.println(SingleObject4.getInstance());
                    }
                }.start());

    }
}

最后:来自虽然帅,但是菜的cxy

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值