传统的生产者消费者问题

HeJin大约 4 分钟JavaJUC并发编程

面试必问:单例模式、排序算法、生产者消费者、死锁。

大有门道。

代码

/**
 * @Desc 线程之间的通信问题:生产者消费者问题 等待唤醒,通知唤醒
 * 线程交替执行:A B操作同一个变量 num = 0
 * A num+1
 * B num-1
 * @Author HeJin
 * @Date 2021/3/11 12:23
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}

/**
 * 资源类
 * 判断等待、业务、通知
 */
class Data{
    private int num = 0;

    public synchronized void increment() throws InterruptedException {
        if (num != 0){
            // 等待
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"==>"+num);
        // 通知其他线程 +1完毕
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        if (num == 0){
            // 等待
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName()+"==>"+num);
        // 通知其他线程 -1完毕
        this.notifyAll();
    }
}

执行结果:

A==>1
B==>0
A==>1
B==>0
A==>1
B==>0
A==>1
B==>0
A==>1
B==>0
A==>1
B==>0
A==>1
B==>0
A==>1
B==>0
A==>1
B==>0
A==>1
B==>0

Process finished with exit code 0

代码存在的问题:如果有A、B、C、D 4个线程,还安全吗?

两个加线程,两个减线程:

public class A {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

测试发现,线程不安全。

A==>1
B==>0
A==>1
B==>0
A==>1
B==>0
A==>1
B==>0
C==>1
A==>2
D==>1
D==>0
A==>1
C==>2
B==>1
B==>0
C==>1
A==>2
D==>1
D==>0
A==>1
C==>2
B==>1
B==>0
C==>1
A==>2
D==>1
D==>0
A==>1
C==>2
B==>1
B==>0
C==>1
D==>0
C==>1
D==>0
C==>1
D==>0
C==>1
D==>0

Process finished with exit code 0

虚假唤醒

原因:虚假唤醒

image-20210311124456156
image-20210311124456156

解决方法:将if判断改成while。

/**
 * 资源类
 * 判断等待、业务、通知
 */
class Data{
    private int num = 0;

    public synchronized void increment() throws InterruptedException {
        while (num != 0){
            // 等待
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"==>"+num);
        // 通知其他线程 +1完毕
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        while (num == 0){
            // 等待
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName()+"==>"+num);
        // 通知其他线程 -1完毕
        this.notifyAll();
    }
}

测试结果:

A==>1
B==>0
A==>1
B==>0
A==>1
B==>0
A==>1
B==>0
A==>1
B==>0
C==>1
B==>0
A==>1
B==>0
C==>1
B==>0
A==>1
B==>0
C==>1
B==>0
A==>1
D==>0
C==>1
D==>0
A==>1
D==>0
C==>1
D==>0
A==>1
D==>0
C==>1
D==>0
C==>1
D==>0
C==>1
D==>0
C==>1
D==>0
C==>1
D==>0

Process finished with exit code 0

虚假唤醒原因

class Data{
    private int num = 0;

    public synchronized void increment() throws InterruptedException {
        if (num != 0){
            // 等待
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"==>"+num);
        // 通知其他线程 +1完毕
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        if (num == 0){
            // 等待
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName()+"==>"+num);
        // 通知其他线程 -1完毕
        this.notifyAll();
    }
}
  • 线程被唤醒后,执行开始的地方是wait之后。
  • 就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码。
  • 使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。

拿两个加法线程A、B来说,比如A先执行,执行时调用了wait方法,那它会等待,此时会释放锁,那么线程B获得锁并且也会执行wait方法,两个加线程一起等待被唤醒。此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程,那么这俩加线程不会一起执行,其中A获取了锁并且加1,执行完毕之后B再执行。如果是if的话,那么A修改完num后,B不会再去判断num的值,直接会给num+1。如果是while的话,A执行完之后,B还会去判断num的值,因此就不会执行。