【JavaEE】【多线程】volatile,wait/notify

目录

  • 一、volatile关键字
    • [1.1 内存可见性](#1.1 内存可见性)
    • [1.2 volatile解决内存可见性问题](#1.2 volatile解决内存可见性问题)
  • 二、wait和notify
    • [2.1 wait](#2.1 wait)
    • [2.2 notify](#2.2 notify)
    • [2.3 使用例子](#2.3 使用例子)
      • [2.3.1 例子1](#2.3.1 例子1)
      • [2.3.2 例子二](#2.3.2 例子二)

一、volatile关键字

volatile可以保证内存可见性,只能修饰变量。

1.1 内存可见性

在前面介绍线程不安全原因时介绍到了,在Java中有JMM (Java Memory Model)(Java内存模型)来介绍。

计算机运行代码/程序的时候,访问数据常常要从内存中访问(定义变量时变量就储存在内存中),

然而CPU从内存中读取数据相比于从寄存器中读取数据要慢上很多(几千上万倍),CPU在进行读/写内存的时候速度就会降低。

为了解决这种问题,提高效率,编译器就可能会对代码优化,把一些本来要读取内存的操作,优化为读取寄存器,减少读取内存的次数。这就会导致内存可见性问题。

例如以下代码输入一个不为0的数,本应该打印"threade1结束",但是并没有。

java 复制代码
import java.util.Scanner;
public class Demo {
    public static int isQuite = 0;
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
           while(isQuite == 0) {
               
           }
            System.out.println("threade1结束");
        }) ;
        Thread thread2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            isQuite = scanner.nextInt();
        });
        thread1.start();
        thread2.start();
    }
}

以上述代码讲解:

在thread1中while先读取isQuite的值,在进行比较,然而编译器/JVM发现多次得到的isQuite都是0,这个线程也没有修改isQuite操作,然后编译器/JVM就大胆优化只进行第一次的读取isQuite操作,后续直接从寄存器里面读取。

其实编译器/JVM进行优化是不可控的,如果在while循环里面加上sleep,sleep的时间够久了,已经够进行读取操作,可能就不会优化了。

1.2 volatile解决内存可见性问题

如上诉代码,我们直接在isQuite加上volatile修饰,就告诉编译器/JVM不要进行优化,就可以解决问题。

volatile可以解决内存可见性问题,解决不了原子性问题。

java 复制代码
import java.util.Scanner;
public class Demo {
	volatile public static int isQuite = 0;
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
           while(isQuite == 0) {
               
           }
            System.out.println("threade1结束");
        }) ;
        Thread thread2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            isQuite = scanner.nextInt();
        });
        thread1.start();
        thread2.start();
    }
}

二、wait和notify

在多线程中一个重要的机制是协调各个线程之间的调度顺序,在操作系统是随机调度。

在前面我们介绍了等待一个线程使用join,但是使用join就要等到调用线程结束。而wait不用,wait和notify就是专门协调线程执行逻辑的顺序的。
wait和notify是Object类的成员方法,也就是每一个对象都有。

wait:等待,让指定线程进入阻塞状态。

notify:通知,唤醒对应进入阻塞状态的线程。

2.1 wait

线程饿死/饿死:线程恶死就是指当多个线程竞争一把锁的时候,当线程1拿到了锁,释放锁之后,又由于操作系统的随机调度再次多次让线程1拿到锁,其他线程多次没拿到锁处于阻塞状态,没分配到CPU资源。就相当于鸟妈妈给小鸟喂食,多次喂食都是给一只小鸟,那么其它小鸟就处于饥饿/饿死状态。

我们可以使用wait来避免线程饥饿,当线程拿到锁发现时机不成熟的时候,就可以使用wait让线程进入阻塞状态,等待唤醒。

语法:

java 复制代码
synchronized(锁对象) {
	锁对象.wait();
}

注意事项:

  • wait()必须搭配synchronized使用:因为wait()的机制就是先释放锁对象的锁,然后等待唤醒在加锁继续执行剩下逻辑。
  • 如果对象没有处于加锁状态,就会抛出IllegalMonitorStateException(非法锁状态异常)。
  • wait会抛InterruptedException
  • wait也有含时间版本,超过时间自动唤醒。

2.2 notify

notify就是唤醒wait。

语法:

java 复制代码
synchronized(锁对象) {
	锁对象.notify();
}

注意事项:

  • notify必须在wait后面执行才能唤醒wait;
  • notify和要唤醒的wait要是同一个锁对象;
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")。
  • 当有多个线程等待时,可以使用notifyAll()来唤醒所有,但是实际上还是一个一个唤醒。

2.3 使用例子

2.3.1 例子1

题目:使用多线程来打印ABC,一个线程一个字母,打印10个ABC。

解析:线程1唤醒线程2,线程2唤醒线程3,线程3唤醒线程1。主线程中保证先唤醒一下线程1即可。

代码:

java 复制代码
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Object block1 = new Object();
        Object block2 = new Object();
        Object block3 = new Object();

        Thread thread1 = new Thread(() -> {
                try {
                    for (int i = 0; i < 10; i++) {
                        synchronized (block1) {
                            block1.wait();
                        }
                        System.out.print("A");
                        synchronized (block2) {
                            block2.notify();
                        }
                    }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread thread2 = new Thread(() -> {
                try {
                    for (int i = 0; i < 10; i++) {
                        synchronized (block2) {
                            block2.wait();
                        }
                        System.out.print("B");
                        synchronized (block3) {
                            block3.notify();
                        }
                    }
            }catch (InterruptedException e) {
                    e.printStackTrace();
                }

        });
        Thread thread3 = new Thread(() -> {
                try {
                    for (int i = 0; i < 10; i++) {
                        synchronized (block3) {
                            block3.wait();
                        }
                        System.out.print("C"+" ");
                        synchronized (block1) {
                            block1.notify();
                        }
                    }
            }catch (InterruptedException e) {
                    e.printStackTrace();
                }

        });

        thread1.start();
        thread2.start();
        thread3.start();

        Thread.sleep(1000);
        synchronized (block1) {
            block1.notify();
        }
    }
}

2.3.2 例子二

题目:有三个线程,线程名称分别为:a,b,c。

每个线程打印自己的名称。

需要让他们同时启动,并按 c,b,a的顺序打印。

代码:

java 复制代码
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Object block1 = new Object();
        Object block2 = new Object();
        Object block3 = new Object();

        Thread a = new Thread( () -> {
            Thread.currentThread().setName("a");
            try{
                synchronized (block1) {
                    block1.wait();
                }
                System.out.println(Thread.currentThread().getName());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        });
        Thread b = new Thread(() -> {
            Thread.currentThread().setName("b");
            try{
                synchronized (block2) {
                    block2.wait();
                }
                System.out.print(Thread.currentThread().getName()+",");

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (block1) {
                block1.notify();
            }
        });
        Thread c = new Thread(() -> {
            Thread.currentThread().setName("c");
            try{
                synchronized (block3) {
                    block3.wait();
                }
                System.out.print(Thread.currentThread().getName()+",");
                synchronized (block2) {
                    block2.notify();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        a.start();
        b.start();
        c.start();
        Thread.sleep(1000);
        synchronized (block3) {
            block3.notify();
        }
    }
}
相关推荐
HanhahnaH12 分钟前
Spring集合注入Bean
java·spring
未定义.22118 分钟前
电子削铅笔刀顺序图详解:从UML设计到PlantUML实现
java·软件工程·uml
雾月5535 分钟前
LeetCode 1292 元素和小于等于阈值的正方形的最大边长
java·数据结构·算法·leetcode·职场和发展
24k小善2 小时前
Flink TaskManager详解
java·大数据·flink·云计算
想不明白的过度思考者2 小时前
Java从入门到“放弃”(精通)之旅——JavaSE终篇(异常)
java·开发语言
.生产的驴2 小时前
SpringBoot 封装统一API返回格式对象 标准化开发 请求封装 统一格式处理
java·数据库·spring boot·后端·spring·eclipse·maven
猿周LV2 小时前
JMeter 安装及使用 [软件测试工具]
java·测试工具·jmeter·单元测试·压力测试
晨集2 小时前
Uni-App 多端电子合同开源项目介绍
java·spring boot·uni-app·电子合同
时间之城2 小时前
笔记:记一次使用EasyExcel重写convertToExcelData方法无法读取@ExcelDictFormat注解的问题(已解决)
java·spring boot·笔记·spring·excel
椰羊~王小美2 小时前
LeetCode -- Flora -- edit 2025-04-25
java·开发语言