初始JavaEE篇——多线程(4):wait、notify,饿汉模式,懒汉模式,指令重排序

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页: 我要学编程(ಥ_ಥ)-CSDN博客

所属专栏:JavaEE

目录

[wait、notify 方法](#wait、notify 方法)

多线程练习

单例模式

饿汉模式

懒汉模式

指令重排序


wait、notify 方法

wait 和 我们前面学习的sleep、join方法一样,也是让线程阻塞,但是其可以被notify方法唤醒,但是sleep是被Interrupt给提前唤醒或者指定时间过了之后自动被唤醒,并且会抛出异常。且 join 是一个线程等待另一个线程,并且要 被等待的线程彻底执行完成之后,等待的线程才会从阻塞的中被唤醒重新执行。

wait方法在使用时,要和synchronized一起搭配使用,因为其是先对调用它的对象进行解锁,阻塞,在被唤醒之后,在进行加锁操作。

java 复制代码
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Thread t = new Thread(()->{
            System.out.println("wait之前");
            synchronized (locker1) { // 加锁
                try {
                    locker1.wait(); // 进入wait方法解锁,出wait方法加锁
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("wait之后");
        });
        t.start();
        // 为了让t线程先wait阻塞等待,得先休眠主线程一会:
        // 可以使用sleep方法,也可以使用IO的方法阻塞
        Thread.sleep(1000);
        System.out.println("输入任意内容,唤醒t线程");
        Scanner scanner = new Scanner(System.in);
        scanner.next();
        // 要出wait方法就需要notify进行唤醒操作
        synchronized (locker1) {
            locker1.notify();
        }
    }
}

注意:

1、在Java中,wait 和 notify 方法一定是和 synchronized 一起使用的。

2、在1的基础上,四者的进行加锁解锁的操作一定是针对同一个锁对象。

3、notify 的唤醒操作一定是在 wait 之前才能有效的唤醒。如果先执行了 notify 的唤醒操作,但是 还没有执行wait的阻塞操作的话,那么线程就一直会阻塞,但是 notify 的唤醒操作对线程本身是不会有影响的。

4、wait 和 notify 方法是 Object 对象的方法,即所有对象都可以使用这两个方法。

5、如果有多个线程处于 wait 的阻塞状态,那么 notify 一次只能随机唤醒一个线程。如果想要全部唤醒的话,得使用 notifyAll 方法。当然,也可以使用 notifyAll 去唤醒一个线程。

6、wait 和 join一样,也提供了最大等待时间。当超出这个最大等待时间时,被 wait 方法阻塞的线程将不会在处于阻塞状态。

java 复制代码
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t = new Thread(()->{
            System.out.println("wait之前");
            synchronized (locker) {
                try {
                    locker.wait(5000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("wait之后");
            System.out.println("t线程结束");
        });

        t.start();
        Thread.sleep(1000);
        System.out.println("输入任意内容,唤醒t线程");
        Scanner scanner = new Scanner(System.in);
        scanner.next();
        synchronized (locker) {
            locker.notify();
        }
    }
}

当我们迟迟没有去输入值时,如果已经超过了 wait 的最大阻塞时间的话, wait 便不会去阻塞 t 线程了,而是会让其继续执行下去,即使我们后续再次输入值来执行 notify 的唤醒操作,也不再有用了。

7、当一个线程执行到 wait 之后,这个锁被释放了,也就意味着有别的线程可以使用这把锁了。

多线程练习

到此为止,我们已经学习了不少的多线程知识,现在我们就来练习一下。

题目:

有三个线程:t1、t2、t3,三者分别打印A、B、C,现在我们需要打印10次ABC。

思路:

1、既然打印有先后顺序,那么我们肯定是可以通过手动控制sleep 的休眠时间来决定的。

2、刚刚我们学习了 wait 和 notify ,应该是可以想到这个应用场景的,完全对上了。一个 打印完A之后,唤醒另一个线程,打印B,接着唤醒另一个线程打印C,最后 t3线程唤醒 t1线程,就这样相互唤醒打印,而 main 线程用来唤醒 t1 线程开始最初的打印即可。

代码实现:

1、暴力-sleep:

java 复制代码
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.print("A");
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.print("B");
            }
        });

        Thread t3 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("C");
            }
        });

        t1.start();
        Thread.sleep(10); // 确保 t1是最先执行的
        t2.start();
        Thread.sleep(10); // 确保 t2比t1后执行,比t3先执行
        t3.start();
    }
}

注意:这里使三个线程的执行顺序的确定,其休眠的时间不能过长,否则不好衔接。

2、wait-notify版本:

java 复制代码
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Object locker3 = new Object();

        Thread t1 = new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    synchronized (locker1) {
                        locker1.wait();
                    }
                    System.out.print("A");
                    synchronized (locker2) { // 要清楚唤醒的是哪个线程
                        locker2.notify();
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        Thread t2 = new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    synchronized (locker2) {
                        locker2.wait();
                    }
                    System.out.print("B");
                    synchronized (locker3) { // 要清楚唤醒的是哪个线程
                        locker3.notify();
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        Thread t3 = new Thread(()->{
            try {
                for (int i = 0; i < 10; i++) {
                    synchronized (locker3) {
                        locker3.wait();
                    }
                    System.out.println("C");
                    synchronized (locker1) { // 要清楚唤醒的是哪个线程
                        locker1.notify();
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        t1.start();
        t2.start();
        t3.start();
        // 确保t1先执行到了wait
        Thread.sleep(1000);
        synchronized (locker1) { // notify一定要和synchronized配合使用
            locker1.notify();
        }
    }
}

单例模式

单例模式属于设计模式的一种,是指一个进程中,一个类只能实例化一个对象,即单个实例。那怎么去实现一个进程中只能有一个对象呢?直接把构造方法改为private即可,这样在外部就不能创建实例了。

单例模式中,最常见的就是饿汉模式与懒汉模式。

饿汉模式

饿汉模式,主要体现在"饿"字上,因为其是迫不及待的去创建类的实例。

代码演示:

java 复制代码
// 饿汉模式
class SingleTon {
    // 迫不及待的创建实例
    private static SingleTon singleTon = new SingleTon();

    public static SingleTon getInstance() {
        return singleTon;
    }

    // 单例模式的构造方法一定是private修饰的
    private SingleTon() {

    }
}

这里创建类的实例是通过创建一个静态的成员变量来实现的,而静态的成员变量是类在加载时,就会被创建,即JVM中有这个类存在的痕迹的话,那么这个实例就会存在。 因此,以"饿"得名。

我们也可以去检查这个饿汉模式是否创建成功,主要检查是否是单例模式。

java 复制代码
public class Test {
    public static void main(String[] args) {
        // SingleTon s = new SingleTon(); // error

        SingleTon s1 = SingleTon.getInstance();
        SingleTon s2 = SingleTon.getInstance();
        System.out.println(s1 == s2); // true
    }
}

从上面的程序运行的结果,可以得知:一个进程中不能实例化多个对象,符合单例模式的特征。

懒汉模式

懒汉模式,主要体现在"懒"字上,只有当迫不得已时,才去创建实例。

代码演示:

java 复制代码
// 懒汉模式
class SingleTonLazy {
    // 迫不得已才创建实例
    private static SingleTonLazy singleTonLazy = null;

    public static SingleTonLazy getInstance() {
        if (singleTonLazy == null) {
            singleTonLazy = new SingleTonLazy(); // 一定要把对象保留下来
        }
        return singleTonLazy;
    }

    // 单例模式的构造方法一定是私有的
    private SingleTonLazy() {

    }
}

懒汉模式只有当外部调用getInstance方法时,才会去创建实例,否则就不会创建实例。

同样也可以去测试这个懒汉模式是否创建成功。

java 复制代码
public class Test {
    public static void main(String[] args) {
        // SingleTonLazy s = new SingleTonLazy(); // error

        SingleTonLazy s1 = SingleTonLazy.getInstance();
        SingleTonLazy s2 = SingleTonLazy.getInstance();
        System.out.println(s1 == s2); // true
    }
}

上面的懒汉模式在单线程下使用没问题,但是在多线程下使用,便会出现线程安全问题。(饿汉模式之所没有线程安全问题,是因为饿汉模式只是进行return的"读"操作,而不是和懒汉模式一样,有"写"操作)

因为懒汉模式的创建线程虽然只是一个赋值代码,也就是对应一条CPU指令,但是有了 if 语句之后,两者就不算是原子的了。

例如,当线程1去实例化一个对象时,执行到 if 语句,但偏偏此时操作系统将其从CPU上踢下去了,然后线程2就也去CPU上执行了实例化对象的操作,和线程1一样只是执行到 if 语句,也被赶下去了,接着 线程1执行了赋值语句成功的创建了一个对象,然后线程2又被调度到CPU上了,也执行了创建对象的赋值语句。

上面就会导致两个问题:

1、 这里new了两次,即创建了两次对象破坏了单例模式的初衷。

2、后一次new的对象会覆盖前面的对象,可以会对程序的数据造成影响,最终导致程序崩溃。

这里有小伙伴可能会疑惑:为什么线程1创建了对象之后,线程2还会去创建对象呢?因为线程1创建完成之后,线程2已经执行到了 if 语句之中,其认为还没有创建对象。

因此,我们得对上述代码进行加锁操作。

java 复制代码
// 懒汉模式
class SingleTonLazy {
    // 迫不得已才创建实例
    private static SingleTonLazy singleTonLazy = null;
    private static Object locker = new Object();

    public static SingleTonLazy getInstance() {
        synchronized (locker) {
            if (singleTonLazy == null) {
                singleTonLazy = new SingleTonLazy(); // 一定要把对象保留下来
            }
        }
        return singleTonLazy;
    }

    // 单例模式的构造方法一定是私有的
    private SingleTonLazy() {

    }
}

加锁操作确实可以实现线程安全,但是它也会造成程序的性能下降,因为当对象的实例被创建出来后,别的线程再去调用这个方法时,就会进行加锁操作,而加锁对于最终的结果来说没影响,也就是加锁加了个寂寞,这就是在浪费时间了。因此,也就导致了性能下降了。

我们的解决方法是在锁的最外层再加上一个 if 语句去判断,这样即使有了实例之后,别的线程再尝试去创建实例时,就会直接return,而不会再去进行加锁操作了,这样性能就提升了不少。

java 复制代码
    public static SingleTonLazy getInstance() {
        if (singleTonLazy == null) {
            synchronized (locker) {
                if (singleTonLazy == null) {
                    singleTonLazy = new SingleTonLazy(); // 一定要把对象保留下来
                }
            }
        }
        return singleTonLazy;
    }

指令重排序

上面的懒汉模式代码,还是有点问题,这个问题和指令重排序有关。

概念:指令重排序是指在不影响代码的执行逻辑的基础上,编译器对要执行的代码其底层对应的计算机指令进行了优化处理,会使其与原来的执行顺序不一致。

懒汉模式的指令重排序体现在 赋值语句。我们先来学习一下,这个赋值语句,其底层对应的逻辑:1、向内存申请了一块空间;2、在这块空间内构造对象(初始化成员变量等)3、将这块空间的首地址给到引用变量。如果将上述三个操作类比到我们日常生活的话,那就是1、买房子;2、装修;3、拿到钥匙。

指令重排序可能会使这个1、2、3的顺序打乱,变成1、3、2。虽然这个在日常生活中,即使打乱之后,我们也是不会直接入住的,因为还没有装修,但是计算机可不一样,它是一个铁憨憨,他只知道执行工作,因此当它执行了1、3之后,也就是拿到了这个对象的引用之后,如果此时操作系统将其从CPU上踢下去了,让别的线程来执行相关方法的话,这个操作就不亚于在毛坯房中直接拎包入住的行为了。这可能直接就把程序给搞崩溃了。因此,我们不能让指令重排序的行为发生,这里就需要用到 volatile 关键字了。这个关键字既可以避免 内存可见性的问题,也可以避免指令重排序的问题。

java 复制代码
private static volatile SingleTonLazy singleTonLazy = null;

好啦!本期 初始JavaEE篇------多线程(4):wait、notify,饿汉模式,懒汉模式,指令重排序 的学习之旅 就到此结束啦!我们下一期再一起学习吧!

相关推荐
Moe4882 分钟前
Redis 缓存三大经典问题:穿透、击穿与雪崩
java·后端·面试
卷Java14 分钟前
Python字典:键值对、get()方法、defaultdict,附通讯录实战
开发语言·数据库·python
liuyao_xianhui14 分钟前
优选算法_翻转链表_头插法_C++
开发语言·数据结构·c++·算法·leetcode·链表·动态规划
happy_baymax17 分钟前
三电平矢量表达式MATLAB实现
开发语言·matlab
xyq202418 分钟前
jEasyUI 创建 XP 风格左侧面板
开发语言
赫瑞19 分钟前
Java中的最长公共子序列——LCS
java·开发语言
于先生吖22 分钟前
零基础开发国际版同城出行平台 JAVA 顺风车预约系统实战教学
java·开发语言
代码雕刻家23 分钟前
2.22.StringBuffer类的常见用法、
java·开发语言
yhole24 分钟前
Java进阶(ElasticSearch的安装与使用)
java·elasticsearch·jenkins
明月(Alioo)39 分钟前
Python 并发编程详解 - Java 开发者视角
java·开发语言·python