1.1.5 线程通信

135 阅读4分钟

通信的方式

  • 要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程执行的结果等等。

    涉及到线程之间相互通信,分为下面四类:

    • 文件共享(通过文件的形式实现数据共享)
    • 网络共享
    • 共享变量
    • jdk提供的线程协调API
      • 细分为:suspend/resume、wait/notify、park/unpark

线程协作 - JDK API

  • JDK中对于需要多线程协作完成某一任务的场景,提供了对应API支持。

  • 多线程协作的典型场景是:生产者 - 消费者模式。(线程阻塞、线程唤醒)

  • 示例:线程1去买包子,没有包子,则不再执行。线程-2生产出包子。通知线程-1继续执行。

  • API - 被弃用的suspend和resume

    • 作用:调用suspend挂起目标线程,通过resume恢复线程执行。

    • 被弃用的原因,就是因为容易出现死锁。

    • 正常的suspend/resume:

      /** 包子店 */
      	public static Object baozidian = null;
      
      	/** 正常的suspend/resume */
      	public void suspendResumeTest() throws Exception {
      		// 启动线程
      		Thread consumerThread = new Thread(() -> {
      			if (baozidian == null) { // 如果没包子,则进入等待
      				System.out.println("1、进入等待");
      				Thread.currentThread().suspend();
      			}
      			System.out.println("2、买到包子,回家");
      		});
      		consumerThread.start();
      		// 3秒之后,生产一个包子
      		Thread.sleep(3000L);
      		baozidian = new Object();
      		consumerThread.resume();
      		System.out.println("3、通知消费者");
      	}
      
    • suspend/resume死锁示例

      • 在同步代码中使用

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

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

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

    • wait方法导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁。

      notify/notifyAll方法唤醒一个或所有正在等待这个对象锁的线程。

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

  • park/unpark 机制

    • 线程调用park则等待“许可”,unpark方法为指定线程提供“许可(permit)”。

    • 不要求 park 和 unpark 方法的调用顺序。

    • 多次调用 unpark 之后,再调用park,线程会直接运行。

    • 不会叠加,也就是说,连续多次调用park方法,第一次会拿到“许可”直接运行,后续调用会进入等待。

    • park/unpark 的使用我们需要借用 java.until.concurrent.locks.LockSupport 工具类提供的支持。

    • 正常的park/unpark:

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

      	/** 死锁的park/unpark */
      	public void parkUnparkDeadLockTest() throws Exception {
      		// 启动线程
      		Thread consumerThread = new Thread(() -> {
      			if (baozidian == null) { // 如果没包子,则进入等待
      				System.out.println("1、进入等待");
      				// 当前线程拿到锁,然后挂起
      				synchronized (this) {
      					LockSupport.park();
      				}
      			}
      			System.out.println("2、买到包子,回家");
      		});
      		consumerThread.start();
      		// 3秒之后,生产一个包子
      		Thread.sleep(3000L);
      		baozidian = new Object();
      		// 争取到锁以后,再恢复consumerThread
      		synchronized (this) {
      			LockSupport.unpark(consumerThread);
      		}
      		System.out.println("3、通知消费者");
      	}
      
  • 总结:

    • 弃用的suspend/resume:对调用顺序有要求,也要开发者在同步代码中自己注意锁的释放;
    • wait/notify:要求在同步关键字里面使用,免去了死锁的困扰(会自己释放锁),但是对调用顺序由要求;
    • park/unpark:没有要求调用的顺序,但是park并不会释放锁,所以在同步代码中使用要注意。
    • 这一节的内容,设计很多JDK多线程开发工具类,它底层实现的原理。掌握这些内容,后续课程中再进行回顾。
  • 伪唤醒