专栏首页编程坑太多JAVA之线程间如何通信(五)

JAVA之线程间如何通信(五)

上节说了CPU缓存和内存屏障,CPU厂家考虑到指令重排的一些解决方案吧,本次说说线程通信,多个线程运行期间,它们之间进行数据交互和协作。

(一)通信的方式

  • ①介绍

多要实现多线程之间的协同,如:线程执行先后顺序,获取某一个线程的执行的结果等等。这个过程就涉及到线程之间的项目通信。

1.文件共享 2.网络共享 3.变量共享 4.jdk提供的线程协调API (重点)

wait/notify,park/unpark

  • ② 文件共享

线程1 写入文件a.txt 数据,线程2读取a.txt数据。

  • ③ 网络共享

ServerThread ,socket的方式,目前java很少做cs开发了,基本不做概述了。

  • ④ 变量共享

定义了一个静态变量,content,一个线程往变量里面写数据,一个线程往变量里面读数据。

(二)线程协作

  • ① 介绍

JDK 中对于需要多线程协作完成某一任务的场景,提供了对应API支持,多线程协作的经典场景就是 生产者 --消费者模式。用到了线程阻塞,线程唤醒。

  • ② 场景

线程1去吃馒头,没有馒头,就不执行。线程2生产馒头,有了就执行线程1可以过来吃了。线程1是消费者,线程2是生产者,这种模型广泛应用在分布式系统架构开发上,以及底层组件的开发上。需要考虑的是如何做到通知,线程2如何通知线程1继续去跑,

  • ③ API-被弃用的suspend 和 resume

调用suspend 挂起目标线程,通过resume可以恢复线程执行。

    public void suspendResumeTest() throws Exception {
        // 启动线程
        Thread consumerThread =  new Thread(new Runnable() {
            public void run() {
                if (mantoudian == null) { // 如果没馒头,则进入等待
                    System.out.println("1、进入等待");
                    Thread.currentThread().suspend();
                }
                System.out.println("3、买到馒头,回家");
            }
        });
        consumerThread.start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        System.out.println("2、通知消费者");
        consumerThread.resume();

    }

方法看起来很简单,通过名称控制线程的挂起和恢复。 被启用的主要原因:容易写出死锁的代码。

演示死锁,对使用的要求非常的严格,不容易控制。

  1. 同步代码中使用
public void suspendResumeDeadLockTest() throws Exception {
        // 启动线程
        Thread consumerThread =  new Thread(new Runnable() {
            public void run() {
                if (mantoudian == null) { // 如果没馒头,则进入等待
                    System.out.println("1、进入等待");
                    // 当前线程拿到锁,然后挂起
                    synchronized (this) {
                        Thread.currentThread().suspend();
                    }
                }
                System.out.println("3、买到馒头,回家");
            }
        });
        consumerThread.start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        // 争取到锁以后,再恢复consumerThread
        
        synchronized (this) {
            consumerThread.resume();
        }
              System.out.println("2、通知消费者");

    }

这个代码的意思当进入【馒头店】没有馒头的时候,消费者把喇叭给拿走了,拿回家等待喇叭通知他有馒头的时候通知他,结果导致生产者去找喇叭的时候找不到,无法通知消费者。就进入了死锁的情况。同步块就是锁了。

  1. suspend 比resume后执行
/** 导致程序永久挂起的suspend/resume */
    public void suspendResumeDeadLockTest2() throws Exception {
        // 启动线程
        Thread consumerThread =  new Thread(new Runnable() {
            public void run() {
                if (mantoudian == null) {
                    System.out.println("1、没馒头,进入等待");
                    try { // 为这个线程加上一点延时
                        Thread.sleep(5000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 这里的挂起执行在resume后面
                    Thread.currentThread().suspend();
                }
                System.out.println("3、买到馒头,回家");
            }
        });
        consumerThread.start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        consumerThread.resume();
                 System.out.println("2、通知消费者");
        consumerThread.join();
    }

这个代码的意思:12点的时候消费者进店了,他说他去买个菜就回来,12点10分的时候馒头店生产者喊馒头来了,但是因为消费者这10分钟不在现场没听到喇叭喊,12点11分他过来之后就一直等喇叭喊,其实喇叭早就喊过了,他不知道,他就一直在门口等。

  • ③ API-wait / notify 机制

方法只能由统一对象锁的持有者线程调用,也就是写在同步块里面,否则会抛出illegalMonitorStateException异常。

wait方法导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁。notify 、 notifyAll方法唤醒一个或所有正在等待这个对象锁的线程。

注意:虽然会wait自动解锁,但是对顺序是有要求的,如果在notify被调用之后,才开始wait方法的调用,线程会永远处于WAITING状态。

正常的情况

public void waitNotifyTest() throws Exception {
        // 启动线程
         new Thread(new Runnable() {
            public void run() {
                if (mantoudian == null) { // 如果没馒头,则进入等待
                    synchronized (this) {
                        try {
                            System.out.println("1、进入等待");
                            this.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                System.out.println("2、买到馒头,回家");
            }
        }).start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3、通知消费者");
        }
    }

注意:先后顺序,避免死锁代码、

    /** 会导致程序永久等待的wait/notify */
    public void waitNotifyDeadLockTest() throws Exception {
        // 启动线程
         new Thread(new Runnable() {
            public void run() {
                if (mantoudian == null) { // 如果没馒头,则进入等待
                    try {
                        Thread.sleep(5000L);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                    synchronized (this) {
                        try {
                            System.out.println("1、进入等待");
                            this.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                System.out.println("2、买到馒头,回家");
            }
        }).start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3、通知消费者");
        }
    }

通知消费者,进入等待状态,不动了,来晚了。注意时间问题。

  • ⑤ park 和 unpark

park是等待一个许可,unpark是为某线程提供一个许可。 先提供许可。当某线程调用park时,已经有许可了,它就消费这个许可,然后可以继续运行。这其实是必须的。考虑最简单的生产者消费者(模型:消费者需要消费一个资源,于是调用park操作等待;生产者则生产资源,然后调用unpark给予消费者使用的许可。非常有可能的一种情况是,生产者先生产,这时候消费者可能还没有构造好(比如线程还没启动,或者还没切换到该线程)。那么等消费者准备好要消费时,显然这时候资源已经生产好了,可以直接用,那么park操作当然可以直接运行下去。如果没有这个语义,那将非常难以操作。

park 和 unpark没有调用顺序要求。wait和notifyAll有顺序性。本身不是基于监视器锁的概念去实现的。

public void parkUnparkTest() throws Exception {
        // 启动线程
        Thread consumerThread =  new Thread(new Runnable() {
            public void run() {
                if (mantoudian == null) { // 如果没馒头,则进入等待
                    System.out.println("1、进入等待");
                    LockSupport.park();
                }
                System.out.println("2、买到馒头,回家");
            }
        });
        consumerThread.start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        LockSupport.unpark(consumerThread);
        System.out.println("3、通知消费者");
    }
public void parkUnparkDeadLockTest() throws Exception {
        // 启动线程
        Thread consumerThread =  new Thread(new Runnable() {
            public void run() {
                if(mantoudian ==null)

                { // 如果没馒头,则进入等待
                    System.out.println("1、进入等待");
                    // 当前线程拿到锁,然后挂起
                    synchronized (this) {
                        LockSupport.park();
                    }
                }
                System.out.println("2、买到馒头,回家");
            }
        });
        consumerThread.start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        // 争取到锁以后,再恢复consumerThread
        synchronized (this) {
            LockSupport.unpark(consumerThread);
        }
        System.out.println("3、通知消费者");
    }

用了同步块,这把锁没人可以拿得到。资源被死锁了。全部都gg了。

(三)伪唤醒

上面的代码用if语句来判断,是否进入等待状态,是错误的! 官方建议应该在循环中检查等待条件,原因处于等待状态的线程可能收到错误警告和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。

伪唤醒是指线程并非因为notify,notifyall,uppark等api调用而唤醒,是更底层原因导致的。

synchronized (obj) {
        while (<condition does not hold>)
               obj.wait();
    ... // Perform action appropriate to condition
    }

//该方法只能由作为该对象的监视器的所有者的线程调用。有关线程可以成为监视器所有者的方式的说明,请参阅notify方法。

完整代码

import java.util.concurrent.locks.LockSupport;

/** 三种线程协作通信的方式:suspend/resume、wait/notify、park/unpark */
public class Demo6 {
    /** 馒头店 */
    public static Object mantoudian = null;

    /** 正常的suspend/resume */
    public void suspendResumeTest() throws Exception {
        // 启动线程
        Thread consumerThread =  new Thread(new Runnable() {
            public void run() {
                if (mantoudian == null) { // 如果没馒头,则进入等待
                    System.out.println("1、进入等待");
                    Thread.currentThread().suspend();
                }
                System.out.println("3、买到馒头,回家");
            }
        });
        consumerThread.start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        System.out.println("2、通知消费者");
        consumerThread.resume();

    }

    /** 死锁的suspend/resume。suspend并不会像wait一样释放锁,故此容易写出死锁代码 */
    public void suspendResumeDeadLockTest() throws Exception {
        // 启动线程
        Thread consumerThread =  new Thread(new Runnable() {
            public void run() {
                if (mantoudian == null) { // 如果没馒头,则进入等待
                    System.out.println("1、进入等待");
                    // 当前线程拿到锁,然后挂起
                    synchronized (this) {
                        Thread.currentThread().suspend();
                    }
                }
                System.out.println("3、买到馒头,回家");
            }
        });
        consumerThread.start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        // 争取到锁以后,再恢复consumerThread

        synchronized (this) {
            consumerThread.resume();
        }
        System.out.println("2、通知消费者");

    }

    /** 导致程序永久挂起的suspend/resume */
    public void suspendResumeDeadLockTest2() throws Exception {
        // 启动线程
        Thread consumerThread =  new Thread(new Runnable() {
            public void run() {
                if (mantoudian == null) {
                    System.out.println("1、没馒头,进入等待");
                    try { // 为这个线程加上一点延时
                        Thread.sleep(5000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 这里的挂起执行在resume后面
                    Thread.currentThread().suspend();
                }
                System.out.println("3、买到馒头,回家");
            }
        });
        consumerThread.start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();

        consumerThread.resume();
        System.out.println("2、通知消费者");
        consumerThread.join();
    }

    /** 正常的wait/notify */
    public void waitNotifyTest() throws Exception {
        // 启动线程
         new Thread(new Runnable() {
            public void run() {
                if (mantoudian == null) { // 如果没馒头,则进入等待
                    synchronized (this) {
                        try {
                            System.out.println("1、进入等待");
                            this.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                System.out.println("2、买到馒头,回家");
            }
        }).start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3、通知消费者");
        }
    }

    /** 会导致程序永久等待的wait/notify */
    public void waitNotifyDeadLockTest() throws Exception {
        // 启动线程
         new Thread(new Runnable() {
            public void run() {
                if (mantoudian == null) { // 如果没馒头,则进入等待
                    try {
                        Thread.sleep(5000L);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                    synchronized (this) {
                        try {
                            System.out.println("1、进入等待");
                            this.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                System.out.println("2、买到馒头,回家");
            }
        }).start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3、通知消费者");
        }
    }

    /** 正常的park/unpark */
    public void parkUnparkTest() throws Exception {
        // 启动线程
        Thread consumerThread =  new Thread(new Runnable() {
            public void run() {
                if (mantoudian == null) { // 如果没馒头,则进入等待
                    System.out.println("1、进入等待");
                    LockSupport.park();
                }
                System.out.println("2、买到馒头,回家");
            }
        });
        consumerThread.start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        LockSupport.unpark(consumerThread);
        System.out.println("3、通知消费者");
    }

    /** 死锁的park/unpark */
    public void parkUnparkDeadLockTest() throws Exception {
        // 启动线程
        Thread consumerThread =  new Thread(new Runnable() {
            public void run() {
                while(mantoudian ==null)

                { // 如果没馒头,则进入等待
                    System.out.println("1、进入等待");
                    // 当前线程拿到锁,然后挂起
                    synchronized (this) {
                        LockSupport.park();
                    }
                }
                System.out.println("2、买到馒头,回家");
            }
        });
        consumerThread.start();
        // 3秒之后,生产一个馒头
        Thread.sleep(3000L);
        mantoudian = new Object();
        // 争取到锁以后,再恢复consumerThread
        synchronized (this) {
            LockSupport.unpark(consumerThread);
        }
        System.out.println("3、通知消费者");
    }

    public static void main(String[] args) throws Exception {
        // 对调用顺序有要求,也要开发自己注意锁的释放。这个被弃用的API, 容易死锁,也容易导致永久挂起。
         //new Demo6().suspendResumeTest();
//       new Demo6().suspendResumeDeadLockTest();
//       new Demo6().suspendResumeDeadLockTest2();

        // wait/notify要求再同步关键字里面使用,免去了死锁的困扰,但是一定要先调用wait,再调用notify,否则永久等待了
//       new Demo6().waitNotifyTest();
//       new Demo6().waitNotifyDeadLockTest();

        // park/unpark没有顺序要求,但是park并不会释放锁,所有在同步代码中使用要注意
//       new Demo6().parkUnparkTest();
//        new Demo6().parkUnparkDeadLockTest();

    }
}

PS:suspend 和 resume对调用顺序有要求,注意锁的释放。这个被弃用的API, 容易死锁,也容易导致永久挂起。wait/notify要求再同步关键字里面使用,免去了死锁的困扰,但是一定要先调用wait,再调用notify,否则永久等待了。park/unpark没有顺序要求,但是park并不会释放锁,所有在同步代码中使用要注意。上边的代码使用了if判断,应该将if更改为while这样才不会出现伪唤醒产生的逻辑问题。之后一起开发JDK多线程工具类的底层实现原理。这次很重要。

本文分享自微信公众号 - 编程坑太多(idig88),作者:诸葛阿明

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 「小程序JAVA实战」 小程序手写属于自己的第一个demo(六)

    IT故事会
  • JAVA之线程状态(二)

    PS:通过API,演示前面说的几种状态,通过图和代码让大家对这6种状态了解透彻,线程状态的知识点就讲解到这里。下次咱们继续说说线程,一起说说线程终止。

    IT故事会
  • 『互联网架构』软件架构-zookeeper场景和实现(34)

    PS:本次主要针对场景进行了,本身zk都是分布式框架,它很少存在宕机的情况,除非外在因素,例如内存硬盘爆了。

    IT故事会
  • No enclosing instance of type Test is accessible. Must qualify the allocation with an enclosing inst

    当时没有注意,仔细想了想,也是合理的。main是Test类的static方法,按照常理它只能访问Test类中static资源,而class A是非static所...

    Frank909
  • Duplicate entry '0' for key 'PRIMARY'的一种可能的解决办法

    在MySQL设计好数据库往往数据库中插入数据的时候, 因为主键ID默认是不赋值的,只给其他项目赋值了,相关的SQL代码是这样的 StringBuilder ...

    码农阿宇
  • 成为大数据工程师必备的技能有哪些?(上)

    http://www.aboutyun.com/thread-11873-1-1.html

    加米谷大数据
  • php学习之类与对象的类的静态方法

    当程序员需要对静态属性进行操作时,就需要定义静态方法处理,静态方法是专门操作静态属性的

    吾爱乐享
  • 海尔Smart净水机如何玩转互联网

    当智能家电浪潮席卷全球时,家用净水机也在悄然掀起物联网革命。 海尔智能净水机HRO500-5于8月底在日日顺净水商城RRS.COM上首发亮相后,凭借领先的智能化...

    罗超频道
  • 修改存储过程

    4.复制以下示例并将其粘贴到查询编辑器中,先创建存储过程。该过程返回 Adventure Works Cycles 数据库中所有供应商的名称、所提供的产品、信用...

    Vincent-yuan
  • JAVA别跑"等等"筒子们

    以下来自,JetBrains 近日发布 的2017 JAVA开发者生态报告。 JAVA 79% 的受访者将票投给了 Java 8 ,还在使用7甚至是65的小伙伴...

    小柒2012

扫码关注云+社区

领取腾讯云代金券