多线程案列之单例模式

上文我们了解了如何创建多线程,以及在多线程下由于不同的操作,可能引发很多线程不安全问题,今天我们就线程使用的案列吧

1. 多线程案例

1.1 单例模式

单例模式能保证某个类在程序中只存在唯⼀⼀份实例,而不会创建出多个实例.

那么我们该如何一个程序中对象是单例的,用什么方法去保证??

  1. 人为口头约定,大家不要new 这个对象,我给大家提供一个方法,这个方法可以返回一个单例的对象,但是毕竟是人为操作,很容易出错
  2. 通过语言自身的语法约束,限制一个类只能被实例化一个对象,把限制的过程交给程序,程序写死了就按照写的逻辑执行,不会改变,只要代码层面能保证是单例,那么执行后一定是个单例,不会出现问题

所以我们采用方式2,在Java中单例模式具体的实现方式有很多.最常⻅的是"饿汉""懒汉"两种.

1.1.1 饿汉模式

类加载的时候就完成对象初始化的创建方式称为’饿汉模式’

实现过程:

1. 要实现单例类, 只需要定义一个static修饰的变量,就可以保证这个变量全局唯一(单例)

在这里插入图片描述
说明:

  • private修饰是为了防止外部对这个变量赋值(修改)
  • static是为了保证全局唯一
  • new SingletonHungry(),表示当类加载到JVM中的时候,就会实列这个变量

2. 既然是单例,就不想让外部去new 这个对象,此时我们就要将SingletonHungry当前类的构造方法私有化

在这里插入图片描述

此时从语法上就不能new对象了

在这里插入图片描述

3. 把获取对象的方法改为static,通过类名.方法名的方式调用

在这里插入图片描述

完整饿汉模式创建如下:

/**
 * 饿汉模式
 */
public class SingletonHungry {
   //定义一个类的成员变量,用static修饰,保证全局唯一
   private static SingletonHungry instance=new SingletonHungry();
   
   //由于是单例就不想外部就new,所以将构造方法私有化
   private SingletonHungry(){
   }
   
   //把获取对象的方法改为static,通过类名.方法名的方式调用
   public static SingletonHungry getInstance() {
      return instance;
   }
}

最后通过一段测试代码,看我们的饿汉模式是否成功了,若打印的对象地址相同则成功,否则失败

public class SingletonHungryTest {
    public static void main(String[] args) {
        SingletonHungry instance1=SingletonHungry.getInstance();
        SingletonHungry instance2=SingletonHungry.getInstance();
        SingletonHungry instance3=SingletonHungry.getInstance();
        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance3);
    }
}

在这里插入图片描述
我们发现成功了!!

总结:
由于程序在启动的时候可能需要加载很多的类(单例类),并不一定要在程序启动的时候用
为了节省计算机资源,加速程序的启动,可以让单例类在用到的时候再进行初始化(new),延迟初始化
在编程中延时加载是个褒义词

1.1.2 懒汉模式-单线程版

类加载的时候不创建实例.第⼀次使用的时候才创建实例.

实现过程:

  1. 只声明这个全局变量,不初始化

在这里插入图片描述

  1. 在获取单例对象的时候加一个是否为的判断,空则创建对象

在这里插入图片描述

  1. 不想外部new新对象

在这里插入图片描述

完整懒汉模式实现代码如下:

public class SingletonLazy {

    //在未使用前不实例化对象
    private static SingletonLazy instance=null;


    //由于是单例就不想外部就new,所以将构造方法私有化
    private SingletonLazy(){
    }

    //在获取单例对象的时候加一个是否为的判断,空则创建对象
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

实现完成后,我们测试在单线程和多线程下是否线程安全??

  1. 单线程
public class SingletonHungryTest {
    public static void main(String[] args) {
        SingletonHungry instance1=SingletonHungry.getInstance();
        SingletonHungry instance2=SingletonHungry.getInstance();
        SingletonHungry instance3=SingletonHungry.getInstance();
        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance3);
    }
}

在这里插入图片描述

  1. 多线程
public class SingletonLazyTestMulti {
    public static void main(String[] args) {
    //创建10个线程
        for (int i = 0; i < 10; i++) {
            Thread thread=new Thread(()->{
                SingletonLazy instance=SingletonLazy.getInstance();
                System.out.println(instance);
            });
            thread.start();
        }
    }
}

在这里插入图片描述

究竟是何种原因导致的呢,我们见下文细说

1.1.3 懒汉模式-多线程版

1. 为何懒汉模式下的多线程有线程安全问题呢?

由于instance是一个共享变量
在这里插入图片描述
创建对象并赋值,是一个修改操作
在这里插入图片描述
多个线程对共享变量进行修改导致引发了线程安全问题
在这里插入图片描述
小tip: 只要变量在运算符左边,都可以理解为先LOAD再进行操作

2. 那么我们该如何解决当前线程安全问题呢??

前面我们对线程安全问题有两种关键字可以帮助我们解决,synchronize和volatile,由于此处是由于原子性问题导致的,我们可以采用synchronize关键字将初始化有关的代码块加锁即可

懒汉模式-多线程版代码块如下:

public class SingletonLazy {

    //在未使用前不实例化对象
    private static SingletonLazy instance=null;

    //由于是单例就不想外部就new,所以将构造方法私有化
    private SingletonLazy(){
    }

    //在获取单例对象的时候加一个是否为的判断,空则创建对象
    public static SingletonLazy getInstance() {
        
        //对初始化相关的代码块加锁,可以解决问题
        synchronized (SingletonLazy.class){
            if (instance == null) {
                instance = new SingletonLazy();
            }
            return instance;
        }
    }
}

1.1.4 懒汉模式-多线程版(改进)

1.上述代码块的写法还有一个问题需要处理?

我们思考一个问题:

  1. 当第一个线程进行这个方法时,如果变量没有初始化,则获取锁进行初始化操作,此时单例对象被第一个线程创建完成
  2. 后面的线程以后也永远不会再执行new 对象的操作
  3. synchronized还有没有必要加了?

由于synchronized关键字对应了CPU上的指令,LOCK和UNLOCK对应的锁指令是互斥锁,比较消耗系统资源,从第二个线程开始这个加锁解锁都是无效的操作,所以我们可以考虑是否减小加锁的次数,我们只需要在加锁前判断是否需要加锁即可


2.在DCL当中是否要加volatile??

我们来分析一下: 在Java中new一个对象的步骤如下:
1.在内存中申请一片空间
2.初始化对象的属性(赋初值)
3.把对象在内存中的首地址赋给对象的引用 其中1.3 是强相关的关系,2并不强相关就有可能发生指令重排序
正常:123 重排序:132 为变量加volatile修饰,禁止指令重排序,初始化指令的执行顺序为123
为了避免"内存可⻅性"导致读取的instance出现偏差,于是补充上volatile.
建议: 只要在多线程环境中修改了共享变量就要给共享变量加volatile


懒汉模式改进版代码如下:

/**
 * Double Check Lock 双重检查锁
 */
public class SingletonLazyDCL {
    private static volatile SingletonLazyDCL instance=null;
    private SingletonLazyDCL(){}
    public static SingletonLazyDCL getInstance(){
        if(instance==null){
            synchronized (SingletonLazyDCL.class){
                if(instance==null){
                    instance=new SingletonLazyDCL();
                }
            }       
        }
        return instance;
    }
}

多线程案例之阻塞队列请见下文!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值