多线程(五):wait 和 单例设计模式

目录

wait 和 notify

单例设计模式

饿汉模式

懒汉模式

双重检查加锁


前提回顾

在开始讲解单例设计模式之前,先来复习先前的知识。

上一章一共讲了以下内容:

1. 给了一个线程不安全的例子(两个线程各自增 5w 次,结果为一个小于 10w 的随机数)

2. 线程抢占式执行,执行到任何一行都可能跳出去执行其他线程的代码。

3. 多个线程同时修改一个变量

4. 修改操作不是原子的

5. 内存可见性

6. 指令重排序

对此的解决方式就是:加锁,也就是将其写在 synchronized 代码块内部。

某个线程中的某个对象调用了  synchronized 代码块 中的代码,就会照成其他线程的 阻塞。

wait 和 notify

已经写到 第五章了,我们从第二章开始就一直说线程调度是无序的,但是总有情况是想要我们写一个有序的代码,当然我们之前学过一个 方法: sleep() ,是的,这个方法可以让线程睡眠,想要一个有序的程序,那么就只能一直睡了,这就相当于写一个 单线程了,效率并没有那么快。

(栗子) 

多线程(五):wait 和 单例设计模式_第1张图片

对于上述情况,我们需要有个人通知他,老铁 ATM 没钱,等到 ATM 中有钱了再来通知他一次,让 老铁再去 抢占。

这里就换成 方法就是 wait 和 notify 。

当第一次发现 ATM 没钱时就 wait 来告诉他, 这里没钱了,让 滑稽 1 号休息一会,等到 notify 通知他的时候,滑稽 1 号就醒过来了,继续去 ATM 机取钱。

涉及到的方法:

wait() / wait(long timeout): 让当前线程进入等待状态.(带参的方法等会再说)
notify() / notifyAll(): 唤醒在当前对象上等待的线程. (notifyAll等会再说)

注意:

wait 和 notify 都是Object方法,只要你是类对象都可以调用 wait 和 notify 

但是 内置类型(基本数据类型)不可以。

我们来写写看这个代码:

多线程(五):wait 和 单例设计模式_第2张图片

 注意,任何可能照成线程阻塞的都需要抛 异常:

 InterruptedException

我们来运行一下:

多线程(五):wait 和 单例设计模式_第3张图片

 我们来翻译一下报的异常:非法的 监视(这里指的是synchroniezd 监视器锁) 状态异常。

这个异常主要是告诉我们 需要搭配 synchroniezd 来使用。

这里的 wait 主要做三件事:

1. 解锁

2. 阻塞等待

3. 当收到通知时,就唤醒,同时尝试重新获得锁。

注意加锁的对象要和 wait 的对象是同一个。

并且,notify 也要放在 synchroniezd 中使用。

来看看代码:

多线程(五):wait 和 单例设计模式_第4张图片

 运行一下:

多线程(五):wait 和 单例设计模式_第5张图片

达到我们想要的效果了。

注意了 notify 必须要在 wait 之后 执行,才有效;如果反了,不会错,但是没有效果。

此时代码 不会被唤醒 ,不会产生其他异常。

多线程(五):wait 和 单例设计模式_第6张图片

上述代码中,虽然是 t1 先执行的,但是我们可以通过 方法来控制代码执行顺序,这就是 wait 和 notify 的功劳。

我们上面还提到了 wait 带参的方法。

wait 带参方法和 notifyAll 方法

这个参数就和 sleep 方法的参数一样,给定一个时间,在时间范围内 wait 被唤醒了就和 wait 方法一样,但是时间已经到了,wait 方法还是没有被唤醒,那么他就自己醒了。

但是无参的就是死等,等不到就一直等,倔强。

这里就相当于设了一个闹钟,时间到了被闹钟吵醒,时间没到自己就醒了。

我们使用了wait 线程就进入到了 WAITING 状态;我们wait 的初心就是让线程进入阻塞状态。

上面还有一个 notifyAll 方法,看名字就知道这个是用于唤醒所有的线程的,这个用的比较少,我们了解一下即可。

wait 和 sleep 的比较

其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,
唯一的相同点就是都可以让线程放弃执行一段时间

当然为了面试的目的,我们还是总结下:
1. wait 需要搭配 synchronized 使用. sleep 不需要.
2. wait 是 Object 的方法 sleep 是 Thread 的静态方法

 二者初心不同:

sleep 单纯的想让线程睡一会,wait 解决线程之间的顺序控制。

带参的 wait 和 sleep 都能被提前唤醒。

单例设计模式

首先介绍一下什么叫做设计模式:

设计模式就相当于 软件开发中的棋谱,通过前辈对一些常见的场景,总结出的代码编写套路。

单例的意思就是说只能创建一个对象,不允许创建多个对象。

我们从语法上如何做出单例模式的实现。

我们要写的话,可以写出 5 ~ 6 种方法;但是我们只讲两种(在校招种一般只考这两种):饿汉式、懒汉式。

饿汉模式(急迫)

懒汉模式(从容)

举个很简单的例子,从硬盘读取内容到显示器上,有两种方式,一种是要等一会,把所有的数据全都读到显示器上,还有一种是立即显示,一点一点读,前者是饿汉模式,后者就是懒汉模式,两者相比,当然是懒汉模式更高效了

饿汉模式

从上述例子看来,我们需要从一开始就 new 出一个对象,并且因为是单例,后面不允许再 new 出新对象。

那么我们可以在类的内部 将 构造器私有化,同时在内部实例化一个对象 ,指定为唯一的对象。

看代码:

class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}

这里的 instance 被 static 修饰,就是类的属性,在 JVM 中每个类对象只有唯一一份,那么这个属性也是唯一一份。

并且我们将构造方法 私有化,外部无法调用 这个构造方法,再提供一个 获取类对象方法。

只能通过 get 方法去获取 对象的引用。

多线程(五):wait 和 单例设计模式_第7张图片

 如上图。 

这样 单例饿汉模式就写好了。

我们每次都只是读取这个实例化对象 ,只读不会对照成线程安全问题。

懒汉模式

非必要,不创建;能不 new ,就不 new 。

我们来实现一个代码:

class SingletonLazy {
    private static SingletonLazy instance = null;
    private SingletonLazy() {}
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }

}

我们先将唯一对象 设为空,只当我们需要调用 获取到实例对象引用时才 给他 new 出新对象;否则一直为空。

多线程(五):wait 和 单例设计模式_第8张图片

我们在单线程下不必考虑线程安全问题,但是在多线程下必须要考虑到线程安全问题。

这里设计到了 " 写 " 的操作,那么必然涉及到了线程安全问题。

在 getInstance 方法中 ,从 if 到 return 这一整段不是原子的,可能会产生如下情况

多线程(五):wait 和 单例设计模式_第9张图片

 看似只是多 new 了几次,如果我们 每个对象占有 100G 的内存呢,多 new 几次,后果不可估量!!!!

所以我们必须得对这段代码进行修改。

我们可以对其进行加锁操作

为了保证这个方法是原子的,可以直接选择对这个方法加锁:

多线程(五):wait 和 单例设计模式_第10张图片

 但是这样一来,会降低整个访问的速度,而且每次都要判断。那么有没有更好的方式来实现呢?

双重检查加锁

可以使用"双重检查加锁"的方式来实现,就可以既实现线程安全,又能够使性能不受到很大的影响。那么什么是"双重检查加锁"机制呢?

所谓双重检查加锁机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

class SingletonLazy3 {
    private static volatile SingletonLazy3 instance = null;
    private SingletonLazy3() {}
    public static SingletonLazy3 getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new SingletonLazy3();
                }
            }
        }
        return instance;
    }
}

多线程(五):wait 和 单例设计模式_第11张图片

总结(保证懒汉式线程安全的方法):

1.加锁,保证if和new是原子的

2.双重if判定,防止不必要的加锁

3.加volatile关键字,禁止指令重排序,保证后面的线程拿到的是完整的对象

饿汉模式:是天然线程安全的,涉及到读操作

懒汉模式:不安全,需要操作把它边安全

单例设计模式就到这里,下一章继续多线程的其他案例。

你可能感兴趣的:(JavaEE(初阶),设计模式,java,开发语言)