JavaEE初阶第八期:解锁多线程,从 “单车道” 到 “高速公路” 的编程升级(六)

专栏:JavaEE初阶起飞计划

个人主页:手握风云

目录

一、volatile关键字

[1.1. 原理](#1.1. 原理)

[1.2. Java memory model(Java内存模型)](#1.2. Java memory model(Java内存模型))

[1.3. volatile与synchronized的区别](#1.3. volatile与synchronized的区别)

二、wait和notify

[2.1. wait()方法](#2.1. wait()方法)

[2.2. notify()方法](#2.2. notify()方法)

[2.3. notifyAll()方法](#2.3. notifyAll()方法)


一、volatile关键字

1.1. 原理

当给变量添加了volatile关键字后,当编译器看到volatile的时候,就会提醒JVM运行的时候不进行上述的优化。具体来说,在读写volatile变量的前后指令添加"内存屏障相关的指令"。

1.2. Java memory model(Java内存模型)

首先一个Java进程,会有一个"主内存"存储空间,每个Java线程又会有自己的"工作内存"存储空间。形如下面的代码,t1进行flag变量的判定,就会把flag值从主内存先读取到工作内存,用工内存中的值进行判定。同时t2对flag进行修改,修改的则是主内存的值,主内存中的值不会影响到工作内存中的值。这里的工作内存相当于是打了个比方,本质上是CPU的寄存器和CPU的缓存构成的统称。

java 复制代码
import java.util.Scanner;
 
public class Demo1 {
    private static int flag = 0;
 
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {
            }
            System.out.println("t1线程结束");
        });
 
        Thread t2 = new Thread(() -> {
            Scanner in = new Scanner(System.in);
            System.out.println("请输入flag的值:");
            flag = in.nextInt();
            System.out.println("t2线程结束");
        });
 
        t1.start();
        t2.start();
 
        t1.join();
        t2.join();
    }
}

其实,存储数据,不光是只有内存,外存(硬盘)、CPU寄存器、CPU上的缓存。

上图中的缓存也是CPU上存储数据的单元。寄存器能存数据,但是空间小;内存能存数据,空间大,但是速度慢。为了能够更好地协调寄存器和内存的数据同步,因此现代CPU都引入了缓存。CPU的缓存,空间比寄存器要大,速度比内存快。

上图中,越往上,速度就越快,空间就越小,成本就越高。编译器优化,把本身从内存读取的值,优化成从寄存器或者L1缓存、L2缓存、L3缓存中读取。

编译器优化,并非是100%触发,根据不同的代码结构,可能产生出不同的优化效果。形如下面的代码

java 复制代码
import java.util.Scanner;

public class Demo2 {
    private static int flag = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1线程结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner in = new Scanner(System.in);
            System.out.println("请输入flag的值:");
            flag = in.nextInt();
            System.out.println("t2线程结束");
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

虽然没写volatile,但是加了sleep也不会触发上述优化:1. 循环速度大幅度降低了;2. 有了sleep之后,一次循环的瓶颈就不是load,在于sleep上,此时优化也没什么用;3. sleep本身会触发线程调度,调度过程触发上下文切换,再次加载也会触发这个值重新读取了。

如下代码,我们改为一个静态成员变量count,会发现count也会触发优化。

java 复制代码
import java.util.Scanner;

public class Demo2 {
    private static int flag = 0;
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {
                count++;
            }
            System.out.println("t1线程结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner in = new Scanner(System.in);
            System.out.println("请输入flag的值:");
            flag = in.nextInt();
            System.out.println("t2线程结束");
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

1.3. volatile与synchronized的区别

java 复制代码
public class Demo3 {
    private static volatile int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50_000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50_000; i++) {
                count++;
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(count);
    }
}

volatile这个关键字,能够解决内存可见性引起的线程安全问题,但是不具备原子性这样的特点。synchronized和volatile是两个不同的维度,synchronized是针对两个线程进行修改,而volatile是读取一个线程,另一个修改。

二、wait和notify

因为线程调度的顺序是不确定的,那我们就得保证每一种可能下都是正确的,就有点难搞了。我们之前提到过,join()方法可以控制线程的结束顺序。两个线程在运行的时候,我们希望是持续执行下去,但是两个线程中的某些环节,我们希望是能够有一定的顺序。

例如,假设有线程1和线程2。我们希望线程1先执行完某个逻辑后,再让线程2执行,此时就可以让线程2通过wait()主动进行阻塞,让线程1先参与调度。等线程1执行完对应的逻辑后,就可以通过notify()唤醒线程2。

另外wait和notify也能解决线程饿死的问题。线程饿死指的是在多线程编程中,某个或某些线程由于长时间无法获取到执行所需的资源,导致其任务迟迟无法完成,甚至永远无法执行的情况。 简而言之,就是某个线程"饿着肚子"等了很久,但一直没能得到"食物"。

如下图所示,当一个滑稽老铁进入ATM机里面取钱时,会进行上锁,其他滑稽老铁就必须在外面阻塞等待。当先进去的滑稽老铁发现ATM机里面没钱,便出去,而后又怀疑自己是不是看错了,于是又再次进入ATM机......如此循环往复,造成其他线程无法去CPU上执行,导致线程饿死。

线程饿死不像死锁那么严重。死锁发生之后,就会僵持住,除非程序重启,否则一直卡住。线程饿死,其他线程还是有一定机会拿到锁的,只是拿到锁的时间会延迟,降低程序的效率。

注意:wait、notify、notifyAll都是Object类里的方法。Java中任何一个类,都会有上述三种方法。

2.1. wait()方法

java 复制代码
public class Demo4 {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println("wait之前");
        o.wait();
        System.out.println("wait之后");
    }
}

在Java标准库中,但凡涉及到阻塞类的方法,都有可能抛出InterruptedException异常,所以我们这里也要在前面加上InterruptedException异常。

java 复制代码
public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        System.out.println("wait之前");
        o.wait();
        System.out.println("wait之后");
    }
}

但我们一运行程序之后,在打印"wait之前"语句后出现了IllegalMonitorStateException异常。此处的Monitor指的是sychronized,因为sychronized在JVM的底层实现被称为"监视器锁"。上面的异常是指锁的状态不符合预期。wait内部的第一件事就是释放锁,但释放锁的前提是得先拿到锁。像前面提到的滑稽老铁发现ATM机里面没有钱,如果滑稽老铁在里面等,意味着一直持有这个锁,其他人进不来。wait方法就要搭配sychronized使用。

java 复制代码
public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        System.out.println("wait之前");
        synchronized (o) {
            o.wait();
        }
        System.out.println("wait之后");
    }
}

此处的阻塞会持续进行,直到其他线程调用notify。

java 复制代码
import java.util.Scanner;

public class Demo5 {
    private static Object locker = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("t1等待之前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1等到之后");
        });

        Thread t2 = new Thread(() -> {
            Scanner in = new Scanner(System.in);
            System.out.println("请输入任意内容,唤醒t1");
            in.next();
            // 必须是同一个锁对象
            synchronized (locker) {
                locker.notify();
            }
        });

        t1.start();
        t2.start();
    }
}

wait要做的事情:

  • 使当前执行代码的线程进行等待(把线程放到等待队列中)
  • 释放当前的锁
  • 满⾜⼀定条件时被唤醒,重新尝试获取这个锁

使用wait的时候,阻塞其实是有两个阶段的:1. WAITING的阻塞,通过wait等待其他线程的通知;2. BLOCKED的阻塞,当收到通知之后,就会重新获取锁,可能又会遇到锁竞争。假设notify后面还有别的逻辑,此时锁就会多占用一会儿。

默认情况下,wait的阻塞是死等。wait也可以设置参数等待时间的上限。

java 复制代码
import java.util.Scanner;

/**
 * @author gao
 * @date 2025/7/9 20:54
 */

public class Demo6 {
    private static Object locker = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("t1等待之前");
            synchronized (locker) {
                try {
                    // t1在1000ms内没有收到任何通知,就会自动唤醒
                    locker.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1等待之后");
        });

        Thread t2 = new Thread(() -> {
            Scanner in = new Scanner(System.in);
            System.out.println("请输入任何内容,唤醒t1");
            in.next();
            synchronized (locker) {
                locker.notify();
            }
        });

        t1.start();
        t2.start();
    }
}

2.2. notify()方法

java 复制代码
import java.util.Scanner;

public class Demo7 {
    public static Object locker = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("t1等待之前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1等待之后");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("t2等待之前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t2等待之后");
        });

        Thread t3 = new Thread(() -> {
            System.out.println("t3等待之前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t3等待之后");
        });

        Thread t4 = new Thread(() -> {
            Scanner in = new Scanner(System.in);
            System.out.println("输入任意内容,唤醒一个线程:");
            in.next();
            synchronized (locker) {
                locker.notify();
            }
        });

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

这里我们就唤醒了t3线程,而其他两个线程还在阻塞等待。我们多运行几次,结果也会不同。注意,这里操作系统的随机调度并不是概率论里的概率均等,而是无法预测的。在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

2.3. notifyAll()方法

使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程。

java 复制代码
import java.util.Scanner;

public class Demo8 {
    private static Object locker = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("t1等待之前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1等待之后");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("t2等待之前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t2等待之后");
        });

        Thread t3 = new Thread(() -> {
            System.out.println("t3等待之前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t3等待之后");
        });

        Thread t4 = new Thread(() -> {
            Scanner in = new Scanner(System.in);
            System.out.println("输入任意内容,唤醒一个线程:");
            in.next();
            synchronized (locker) {
                locker.notifyAll();
            }
        });

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

如果没有任何对象在wait,凭空调用notify或者notifyAll也不会有任何副作用。

相关推荐
程序员的世界你不懂20 分钟前
基于Java+Maven+Testng+Selenium+Log4j+Allure+Jenkins搭建一个WebUI自动化框架(2)对框架加入业务逻辑层
java·selenium·maven
风吹落叶花飘荡23 分钟前
2025 Next.js项目提前编译并在服务器
服务器·开发语言·javascript
失败又激情的man1 小时前
python之requests库解析
开发语言·爬虫·python
专注VB编程开发20年1 小时前
常见 HTTP 方法的成功状态码200,204,202,201
开发语言·网络协议·tcp/ip·http
有没有没有重复的名字1 小时前
线程安全的单例模式与读者写者问题
java·开发语言·单例模式
开开心心_Every2 小时前
便捷的电脑自动关机辅助工具
开发语言·人工智能·pdf·c#·电脑·音视频·sublime text
程序员的世界你不懂3 小时前
基于Java+Maven+Testng+Selenium+Log4j+Allure+Jenkins搭建一个WebUI自动化框架(4)集成Allure报表
java·selenium·maven
isNotNullX3 小时前
数据中台架构解析:湖仓一体的实战设计
java·大数据·数据库·架构·spark
皮皮林5513 小时前
“RPC好,还是RESTful好?”,这个问题不简单
java
Xiaouuuuua3 小时前
一个简单的脚本,让pdf开启夜间模式
java·前端·pdf