多线程基础技术②

1.线程同步

1.1 synchronized 同步方法

使用程技术时一定会遇到的经典问题。“非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据其实是被更改过的。而“线程安全”就是以获得的实例变量的值是经过同步处理的,不会出现脏读的现象。

1.1.1方法内的变量为线程安全

“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题,所得结果也就是“线程安全”的了。

-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
public void addI(String username){
try {
int num = 0;
if (username.equals("a")){
num=100;
System.out.println("a 赋值成功!");
Thread.sleep(2000);
}else {
num = 200;
System.out.println("b 赋值成功!");
}
System.out.println(username + " num="+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadA extends Thread{
private Test test;
public ThreadA(Test test) {
super();
this.test = test;
}
@Override
public void run() {
super.run();
test.addI("a");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadB extends Thread{
private Test test;
public ThreadB(Test test) {
super();
this.test = test;
}
@Override
public void run() {
super.run();
test.addI("b");
}
}
1
2
3
4
5
6
7
8
9
10
public class Run2 {
public static void main(String[] args) {
Test test= new Test();
ThreadA a = new ThreadA(test);
a.start();
ThreadB b = new ThreadB(test);
b.start();
}
}

image-20221121110928851

方法中的变量不存在非线程安全问题,永远都是线程安全的。这是方法内部的变量是私有的特性造成的。

1.1.2方法外的实例变量非线程安全

如果多个线程共同访问1个对象中的实例变量,则有可能出现“非线程安全”问题

我们把上面的 num变量放到方外

1
2
3
4
5
public class Test {
private int num = 0;
public void addI(String username){
.....
}

image-20221121111716996

结果大相径庭,就会出现非线程安全的问题,数据随机读取

1.1.3 synchronized 同步方法解决线程安全

  • 同步方法的格式

    同步方法:就是把synchronized关键字加到方法上

    1
    2
    3
    修饰符 synchronized 返回值类型 方法名(方法参数) { 
    方法体;
    }

    同步方法的锁对象是什么呢?

    this
    
  • 静态同步方法

    同步静态方法:就是把synchronized关键字加到静态方法上

    1
    2
    3
    修饰符 static synchronized 返回值类型 方法名(方法参数) { 
    方法体;
    }

    同步静态方法的锁对象是什么呢?

    类名.class
    

    代码修改

    1
    2
    3
    4
    5
    public class Test {
    private int num = 0;
    synchronized public void addI(String username){
    .....
    }

    测试:

    image-20221121112107235

    实验结论:在两个线程访问同一个对象中的同步方法时一定是线程安全的。本实验由于是同步访问,所以先打印出 a,然后打印出 b。

1.1.4 多个对象和多个锁

你是否想过为什么1.1.2的打印会把a打印完再打印b,创建不同业务对象会不会有不同变化?

改写代码:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
//创建两个对象
Test test1= new Test();
Test test2= new Test();
ThreadA a = new ThreadA(test1);
a.start();
ThreadB b = new ThreadB(test2);
b.start();
}
image-20221121113129001

结果是异步打印的,为什么?

​ 关键字 synchronized 取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁所以在上面的示例中,哪个线程先执行带synchronized 关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。
​ 但如果多个线程访问多个对象,则JVM会创建多个锁。上面的示例就是创建了2个Test.java类的对象,所以就会产生出2个锁

同步的单词为synchronized.异步的单词为asynchronized

1.1.5 脏读

避免数据出现交叉的情况使用synchronized关键字来进行同步。
虽然在赋值时进行了同步,但在取值时有可能出现一些意想不到的意外,这种情况就是脏读(dirtyRead)。发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PublicVar {
public String username="A";
public String password = "AA";
synchronized public void setValue(String username,String password){
try {
this.username=username;
Thread.sleep(5000);
this.password = password;
System.out.println("setValue method thread name="
+Thread.currentThread().getName() + "username=" + username + "password="+password); } catch (InterruptedException e) {
e.printStackTrace();
}
}
public void getValue(){
System.out.println("getValue method thread name="
+Thread.currentThread().getName() +
"username=" + username + "password="+password);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

public class ThreadA extends Thread{
private PublicVar publicVar;
public ThreadA(PublicVar publicVar) {
super();
this.publicVar= publicVar;
}
@Override
public void run() {
super.run();
publicVar.setValue("B","BB");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {
try {
PublicVar publicVarRef=new PublicVar();
ThreadA threadA=new ThreadA(publicVarRef);
threadA.start();
ThreadA.sleep(200);
publicVarRef.getValue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

image-20221121212850461

出现脏读是因为 public void getValue0 方法并不是同步的,所以可以在任意时候进行调用。解决办法当然就是加上同步synchronized 关键字,代码如下:

我们给getValue方法添加synchronized关键字

1
2
3
4
5
synchronized  public  void getValue(){
System.out.println("getValue method thread name="
+Thread.currentThread().getName() +
" username=" + username + " password="+password);
}

image-20221121214552871

可见,方法 setValue0和getValue0 被依次执行。通过这个案例不仅要知道脏读是通过synchronized关键字解决的。

脏读一定会出现操作实例变量的情况下,这就是不同线程“争抢”实例变量的结果。

1.1.6 synchronized 锁重入

锁重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞

synchronized 拥有锁重入的功能,使用synchronized 时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronized 方法/块的内部调用本类的其他synchronized 方法/块时,是永远可以得到锁的。

1.2 同步代码块【应用】

安全问题出现的条件

    • 是多线程环境

    • 有共享数据

    • 有多条语句操作共享数据

  • 如何解决多线程安全问题呢?

    • 基本思想:让程序没有安全问题的环境
  • 怎么实现呢?

    • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可

    • Java提供了同步代码块的方式来解决

  • 同步代码块格式:

    1
    2
    3
    synchronized(任意对象) { 
    多条语句操作共享数据的代码
    }

    synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁

  • 同步的好处和弊端

    • 好处:解决了多线程的数据安全问题

    • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    public class ObjectService {
    public void method1() {
    try {
    synchronized (this){
    System.out.println("begin time=" + System.currentTimeMillis());
    Thread.sleep(2000);
    System.out.println("end time="+System.currentTimeMillis());
    }
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class ThreadA extends Thread{
    private ObjectService service;
    public ThreadA(ObjectService service) {
    super();
    this.service = service;
    }
    @Override
    public void run() {
    super.run();
    service.method1();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class ThreadB extends Thread{
    private ObjectService service;
    public ThreadB(ObjectService service) {
    super();
    this.service = service;
    }
    @Override
    public void run() {
    super.run();
    service.method1();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Run {
    public static void main(String[] args) {
    ObjectService service = new ObjectService();
    ThreadA a=new ThreadA(service);
    a.setName("a");
    a.start();
    ThreadB b = new ThreadB(service);
    b.setName("b");
    b.start();
    }
    }

image-20221122213502739

上面的实验中虽然使用了synchronized 同步代码块,但执行的效率还是没有提高,执行的效果还是同步运行的。如何用synchronized同步代码块解决程序执行效率低的问题呢?

1.2.1 同步和异步

为证明不在synchronized 块中就是异步执行,在synchronized块中就是同步执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Task {
public void doLongTimeTask() {
for (int i = 0; i <100 ; i++) {
System.out.println("Nosynchronized threadName="
+Thread.currentThread().getName() + " i="+ (i+1));
}
System.out.println("");
synchronized (this) {
for (int i = 0; i < 100; i++) {
System.out.println("synchronized threadName="
+Thread.currentThread().getName() + " i="+(i+1));
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyThread1 extends Thread{
private Task task;
public MyThread1(Task task) {
super();
this.task = task;
}
@Override
public void run() {
super.run();
task.doLongTimeTask();
}
}

public class MyThread2 extends Thread{
private Task task;
public MyThread2(Task task) {
super();
this.task = task;
}

@Override
public void run() {
super.run();
task.doLongTimeTask();
}
}

1
2
3
4
5
6
7
8
9
public class Run {
public static void main(String[] args) {
Task task = new Task();
MyThread1 thread1 = new MyThread1(task);
thread1.start();
MyThread2 thread2 = new MyThread2(task);
thread2.start();
}
}

进入synchronized代码块后则排队执行,

image-20221124120349442 image-20221124120422429

注意:在使用同步synchronized(this)代码块时需要注意的是,线程访问object的synchronized(this)同步代码块时,其他线程对同一个object 中所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized 使用的“对象监视器”是一个。

1.2.2 死锁

Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。在多线程技术中,“死锁”是必须避免的,因为这会造成线程的“假死”

  • 概述

    线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行

  • 什么情况下会产生死锁

    1. 资源有限
    2. 同步嵌套
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class Demo {
    public static void main(String[] args) {
    Object objA = new Object();
    Object objB = new Object();

    new Thread(()->{
    while(true){
    synchronized (objA){
    //线程一
    synchronized (objB){
    System.out.println("小康同学正在走路");
    }
    }
    }
    }).start();

    new Thread(()->{
    while(true){
    synchronized (objB){
    //线程二
    synchronized (objA){
    System.out.println("小薇同学正在走路");
    }
    }
    }
    }).start();
    }
    }

2.生产者消费者

2.1生产者和消费者模式概述【应用】

  • 概述

    生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。

    所谓生产者消费者问题,实际上主要是包含了两类线程:

    一类是生产者线程用于生产数据
    
    一类是消费者线程用于消费数据
    

    为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

    生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为

    消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

  • Object类的等待和唤醒方法

    方法名 说明
    void wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
    void notify() 唤醒正在等待对象监视器的单个线程
    void notifyAll() 唤醒正在等待对象监视器的所有线程

2.2生产者和消费者案例【应用】

  • 案例需求

    • 桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量

    • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

      1.判断是否有包子,决定当前线程是否执行

      2.如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子

      3.生产包子之后,更新桌子上包子状态,唤醒消费者消费包子

    • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务

      1.判断是否有包子,决定当前线程是否执行

      2.如果没有包子,就进入等待状态,如果有包子,就消费包子

      3.消费包子后,更新桌子上包子状态,唤醒生产者生产包子

    • 测试类(Demo):里面有main方法,main方法中的代码步骤如下

      创建生产者线程和消费者线程对象

      分别开启两个线程

  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    public class Desk {

    //定义一个标记
    //true 就表示桌子上有汉堡包的,此时允许吃货执行
    //false 就表示桌子上没有汉堡包的,此时允许厨师执行
    public static boolean flag = false;

    //汉堡包的总数量
    public static int count = 10;

    //锁对象
    public static final Object lock = new Object();
    }

    public class Cooker extends Thread {
    // 生产者步骤:
    // 1,判断桌子上是否有汉堡包
    // 如果有就等待,如果没有才生产。
    // 2,把汉堡包放在桌子上。
    // 3,叫醒等待的消费者开吃。
    @Override
    public void run() {
    while(true){
    synchronized (Desk.lock){
    if(Desk.count == 0){
    break;
    }else{
    if(!Desk.flag){
    //生产
    System.out.println("厨师正在生产汉堡包");
    Desk.flag = true;
    Desk.lock.notifyAll();
    }else{
    try {
    Desk.lock.wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }
    }
    }
    }

    public class Foodie extends Thread {
    @Override
    public void run() {
    // 1,判断桌子上是否有汉堡包。
    // 2,如果没有就等待。
    // 3,如果有就开吃
    // 4,吃完之后,桌子上的汉堡包就没有了
    // 叫醒等待的生产者继续生产
    // 汉堡包的总数量减一

    //套路:
    //1. while(true)死循环
    //2. synchronized 锁,锁对象要唯一
    //3. 判断,共享数据是否结束. 结束
    //4. 判断,共享数据是否结束. 没有结束
    while(true){
    synchronized (Desk.lock){
    if(Desk.count == 0){
    break;
    }else{
    if(Desk.flag){
    //有
    System.out.println("吃货在吃汉堡包");
    Desk.flag = false;
    Desk.lock.notifyAll();
    Desk.count--;
    }else{
    //没有就等待
    //使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
    try {
    Desk.lock.wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }
    }

    }
    }

    public class Demo {
    public static void main(String[] args) {
    /*消费者步骤:
    1,判断桌子上是否有汉堡包。
    2,如果没有就等待。
    3,如果有就开吃
    4,吃完之后,桌子上的汉堡包就没有了
    叫醒等待的生产者继续生产
    汉堡包的总数量减一*/

    /*生产者步骤:
    1,判断桌子上是否有汉堡包
    如果有就等待,如果没有才生产。
    2,把汉堡包放在桌子上。
    3,叫醒等待的消费者开吃。*/

    Foodie f = new Foodie();
    Cooker c = new Cooker();

    f.start();
    c.start();

    }
    }

2.3生产者和消费者案例优化【应用】

  • 需求

    • 将Desk类中的变量,采用面向对象的方式封装起来
    • 生产者和消费者类中构造方法接收Desk类对象,之后在run方法中进行使用
    • 创建生产者和消费者线程对象,构造方法中传入Desk类对象
    • 开启两个线程
  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    public class Desk {

    //定义一个标记
    //true 就表示桌子上有汉堡包的,此时允许吃货执行
    //false 就表示桌子上没有汉堡包的,此时允许厨师执行
    //public static boolean flag = false;
    private boolean flag;

    //汉堡包的总数量
    //public static int count = 10;
    //以后我们在使用这种必须有默认值的变量
    // private int count = 10;
    private int count;

    //锁对象
    //public static final Object lock = new Object();
    private final Object lock = new Object();

    public Desk() {
    this(false,10); // 在空参内部调用带参,对成员变量进行赋值,之后就可以直接使用成员变量了
    }

    public Desk(boolean flag, int count) {
    this.flag = flag;
    this.count = count;
    }

    public boolean isFlag() {
    return flag;
    }

    public void setFlag(boolean flag) {
    this.flag = flag;
    }

    public int getCount() {
    return count;
    }

    public void setCount(int count) {
    this.count = count;
    }

    public Object getLock() {
    return lock;
    }

    @Override
    public String toString() {
    return "Desk{" +
    "flag=" + flag +
    ", count=" + count +
    ", lock=" + lock +
    '}';
    }
    }

    public class Cooker extends Thread {

    private Desk desk;

    public Cooker(Desk desk) {
    this.desk = desk;
    }
    // 生产者步骤:
    // 1,判断桌子上是否有汉堡包
    // 如果有就等待,如果没有才生产。
    // 2,把汉堡包放在桌子上。
    // 3,叫醒等待的消费者开吃。

    @Override
    public void run() {
    while(true){
    synchronized (desk.getLock()){
    if(desk.getCount() == 0){
    break;
    }else{
    //System.out.println("验证一下是否执行了");
    if(!desk.isFlag()){
    //生产
    System.out.println("厨师正在生产汉堡包");
    desk.setFlag(true);
    desk.getLock().notifyAll();
    }else{
    try {
    desk.getLock().wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }
    }
    }
    }

    public class Foodie extends Thread {
    private Desk desk;

    public Foodie(Desk desk) {
    this.desk = desk;
    }

    @Override
    public void run() {
    // 1,判断桌子上是否有汉堡包。
    // 2,如果没有就等待。
    // 3,如果有就开吃
    // 4,吃完之后,桌子上的汉堡包就没有了
    // 叫醒等待的生产者继续生产
    // 汉堡包的总数量减一

    //套路:
    //1. while(true)死循环
    //2. synchronized 锁,锁对象要唯一
    //3. 判断,共享数据是否结束. 结束
    //4. 判断,共享数据是否结束. 没有结束
    while(true){
    synchronized (desk.getLock()){
    if(desk.getCount() == 0){
    break;
    }else{
    //System.out.println("验证一下是否执行了");
    if(desk.isFlag()){
    //有
    System.out.println("吃货在吃汉堡包");
    desk.setFlag(false);
    desk.getLock().notifyAll();
    desk.setCount(desk.getCount() - 1);
    }else{
    //没有就等待
    //使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
    try {
    desk.getLock().wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }
    }

    }
    }

    public class Demo {
    public static void main(String[] args) {
    /*消费者步骤:
    1,判断桌子上是否有汉堡包。
    2,如果没有就等待。
    3,如果有就开吃
    4,吃完之后,桌子上的汉堡包就没有了
    叫醒等待的生产者继续生产
    汉堡包的总数量减一*/

    /*生产者步骤:
    1,判断桌子上是否有汉堡包
    如果有就等待,如果没有才生产。
    2,把汉堡包放在桌子上。
    3,叫醒等待的消费者开吃。*/

    Desk desk = new Desk();

    Foodie f = new Foodie(desk);
    Cooker c = new Cooker(desk);

    f.start();
    c.start();

    }
    }

2.4阻塞队列基本使用【理解】

阻塞队列继承结构

06_阻塞队列继承结构

  • 常见BlockingQueue:

    ArrayBlockingQueue: 底层是数组,有界

    LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值

  • BlockingQueue的核心方法:

    put(anObject): 将参数放入队列,如果放不进去会阻塞

    take(): 取出第一个数据,取不到会阻塞

  • 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Demo02 {
    public static void main(String[] args) throws Exception {
    // 创建阻塞队列的对象,容量为 1
    ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);

    // 存储元素
    arrayBlockingQueue.put("汉堡包");

    // 取元素
    System.out.println(arrayBlockingQueue.take());
    System.out.println(arrayBlockingQueue.take()); // 取不到会阻塞

    System.out.println("程序结束了");
    }
    }

2.5 阻塞队列实现等待唤醒机制【理解】

  • 案例需求

    • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

      1.构造方法中接收一个阻塞队列对象

      2.在run方法中循环向阻塞队列中添加包子

      3.打印添加结果

    • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务

      1.构造方法中接收一个阻塞队列对象

      2.在run方法中循环获取阻塞队列中的包子

      3.打印获取结果

    • 测试类(Demo):里面有main方法,main方法中的代码步骤如下

      创建阻塞队列对象

      创建生产者线程和消费者线程对象,构造方法中传入阻塞队列对象

      分别开启两个线程

  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    public class Cooker extends Thread {

    private ArrayBlockingQueue<String> bd;

    public Cooker(ArrayBlockingQueue<String> bd) {
    this.bd = bd;
    }
    // 生产者步骤:
    // 1,判断桌子上是否有汉堡包
    // 如果有就等待,如果没有才生产。
    // 2,把汉堡包放在桌子上。
    // 3,叫醒等待的消费者开吃。

    @Override
    public void run() {
    while (true) {
    try {
    bd.put("汉堡包");
    System.out.println("厨师放入一个汉堡包");
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }

    public class Foodie extends Thread {
    private ArrayBlockingQueue<String> bd;

    public Foodie(ArrayBlockingQueue<String> bd) {
    this.bd = bd;
    }

    @Override
    public void run() {
    // 1,判断桌子上是否有汉堡包。
    // 2,如果没有就等待。
    // 3,如果有就开吃
    // 4,吃完之后,桌子上的汉堡包就没有了
    // 叫醒等待的生产者继续生产
    // 汉堡包的总数量减一

    //套路:
    //1. while(true)死循环
    //2. synchronized 锁,锁对象要唯一
    //3. 判断,共享数据是否结束. 结束
    //4. 判断,共享数据是否结束. 没有结束
    while (true) {
    try {
    String take = bd.take();
    System.out.println("吃货将" + take + "拿出来吃了");
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }

    }
    }

    public class Demo {
    public static void main(String[] args) {
    ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);

    Foodie f = new Foodie(bd);
    Cooker c = new Cooker(bd);

    f.start();
    c.start();
    }
    }

3. Java线程间的通信

3.1等待/ 通知机制

等待/通知机制在生活中比比皆是,比如在就餐时就会出现,如图所示厨师和服务员之间的交互要在“菜品传递台”上,在这期间会有几个问题:

1)厨师做完一道菜的时间不确定,所以厨师将菜品放到“菜品传递台”上的时间也不确定

image-20221125205607505

2)服务员取到菜的时间取决于厨师,所以服务员就有“等待”(wait)的状态。

3)服务员如何能取到菜呢?这又得取决于厨师,厨师将菜放在“菜品传递台”上,其实就相当于一种通知(notify),这时服务员才可以拿到菜并交给就餐者。
4)在这个过程中出现了“等待/通知”机制。


多线程基础技术②
https://1902756969.github.io/Hexo/2022/11/30/多线程/java多线程基础技术②/
作者
发布于
2022年11月30日
许可协议