Java并发编程——线程之间的协作

概述

线程开始运行的时候,拥有自己的栈空间,如同一个脚本按照既定的代码一步一步执行。但是如果每个线程孤立的运行只能产生一点点价值,如果多个线程之间能够相互配合完成工作,就会带来巨大的价值。最典型的例子就是生产者-消费者模型:当队列满的时候,生产者需要等待队列有空间才能继续往里添加商品,在等待的过程中,生产者必须释放对于队列的占有权,这样消费者才能有空间去消费队列中的商品。

因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了,这种互相通信的过程就是线程间的协作。

一、等待/通知机制

当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。

等待/通知机制是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()的方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。

上述两个线程通过对象O来完成交互,而对象上的wait和notify/notifyAll的关系如同开关信号一样,用来完成等待方和通知方之间的交互工作。

1.1 wait()、notify() 和 notifyAll()

1、wait()
让当前线程进入waitng状态,只会等到另一个线程的通知或中断才会返回,需要注意的是,调用wait()方法后,会释放对象的锁;

//方法声明
public final native void wait(long timeout) throws InterruptedException;

2、notify()
通知一个在对象上等待的线程,使其从wait()方法返回,返回的前提是该线程获取到了对象的锁。

//方法声明
public final native void notify();

3、notifyAll()
通知所有等待在该对象上的线程使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行;

//方法声明
public final native void notifyAll();

注意事项:

  1. 使用wait()、notify()和notify()时需要先对调用对象加锁;
  2. 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列;
  3. notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifyAll()方法线程释放锁之后,等待线程才有可能从wait()返回;
  4. notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中的所有线程全部移到同步队列中,被移动的线程状态由WAITING变为BLOCKED;
  5. 从wait()方法返回的前提是获得了调用对象的锁;

1.2 方法调用和线程之间的关系

每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。
就绪队列中存储了已就绪的线程,阻塞队列存储了被阻塞的线程,当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度;反之,当一个线程处于wait状态,就会进入阻塞队列,等待被唤醒;

方法调用和线程之间的关系

1.3 await() signal() signalAll()

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。

必须要注意的是,**Condition 的 await()/signal() 使用都必须在lock保护之内,也就是说,必须在lock.lock()和lock.unlock之间才可以使用。**事实上,Conditon的await()/signal() 与 Object的wait()/notify() 有着天然的对应关系:

  • Conditon中的await()对应Object的wait();
  • Condition中的signal()对应Object的notify();
  • Condition中的signalAll()对应Object的notifyAll()

多个线程(Thread)可以竞争同一把锁,一把锁也可以关联多个Condition,以便于多个线程之间进行通信和协同;Condition实现了一种分组机制,将所有临界资源进行访问的线程进行分组,以便实现线程间更精细化的的协作,例如通知部分线程。

二、等待/通知的经典范式

等待/通知机制 最经典的应用就是 生产者-消费者模型。该范式分为两部分,分别针对等待方(消费者)/通知方(生产者);

(等待方)消费者遵循的原则:

  1. 获取对象的锁;
  2. 如果条件不满足,就调用对象的wait()方法,被通知后仍要检查条件;
  3. 条件满足执行对应的逻辑;
    伪代码如下:
synchronized(对象){
	while(条件不满足){
		对象.wait();
	}
	对应的处理逻辑;
}

(通知方)生产者遵循的原则:

  1. 获得对象的锁;
  2. 改变条件;
  3. 通知所有等待在对象上的线程;

伪代码如下:

synchronized(对象){
	改变条件;
	对象.notifyAll();
}

注意事项:

  • 在多个同类型线程(多个生产者线程或者消费者线程)的场景中,为防止wait的条件发生变化而导致线程异常终止,我们在阻塞线程被唤醒的同时还必须对wait的条件进行额外的检查,即 使用 while 循环代替 if 条件
  • 在多个同类型线程(多个生产者线程或者消费者线程)的场景中,为防止生产者(消费者)唤醒生产者(消费者),保证生产者和消费者互相唤醒,需要 使用 notify 替代 notifyAll.

三、线程通信:管道

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,其主要用于线程之间的数据传输,而传输的媒介为内存;

PipedInputStream类PipedOutputStream类 用于在应用程序中创建管道通信;
一个PipedInputStream实例对象必须和一个PipedOutputStream实例对象进行连接而产生一个通信管道;PipedOutputStream可以向管道中写入数据,PipedIntputStream可以读取PipedOutputStream向管道中写入的数据,这两个类主要用来完成线程之间的通信。一个线程的PipedInputStream对象能够从另外一个线程的PipedOutputStream对象中读取数据。

管道
 在 Java 的 JDK 中,提供了四个类用于线程间通信:

  • 字节流:PipedInputStream 和 PipedOutputStream;
  • 字符流:PipedReader 和 PipedWriter;

四、Thread.join()的使用


4.1 join()的定义

当前线程A等待thread线程终止之后才从thread.join()返回;
线程Thread除了提供join()方法之外,还提供了**join(long millis)join(millis,int nanos)**这两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的时间里没有终止,那么将从该超时方法中返回。

//join()方法有三个重载版本
public final synchronized void join(long millis) throws InterruptedException {...}
public final synchronized void join(long millis, int nanos) throws InterruptedException {...}
public final void join() throws InterruptedException {...}

4.2 join()使用实例及原理

//示例代码
public class Test {
     
    public static void main(String[] args) throws IOException  {
        System.out.println("进入线程"+Thread.currentThread().getName());
        Test test = new Test();
        MyThread thread1 = test.new MyThread();
        thread1.start();
        try {
            System.out.println("线程"+Thread.currentThread().getName()+"等待");
            thread1.join();
            System.out.println("线程"+Thread.currentThread().getName()+"继续执行");
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    } 
     
    class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("进入线程"+Thread.currentThread().getName());
            try {
                Thread.currentThread().sleep(5000);
            } catch (InterruptedException e) {
                // TODO: handle exception
            }
            System.out.println("线程"+Thread.currentThread().getName()+"执行完毕");
        }
    }
}/* Output:
        进入线程main
        线程main等待
        进入线程Thread-0
        线程Thread-0执行完毕
        线程main继续执行
 *///~

当 main线程 运行到 thread1.join() 时,main线程会获得线程对象thread1的锁(wait 意味着拿到该对象的锁)。只要 thread1线程 存活, 就会调用该对象锁的wait()方法阻塞 main线程。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页