java 线程之间的通信-生产者消费者问题

线程间的通信

线程间通信又称为进程内通信,多个线程实现互斥访问共享资源时会互相发送信号或等待信号,比如线程等待数据到来的通知,线程收到变量改变的信号等。

1 单线程间通信

1.1 初识 wait 和 notify

假设我们现在需要两个线程,一个负责生产数据,一个负责消费数据,理想状态下我们希望一边生产一边消费。

public class ProduceConsumerVersion {
    private final Object LOCK = new Object();
    private int i;
    // 生产者
    public void produce() {
        synchronized (LOCK) {
            System.out.println("P -> " + (i++));
        }
    }
    // 消费者
    public void consumer() {
        synchronized (LOCK) {
            System.out.println("C -> " + i);
        }
    }
    public static void main(String[] args) {
        ProduceConsumerVersion pc = new ProduceConsumerVersion();
        new Thread(() -> {
            while (true)
                pc.produce();
        }).start();
        new Thread(() -> {
            while (true)
                pc.consumer();
        }).start();
    }
}

上面的代码运行以后,结果和我们想的并不是一样的。因为我们希望的是生产一个就消费一个。所以我们使用我们使用 wait 和 notify 重构代码。代码如下:

public class ProduceConsumerVersion2 {
    private final Object LOCK = new Object();
    private int i;
    private boolean isProduced;
    // 生产者
    public void produce() {
        synchronized (LOCK) {
            // 如果已生产者数据等待消费者消费,否则生产数据
            if (isProduced) {
                try {
                    LOCK.wait();// 等待消费者消费
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                i++;// 生产数据
                System.out.println("P -> " + i);
                isProduced = true;
                LOCK.notify();// 唤醒消费者进行消费
            }
        }
    }
    // 消费者
    public void consumer() {
        synchronized (LOCK) {
            // 如果已生产者数据就消费数据,否则等待生产者生产数据
            if (isProduced) {
                System.out.println("C -> " + i);
                isProduced = false;
                LOCK.notify();// 唤醒生产者生产数据
            } else {
                try {
                    LOCK.wait();// 等待生产者生产数据
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) {
        ProduceConsumerVersion2 pc = new ProduceConsumerVersion2();
        new Thread(() -> {
            while (true)
                pc.produce();
        }).start();
        new Thread(() -> {
            while (true)
                pc.consumer();
        }).start();
    }
}

生产者生产数据以后会通知消费者消费,并进入阻塞等待状态;消费者消费以后会唤醒 生产者生产数据,并进入阻塞等待状态,如此循环往复。该结果正是我们需要的。但如果是多个消费者和生产者的,上面的程序就会有问题。所以接下来讨论多线程通信问题。

2 多线程通信

消费者和生产者不变,我们改变线程个数进行调用。

public static void main(String[] args) {
        ProduceConsumerVersion3 pc = new ProduceConsumerVersion3();
        Stream.of("P1", "P2").forEach(p -> {
            new Thread(() -> {
                while (true)
                    pc.produce();
            }).start();
        });
        Stream.of("C1", "C2").forEach(c -> {
            new Thread(() -> {
                while (true)
                    pc.consumer();
            }).start();
        });
    }

该代码运行,会出现一直等待的问题。
2.1 问题分析:
在这里插入图片描述
当两个生产者和消费者都处于等待状态之后,notify就不知道去唤醒谁,从而导致他们一直在等待唤醒,从来导致程序出现假死状态。

2.2 认识notifyAll
通过 notifyAll 可以解决以上问题,我们只需要唤醒所有其他线程即可,然后等待 CPU 调度获得锁资源继续执行。

public class ProduceConsumerVersion4 {
    private final Object LOCK = new Object();
    private int i;
    private boolean isProduced;
    // 生产者
    public void produce() {
        synchronized (LOCK) {
            // 如果已生产者数据等待消费者消费,否则生产数据
            if (isProduced) {
                try {
                    LOCK.wait();// 等待消费者消费
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                i++;// 生产数据
                System.out.println(Thread.currentThread().getName() + " -> " + i);
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                isProduced = true;
                LOCK.notifyAll();// 唤醒消费者进行消费
            }
        }
    }
    // 消费者
    public void consumer() {
        synchronized (LOCK) {
            // 如果已生产者数据就消费数据,否则等待生产者生产数据
            if (isProduced) {
                System.out.println(Thread.currentThread().getName() + " -> " + i);
                isProduced = false;
                LOCK.notifyAll();// 唤醒生产者生产数据
            } else {
                try {
                    LOCK.wait();// 等待生产者生产数据
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) {
        ProduceConsumerVersion4 pc = new ProduceConsumerVersion4();
        Stream.of("P1", "P2", "P3").forEach(p -> {
            new Thread(p) {
                @Override
                public void run() {
                    while (true)
                        pc.produce();
                }
            }.start();
        });
       Stream.of("C1", "C2", "C3", "C4", "C5").forEach(c -> {
            new Thread(c) {
                @Override
                public void run() {
                    while (true)
                        pc.consumer();
                }
            }.start();
        });
    }
}

运行以上代码以后,从结果可以看到之前的问题已经得到解决。

3 wait 和 sleep 的区别

  • sleep 是属于 Thread 的方法,wait 属于 Object;
  • sleep 方法不需要被唤醒,wait 需要;
  • sleep 方法不需要 synchronized,wait 需要;
  • sleep 不会释放锁,wait 会释放锁并将线程加入 wait 队列;

4 脏读

对于对象的同步和异步的方法,一定要考虑问题的整体,不然就会出现数据不一致的错误,很经典的错误就是脏读( dirtyread)示例代码如下:


/**
 * 业务整体需要使用完整的 synchronized,保持业务的原子性。
 */
public class DirtyRead {

    private String username = "bjsxt";
    private String password = "123";

    public synchronized void setValue(String username, String password) {
        this.username = username;

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.password = password;

        System.out.println("setValue最终结果:username = " + username + " , password = " + password);
    }

    public synchronized void getValue() {
        System.out.println("getValue方法得到:username = " + this.username + " , password = " + this.password);
    }

    public static void main(String[] args) {
        final DirtyRead dr = new DirtyRead();
        Thread t1 = new Thread(() -> dr.setValue("z3", "456"));
        t1.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        dr.getValue();
    }
}

示例总结:
在我们对一个对象的方法加锁的时候,需要考虑业务的整体性,即为setValue/getValue 方法同时加锁 synchronized 同步关键字,保证业务(service)的原子性,不然会出现业务错误(也从侧面保证业务的一致性)。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值