一、内存模型与并发问题
1、Java内存模型基础
(1)Java内存模型(JMM)的抽象结构
- 在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。局部变量,方法定义参数和异常处理器参数不会在线程之间共享。
- 线程之间的通信由JMM控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
- 从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程已读/写共享变量的副本。
- 本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
线程A与线程B之间想要通信,必须经历下面2个步骤:
1、线程A把本地内存A中更新过的共享变量刷新到主内存中
2、线程B到内存中去读取线程A之前已更新过的共享变量
(2)共享内存的并发问题
一个线程对主内存中数据的更新并不会通知另一个线程,另一个线程可能基于本地内存中之前缓存的数据进行操作,造成并发问题。
/**
* @document: 共享内存并发问题
* @Author:SmallG
* @CreateTime:2023/8/11+9:20
*/
public class Demo09 {
public static void main(String[] args) {
//创建保存共享数据的对象
SharedData sharedData = new SharedData();
//启动线程修改共享变量数据 改为false
new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + "正在执行!");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//改变共享变量
sharedData.setFlagFalse();
System.out.println(name + "更新了共享数据,当前数据为:" + sharedData.flag);
}, "子线程").start();
//确保主线程一定能够得到数据
/**
* 子线程获取共享数据之后,会在自己的内存中创建一个副本,改变的是副本。
* 子线程改变的共享数据之后,会把数据提交到主内存当中。
* 主线程拿到数据之后,也会在自己的内存中创建一个副本,副本不会改变。
*/
boolean flag = sharedData.flag;
while (flag) {//主线程拿到的数据是个副本
//让主线程休眠5秒,重新读取主内存
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = sharedData.flag;
}
System.out.println("主线程读取到的共享数据:" + sharedData.flag); //true
}
}
//存储共享数据
class SharedData {
boolean flag = true;
//提供方法改变共享变量
public void setFlagFalse() {
flag = false;
}
}
2、volatile关键字
volatile关键字修饰成员属性,即规定线程对该变量的访问均需要从共享内存中获取,对该变量的修改也必须同步刷新到共享内存中,以保证资源的可见性。
/**
* @document: 共享内存并发问题
* @Author:SmallG
* @CreateTime:2023/8/11+9:20
*/
public class Demo09 {
public static void main(String[] args) {
//创建保存共享数据的对象
SharedData sharedData = new SharedData();
//启动线程修改共享变量数据 改为false
new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + "正在执行!");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//改变共享变量
sharedData.setFlagFalse();
System.out.println(name + "更新了共享数据,当前数据为:" + sharedData.flag);
}, "子线程").start();
//确保主线程一定能够得到数据
/**
* 子线程获取共享数据之后,会在自己的内存中创建一个副本,改变的是副本。
* 子线程改变的共享数据之后,会把数据提交到主内存当中。
* 主线程拿到数据之后,也会在自己的内存中创建一个副本,副本不会改变。
*/
while (sharedData.flag) {
}
System.out.println("主线程读取到的共享数据:" + sharedData.flag); //true
}
}
//存储共享数据
class SharedData {
volatile boolean flag = true; //volatile修饰的属性 是一个易变的数据,要求访问时进行注意
//提供方法改变共享变量
public void setFlagFalse() {
flag = false;
}
}
二、多线程协作
1、多线程协作概述
(1)狭义的线程同步
广义的线程同步被定义为一种机制,用于确保两个或多个并发的线程不会同时进入临界区。
狭义的线程同步在线程互斥的基础上增加了对多个线程执行顺序的要求,即两个或多个并发的线程应按照特定的顺序进入临界区。
可以简单地总结为,狭义的线程同步是一种强调执行顺序的线程互斥,也称为多线程协作。
2、线程同步
- wait():导致当前线程等待,并释放持有的锁;直到其他持有相同锁的线程调用notify()方法或notifyAll()方法来唤醒该线程
- notify():随机唤醒一个在此锁上等待的线程
- notifyAll():唤醒所有在此锁上等待的线程
/**
* @document: 线程通信
* wait()让一个线程处于等待状态(也是一种阻塞)
* notify()让一个等待状态的线程开始执行
* notifyAll()让所有等待线程开始执行
* 这三个方法来自 Object类 可供所有类型的线程使用
* @Author:SmallG
* @CreateTime:2023/8/11+10:36
*/
public class Demo10 {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number, "线程1");
Thread t2 = new Thread(number, "线程2");
t1.start();
t2.start();
}
}
class Number implements Runnable {
private int number = 1;
public void run() {
while (true) {
synchronized (this) {
//唤醒等待池中的一个线程,让这个线程进入锁池,等待当前线程释放锁
this.notify();
//获取线程名
String name = Thread.currentThread().getName();
//当前线程开始打印
if (number <= 10) {
System.out.println(name + " 打印:" + number);
number++;
} else {
break;
}
//打印完成之后 当前线程进入等待池 释放持有的锁
try {
TimeUnit.SECONDS.sleep(1);
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
等待阻塞状态
当一个线程因wait()方法进入阻塞状态时,该线程处于等待阻塞状态。当一个处于等待阻塞的线程被notify()或notifyAll()方法唤醒时,该线程先进入同步阻塞状态,得到锁后进入可运行状态。
3、经典案例:生产者消费者问题
生产者消费者问题,也称有限缓冲问题。该问题描述了共享固定大小缓冲区的多个线程-即所谓的“生产者”和“消费者”-在实际运行时会发生的问题。
要解决该问题,就必须让生产者在缓冲区满时休眠,等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。
/**
* @document: 餐厅 主类
* 创建柜台 容量是5
* 创建3名厨师
* 创建3名服务员
* 启动各个线程
* @Author:SmallG
* @CreateTime:2023/8/11+11:25
*/
public class Restaurant {
public static void main(String[] args) {
//创建柜台
Queue<Food> queue = new LinkedList<>();
new Cook(queue, "一号厨师").start();
new Cook(queue, "二号厨师").start();
new Cook(queue, "三号厨师").start();
new Waiter(queue, "一号服务员").start();
new Waiter(queue, "二号服务员").start();
new Waiter(queue, "二号服务员").start();
}
}
/**
* 创建食物类表示菜品
*/
class Food {
private static int counter = 0; //所有的菜共用一个计数器
private int i; //当前菜的编号
//构造方法
public Food() {
//修改计数器,并给菜编号
i = ++counter;
}
@Override
public String toString() {
return "第" + i + "道菜";
}
}
/**
* 让线程随机休眠一段时间的工具类
*/
class SleepUtil {
private static Random random = new Random();
public static void randomSleep() {
try {
TimeUnit.MILLISECONDS.sleep(random.nextInt(1000) + 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 创建一个厨师类 一名厨师是一个线程
*/
class Cook extends Thread {
//定义一个队列
private Queue<Food> queue; //柜台的队列
//构造方法
public Cook(Queue<Food> queue, String name) {
super(name); //为线程起名字
this.queue = queue;
}
@Override
public void run() {
while (true) {
SleepUtil.randomSleep(); //厨师炒菜的时间
Food food = new Food(); //炒了一道菜
System.out.println(getName() + "炒了一道菜" + food);
//菜放在柜台上,柜台是公共的资源,需要加锁
synchronized (queue) {
while (queue.size() >= 5) {
//柜台满了,厨师休息
System.out.println("柜台满了,当前有:" + queue.size() + "道菜," + getName() + "厨师等待中");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//柜台未满
queue.add(food); //把菜放入柜台
//告知所有服务员,端菜
queue.notifyAll();
}
}
}
}
/**
* 创建服务员 也是一个线程
*/
class Waiter extends Thread {
private Queue<Food> queue; //柜台
public Waiter(Queue<Food> queue, String name) {
super(name);
this.queue = queue;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.size() < 1) {
//柜台空了
System.out.println("当前有:" + queue.size() + "个菜," + getName() + "服务员等待");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//服务员端菜
Food food = queue.remove();
System.out.println(getName() + "获取到:" + food);
//叫醒所有厨师
queue.notifyAll();
}
SleepUtil.randomSleep(); //模拟服务员端菜时间
}
}
}
三、练习
1 多线程打印ABC
请基于多线程API编写一个程序,由3个线程在控制台交替打印ABC。
具体要求:
- 总计打印5组ABC
- 控制台中输出的内容要符合A、B、C的顺序
- 不能由一个线程连续输出多次
- 不限定必须由某个线程输出某个字母
程序运行效果如下所示:
/**
* @document: 多线程打印ABC
* @Author:SmallG
* @CreateTime:2023/8/11+13:12
*/
public class PrintABC {
public static void main(String[] args) {
Print print = new Print();
new Thread(print, "线程1").start();
new Thread(print, "线程2").start();
new Thread(print, "线程3").start();
}
}
class Print implements Runnable {
private int number = 1;
private int count = 0;
@Override
public void run() {
while (true) {
synchronized (this) {
this.notifyAll();
if (number>5){
break;
}
if (count ==0){
System.out.println("---------第"+number+"组---------");
System.out.println(Thread.currentThread().getName()+" : A");
}
if (count ==1){
System.out.println(Thread.currentThread().getName()+" : B");
}
if (count ==2){
System.out.println(Thread.currentThread().getName()+" : C");
}
if (count ==3){
count = 0;
number++;
continue;
}
count++;
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
2 【简答题】volatile
请简明的介绍一下volatile关键字,再展开介绍一下它和synchronized关键字的区别。
volatile 是 Java 中的关键字,用于修饰变量。它保证了线程之间对该变量的可见性,即当一个线程修改了被 volatile 修饰的变量的值时,其他线程能够立即看到最新的值,而不是使用缓存中的旧值。同时,volatile 也可以防止指令重排优化。在多线程环境下,使用 volatile 可以确保共享变量在多个线程之间的一致性。
volatile 和 synchronized 关键字的区别:
1、用途:
- volatile 关键字用于保证共享变量的可见性,它适用于那些不涉及原子性操作的场景。
- synchronized 关键字用于实现线程之间的同步,可以确保多个线程对共享资源的访问具有原子性、可见性和有序性。
2、原子性:
- volatile 关键字只保证可见性,不保证原子性。即单独使用 volatile 不能解决多线程环境下的原子性问题,需要额外的同步手段。
- synchronized 关键字可以保证代码块的原子性,同一时刻只有一个线程可以进入被 synchronized 修饰的代码块。
3、锁机制:
- volatile 不涉及锁机制,因此效率相对较高。
- synchronized 通过锁机制来实现同步,它可以保证多个线程之间的互斥访问,但可能引起线程阻塞和上下文切换,影响性能。
4、适用范围:
- volatile 适用于那些对变量的写操作不依赖于当前值的场景,例如标志位的判断、停止线程等。
- synchronized 更加通用,适用于任何复杂的同步需求,可以用于保护任意代码块,但需要更谨慎地使用,以避免死锁和性能问题。
5、使用灵活性:
- volatile 使用较为简单,只需要在共享变量上添加关键字即可。
- synchronized 使用较为复杂,需要手动获取和释放锁,同时考虑锁的粒度和范围,以避免不必要的同步开销。
3 【简答题】sleep和wait
请介绍一下sleep()方法和wait()方法的区别是什么?
sleep() 方法和 wait() 方法是 Java 中用于线程控制的两个重要方法,它们用于控制线程的暂停和等待。它们的主要区别如下:
1、来源:
- sleep() 方法是 Thread 类的静态方法,用于暂停当前正在执行的线程。
- wait() 方法是 Object 类的实例方法,用于使当前线程进入等待状态,并释放持有的锁。
2、执行的位置:
- sleep() 方法可以在任何地方使用,不依赖于对象的监视器。
- wait() 方法必须在同步代码块(synchronized block)内部使用,即在持有锁的情况下调用。
3、锁的释放:
- sleep() 方法不会释放锁,线程在暂停期间仍然持有锁。
- wait() 方法在调用时会释放持有的锁,使得其他线程可以进入同步代码块。
4、唤醒方式:
- sleep() 方法会暂停当前线程一段指定的时间后自动唤醒。
- wait() 方法需要被其他线程显式地调用 notify() 或 notifyAll() 方法来唤醒等待的线程。
5、使用场景:
- sleep() 方法主要用于模拟暂停、延迟等操作,没有特定的同步需求。
- wait() 方法主要用于线程间的通信和协调,需要配合 synchronized 关键字使用来保证同步和线程安全。
4 多线程打印ABC(进阶)
请基于多线程API编写一个程序,由3个线程在控制台交替打印ABC。
具体要求:
- 将三个线程的名字分别设置为A、B、C,每个线程输出的是自己的名字
- 总计打印5组ABC
- 控制台中输出的内容要符合A、B、C的顺序
- 可以在main方法中控制三个线程的启动顺序
提示:可以使用wait方法、notifyAll方法搭配双层的同步代码块来实现。
程序运行效果如下所示:
/**
* @document: 多线程打印ABC(进阶)
* @Author:SmallG
* @CreateTime:2023/8/11+18:31
*/
public class homework02 {
public static void main(String[] args) throws InterruptedException {
Object a = new Object();
Object b = new Object();
Object c = new Object();
ABC a1 = new ABC(c, a, "A");
ABC b1 = new ABC(a, b, "B");
ABC c1 = new ABC(b, c, "C");
new Thread(a1, "A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(b1, "B").start();
TimeUnit.SECONDS.sleep(1);
new Thread(c1, "C").start();
}
}
class ABC implements Runnable {
private Object pre; //上一个锁
private Object now; //当前的锁
private String name;
public ABC(Object pre, Object now, String name) {
this.pre = pre;
this.now = now;
this.name = name;
}
@Override
public void run() {
int count = 1;
while (count < 6) {
synchronized (pre) {
synchronized (now) {
if (name.equals("A")) {
System.out.println("-------第" + count + "组-------");
}
System.out.println(name);
count++;
now.notifyAll();
}
if (count > 5) {
pre.notifyAll();
} else {
try {
pre.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}