进程与线程

71 阅读15分钟

进程与线程

进程的产生

最初的计算机只能接受一些特定的指令,计算机等待用户输入,用户每输入一个指令,计算机就做出一个操作。大多时候,计算机都处在等待状态。因此效率低下。

批处理操作系统

后来便有了批处理操作系统:用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。

程序是用某种编程语言编写的,能完成一定任务的代码集合,它是一段静态代码。

但是由于批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行,后面的程序需要等待前面的程序执行完成后才能开始执行,而前面的程序有时会由于I/O操作、网络等原因阻塞,所以批处理操作效率也不高

进程的提出

在批处理系统中,内存只能存放一个程序,为了使内存存放多个程序,便有了进程的概念。

进程就是应用程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不干扰。同时进程保存着程序每一个时刻运行的状态。

CPU为每个进程分配一个时间片,若时间片结束后进程还在运行,则暂停该进程的运行,并将CPU分配给另一个进程。这个过程称为上下文切换。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。

从宏观上看,同一时段是在执行多个任务,可以说进程让操作系统的并发成为了可能,但事实上,对于单核CPU而言,某一时刻只有一个任务在占用CPU。

线程的产生

随着时间的推移,人们并不满足一个进程在一段时间只能做一件事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。

比如说杀毒软件的检测功能,若某一项检测出现问题,这会影响到后面的检测。

为了让这些子任务同时执行,便有了线程的概念:一个线程执行一个子任务,一个进程中包含多个线程。

总结的说,进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。

为什么使用多线程?

虽然通过多进程的方式也可以实现并发,但多线程有如下的优点:

  • 多进程的通信较为复杂,而线程的通信比较简单。通常情况下我们需要使用共享资源,而进程与进程之间是存在内存分隔的,数据是分开的,这使得数据共享变得复杂;而线程之间共享所属进程占用的内存地址空间和资源,数据共享简单。
  • 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。

不过线程也有如下的缺点:

  • 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
  • 虽然进程与进程间共享数据较为复杂,但同步简单;而线程是共享数据简单,同步复杂。

进程与线程的本质区别在于是否单独占有内存地址空间及其它系统资源(比如I/O),以及进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位(CPU分配时间的单位)

上下文切换

上下文切换是指 CPU 从一个进程(或线程)切换到另一个进程(或线程),而上下文是指某一时间点 CPU 寄存器和程序计数器的内容。

寄存器:CPU内部的少量速度很快的闪存,通常存储和访问计算过程的中间值提高计算机程序的运行速度。

程序计数器:一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置。

由于线程存在创建和上下文切换的开销,因此并发执行不一定比串行快。

并发:同一时间段内,多个任务都在执行。

并行:单位时间内,多个任务同时执行。

如何减少上下文切换

  • 无锁并发编程:多线程竞争锁时,会引起上下文切换。因此我们可以用一些方法避免使用锁,比如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
  • CAS算法:Java的Atomic包使用CAS算法来更新数据,不需要加锁。
  • 使用最少线程:避免创建不需要的线程。比如任务很少,却创建了很多线程来处理,这样会造成大量线程出于等待状态。
  • 协程:在单线程里实现多任务的调度,并维持多个任务间的切换。

线程的相关类和接口

新建线程有两种方法:继承Thread类,并重写run方法;实现Runnable接口的run方法;

实现Runnable接口

Runnable接口如下所示

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

示例程序

public class Test {
    public static class MyTask implements Runnable {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {
        new Thread(MyTask).run();
    }
}

继承Thread类

示例程序

public class Demo {
    public static class MyThread extends Thread {
        @Override
        public void run() { System.out.println("MyThread"); }
    }

    public static void main(String[] args) {
        Thread myThread = new MyThread();
        myThread.start();
    }
}

Thread类是一个Runnable接口的实现类,它的构造方法使用了私有的init方法来实现初始化。

//构造方法1
public Thread() {
     init(null, null, "Thread-" + nextThreadNum(), 0);
}

// 构造方法2
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name,long stackSize) {
    init(g, target, name, stackSize, null);
}

点击并拖拽以移动

介绍一下init方法里的参数:

  • g:线程组,指定这个线程是在哪个线程组下;
  • target:指定要执行的任务;
  • name:线程的名字,多个线程的名字是可以重复的;
  • inheritThreadLocals:可继承的ThreadLocal;

示例程序

当程序中调用start()方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。

public class Test{
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {
        Thread myThread = new MyThread();
        myThread.start();
    }
}

如果你只是直接调用run方法,则run方法只是在当前线程里执行。

Runnable接口与Thread类的比较

实现Runnable接口的优点:

  • 创建线程的同时可以继承其他类。
  • 多个线程可以共享一个任务:Runnable的实现类,非常适合多个相同线程来处理同一份资源的情况

劣势:

  • 编写略微复杂,若要访问当前线程,则需要使用Thread.currentThread的方法

继承Thread类的优点:

  • 编写简单,如果需要访问当前线程,直接使用this即可。

劣势:

  • 无法继承其他类。

Callable、Future与FutureTask

使用RunnableThread来创建线程有一个弊端:方法没有返回值的。而有时我们希望线程执行的任务执行完成后有一个返回值。那么我们可以使用Callable接口与Future类。

Callable接口

Callable接口只有一个方法,该方法有返回值且支持泛型。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

我们一般通过配合线程池工具ExecutorService来使用Callable接口:通过ExecutorService的submit方法来让一个Callable接口执行,我们可以通过返回的Future的get方法得到结果。

class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        Thread.sleep(1000);
        return 2;
    }
    public static void main(String args[]){
        // 使用
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        Future<Integer> result = executor.submit(task);
        // get方法会阻塞当前线程,直到得到结果。
        // 因此实际编程中建议使用可以设置超时时间的重载get方法。
        System.out.println(result.get()); 
    }
}

Future接口

Future接口的方法如下所示:

public abstract interface Future<V> {
    public abstract boolean cancel(boolean paramBoolean);
    public abstract boolean isCancelled();
    public abstract boolean isDone();
    public abstract V get() throws InterruptedException, ExecutionException;
    public abstract V get(long paramLong, TimeUnit paramTimeUnit) throws InterruptedException, ExecutionException, TimeoutException;
}

点击并拖拽以移动

其中cancel方法是试图取消一个线程的执行,但不一定能取消成功。因为任务可能已完成、已取消。该方法的boolean返回值表示“是否取消成功”,参数paramBoolean表示是否采用中断的方式取消线程执行。

如果我们需要让任务有能够取消的功能,可以使用Callable来代替Runnable,但如果只是为了可取消,而不需要返回值,则可以声明形式类型、并返回 null作为底层任务的结果。

FutureTask类

Future接口的cancel,get方法实现起来复杂,因此JDK提供了FutureTask类来供我们使用。FutureTask类实现了RunnableFuture接口的,而RunnableFuture接口同时继承了Runnable接口和Future接口。

class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        // 模拟计算
        Thread.sleep(1000);
        return 2;
    }
    public static void main(String args[]){
       
        ExecutorService executor = Executors.newCachedThreadPool();
        FutureTask<Integer> futureTask = new FutureTask<>(new Task());
        executor.submit(futureTask);
        System.out.println(futureTask.get());
    }
}

点击并拖拽以移动

通过与Callable接口的那段示例代码比较,可以发现此处的submit方法没有返回值,它实际调用了submit(Runnable task)方法,而之前调用的是submit(Callable<T> task)方法。

线程状态及主要转化方法

线程状态

线程的状态分为6种:

(1)新建状态NEW:该状态的线程尚未启动,即还未执行start()方法。

start()方法代码如下,方法内部有一个变量threadStatus。第一次调用start()方法后,threadStatus的值会改变(不等于零),如果再次调用则会抛出IllegalThreadStateException异常。

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {

        }
    }
}

(2)运行状态RUNNABLE:处于该状态的线程可能在JVM运行,也可能在等待其他系统资源(比如I/O)。该状态实际包括了传统操作系统的ready和running状态。

(3)阻塞状态BLOCKED:该状态的线程正等待锁的释放。

4)等待状态WAITING:造成该状态的方法有:

​ 4-1)Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;

​ 4-2)Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法;

​ 4-3)LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。

(5)超时等待状态TIMED_WAITING:线程等待一个具体的时间,时间到后会被自动唤醒。造成该状态的方法有:

1)Object.sleep(long millis):使当前线程睡眠指定时间;

2)Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;

3)Thread.join(long millis):等待当前线程最多执行millis毫秒

4)LockSupport.parkNanos(long millis):除非获得调用许可,否则禁用当前线程进行线程调度直到指定时间。

(6)终止状态TERMINATED:该状态的线程已执行完毕

wait方法,yield方法,sleep方法和join方法

wait方法

将当前线程置入休眠状态。该方法使用前必须持有对象的锁,调用方法时会先释放当前的锁,直到有其他线程调用notify(),notifyAll()方法唤醒等待锁的线程。该方法可以加上指定等待时间的参数,经过指定时间long之后它会自动唤醒,无论其他线程是否唤醒他。

notify() / notifyAll()方法

由于notify()方法只会唤醒单个等待锁的线程,因此如果有多个线程都在等待这个锁的话,则不一定会唤醒到之前调用wait()方法的线程。同样的。调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。

sleep方法

使用该方法会使线程停止执行,并不会释放锁。时间到后,线程会重新进入RUNNABLE状态。

join方法

若在某一线程A中调用了线程B的join()方法,则A需要等待B执行完后,A线程才能继续。该方法不会释放锁。

yield方法

当前线程放弃CPU资源,将CPU让给其他任务。不过可能出现 该线程刚放弃CPU,立马又获得CPU时间片的情况。

线程中断

某些情况下,我们在线程启动后发现不需要它继续执行,则可以中断线程。此处中断的意思只是把线程的中断标志置为true(默认是flase),而非真正的停止线程。

Thread.interrupt()

将调用该方法的线程对象的断标志置为true,不会停止线程。需要我们自己去监视线程的状态位并做出相应处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态。一旦线程的中断状态被置为“中断状态”,就会抛出中断异常interruptedException

Thread.interrupted()

判断 **当前线程(而非调用该方法的线程对象)**是否被中断。该方法在返回结果前,会清除线程的中断状态(即置为false),因此如果连续两次调用该方法,第二次的结果是false。

测试代码如下,我们将当前线程(即main线程)中断,通过两次调用worker.interrupted()来判断当前线程是否被中断:第一次为true,第二次为false。

public class Test {
    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("worker线程启动");
                System.out.println("worker线程终止");
            }
        });
        worker.start();

        Thread.sleep(300);
        Thread.currentThread().interrupt();     //将当前线程的中断标志置为true
        System.out.println(Thread.currentThread().getName() +"线程的中断标志:" + worker.interrupted()); //查看当前线程的中断标志
        System.out.println(Thread.currentThread().getName() +"线程的中断标志:" + worker.interrupted());
    }
}

点击并拖拽以移动

输出:

点击并拖拽以移动

Thread.isInterrupted()

判断 调用该方法的线程对象是否被中断。该方法不会清除中断标志。

    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 1000; i++) {
                    System.out.println("i= " + i);
                }
            }
        });
        worker.start();

        Thread.sleep(10);
        worker.interrupt();

        System.out.println("worker interrupted:" + worker.isInterrupted());
        System.out.println("worker interrupted:" + worker.isInterrupted());

    }

点击并拖拽以移动

输出:

img点击并拖拽以移动

当睡眠时遇到中断

当在sleep中的线程被中断时,会抛出InterruptedException 异常。如果我们在线程的run方法中捕获这个异常,会发现此时的中断标志位为false

测试代码如下,当我们抛出异常时,中断状态已被清除,因此输出的是false。

public class Test {
    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("worker线程启动");

                try {
                    Thread.sleep(10000);
                }catch (InterruptedException e) {
                    //输出false
                    System.out.println("run方法 interrupted:" + Thread.currentThread().isInterrupted());
                }
                System.out.println("worker线程终止");
            }
        });
        worker.start();

        Thread.sleep(10);
        worker.interrupt();

        System.out.println("main线程停止");
    }
}

点击并拖拽以移动

如果我们希望isInterrupted方法返回true,则可以在isInterrupted方法前再次中断该线程,这样两次的输出都是true

public class Test {
    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("worker线程启动");

                try {
                    Thread.sleep(10000);
                }catch (InterruptedException e) {
//                    再次中断线程
                    Thread.currentThread().interrupt();
                    //输出true
                    System.out.println("run方法 interrupted:" + Thread.currentThread().isInterrupted());
                }
                System.out.println("worker线程终止");
            }
        });
        worker.start();

        Thread.sleep(10);
        worker.interrupt();
        System.out.println("run方法 interrupted:" + worker.isInterrupted());
        System.out.println("main线程停止");
    }
}

点击并拖拽以移动

线程组

线程组的作用是批量管理线程或线程组,每个线程必然存在于一个线程组中。若在newThread时没有显式指定所属线程组,则默认将当前执行new Thread的线程所在的线程组设置为自己的线程组,比说如果在main函数里new Thread,则该线程属于main线程组。

线程组是一个树状结构,线程组里可有线程对象和线程组。

img点击并拖拽以移动

线程组的常用方法

  • static int enumerate(Thread[] tarray): 将线程组中的子线程以复制的形式拷贝到groupList中。

  • int activeGroupCount(): 返回此线程组及其子组中活动组数的估计值

通过上面的方法,我们将新建的一个线程ta放入新建的线程组group,然后取出线程组里的线程,

public class Test {
    public static void main(String[] args) throws Exception {

 		ThreadGroup groupA = new ThreadGroup("A");
        //0
        System.out.println("goupA的活跃子线程数:" + groupA.activeCount());
        //main
        System.out.println("groupA的父线程组名:" + groupA.getParent().getName());
		//把ta线程放进线程组groupA
        Thread ta = new Thread(groupA, new Runnable() {
            @Override
            public void run() {		//A
                System.out.println("当前线程所属线程组名:"+ Thread.currentThread().getThreadGroup().getName());
            }
        }, "ta");
        ta.start();

        Thread[] threadArray = new Thread[groupA.activeCount()];
        groupA.enumerate(threadArray);
        for(Thread thread : threadArray) {
            System.out.println("groupA的子线程:" + thread.getName());		//ta
        }
    }
}

点击并拖拽以移动

同样我们可以把新建的一个线程组放入当前线程组:

public class Test {
    public static void main(String[] args) throws Exception {
        
        System.out.println("当前线程:" + Thread.currentThread().getName() + "所属的线程组名为:" +
                Thread.currentThread().getThreadGroup().getName() + ",该组有线程组数量:" +
                Thread.currentThread().getThreadGroup().activeGroupCount());

        ThreadGroup group = new ThreadGroup("新的组"); //默认加到main组
        System.out.println("当前线程:" + Thread.currentThread().getName() + "所属的线程组名为:" +
                Thread.currentThread().getThreadGroup().getName() + ",该组有线程组数量:" +
                Thread.currentThread().getThreadGroup().activeGroupCount());
            //把main线程组里包含的所有线程组 复制到线程组数组groupList中。
        ThreadGroup[] groupList = new ThreadGroup[Thread.currentThread().getThreadGroup().activeGroupCount()];
        Thread.currentThread().getThreadGroup().enumerate(groupList);
        for(int i = 0; i < groupList.length; i++) {
            System.out.println("线程组名称:" + groupList[i].getName());
        }
    }
}

点击并拖拽以移动

输出:

img点击并拖拽以移动

线程通信

各个线程都有自己私有的线程上下文,互不干涉,但各个线程也需要通信。除了前面介绍过的join(),sleep()方法,线程还可以通过几种方法/原理来进行通信。

在多线程环境中,如果多个线程同时操作一个共享的变量,则可能会导致数据不准确,产生冲突。因此我们需要同步,所谓的同步就是按先后次序来运行,即多个线程之间按一定顺序执行。为了达成同步,我们可以通过锁来实现。

Java对象可以被当成锁来使用,同一时间一个锁只能被一个线程持有,而其他线程需要等待锁的释放。

等待/通知

等待/通知机制是基于Object类的wait()方法和notify(), notifyAll()方法来实现的。

执行wait()方法前需要线程A先拿到锁lock,然后线程A可以通过lock.wait()方法进入等待状态,此时锁被释放。另一个线程B获得锁开始执行后,它可以选择在某一时刻使用lock.notify(),此时线程B还未释放锁,除非他使用lock.wait()方法释放锁,或者他执行任务,就会自动释放锁。

我们通过wait/notify来实现生产者-消费者模式:

(1)消费者

public class Customer implements Runnable {
    private LinkedList<Integer> objList = new LinkedList<>();

    public Customer(LinkedList<Integer> objList) {
        this.objList = objList;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (objList) {
                try {
                    while(objList.isEmpty()) {
                        System.out.println("消费者" + Thread.currentThread().getName() + "无法取出数据");
                        objList.wait();
                    }
                    int getNum = objList.removeFirst();
                    System.out.println("消费者" + Thread.currentThread().getName() + "成功取出数据" + getNum) ;

                    objList.notifyAll();
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }

        }
    }
}

(2)生产者:

public class Producer implements Runnable {
    private LinkedList<Integer> objList = new LinkedList<>();

    public Producer(LinkedList<Integer> objList) {
        this.objList = objList;
    }

    @Override
    public void run() {
        while(true) {
            synchronized (objList) {
                try {
                    while(objList.size() > 2) {		//缓存最大存放2个数据
                        System.out.println("生产者" + Thread.currentThread().getName() + "不能生产数据");
                        objList.wait();
                    }
                    Random random = new Random();
                    int putNum = random.nextInt(10);
                    objList.add(putNum);
                    System.out.println("生产者" + Thread.currentThread().getName() + "成功生产数据" + putNum) ;

                    objList.notifyAll();
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

(3)测试:

	 @Test
    public void runConsumerAndProducer() throws InterruptedException{
        LinkedList<Integer> objList = new LinkedList<>();
        ExecutorService service = Executors.newFixedThreadPool(15);
        for(int i = 0; i < 5; i++) {
            service.submit(new Producer(objList));
        }
        for(int i = 0; i < 10; i++) {
            service.submit(new Customer(objList));
        }
    }
/*
一个可能的输出:

    生产者pool-1-thread-1成功生产数据6
    生产者pool-1-thread-1成功生产数据5
    生产者pool-1-thread-1成功生产数据5
    生产者pool-1-thread-1不能生产数据
    生产者pool-1-thread-2不能生产数据
    生产者pool-1-thread-3不能生产数据
    生产者pool-1-thread-4不能生产数据
    生产者pool-1-thread-5不能生产数据
    消费者pool-1-thread-6成功取出数据6
    消费者pool-1-thread-6成功取出数据5
    消费者pool-1-thread-6成功取出数据5
    消费者pool-1-thread-6无法取出数据
    生产者pool-1-thread-5成功生产数据3
    生产者pool-1-thread-5成功生产数据7
    生产者pool-1-thread-5成功生产数据2
    生产者pool-1-thread-5不能生产数据
    生产者pool-1-thread-4不能生产数据
    生产者pool-1-thread-3不能生产数据

*/

信号量

对一个关键代码段,若我们希望在某一时刻只有一个线程能进入该代码段,则可以使用锁或者等待/通知方式。而如果在某一时刻,关键代码段可以有多个线程进入,我们可以使用信号量。

在进入关键代码段前,线程必须获取一个许可,执行完代码段后,释放许可。许可的总数n表示某一时刻最多同时有n个线程在代码段。

管道流

管道流与I/O流相关,当一个线程希望像另一个线程发送信息(例如字符串),则可以使用管道通信:一个线程发送数据到输出管道,另一个线程从输入管道中读取数据

JDK提供了PipedWriterPipedReaderPipedOutputStreamPipedInputStream。其中,前面两个是基于字符的,后面两个是基于字节流的。

现在通过PipedOutputStreamPipedInputStream来写入和读出字符串0~299。

public class Test {
    static class WriteTask {
        public void write(PipedOutputStream out) {
            try {
                System.out.println("开始写入:");
                for(int i = 0; i < 300; i++) {
                    String data = "" + (i + 1);
                    out.write(data.getBytes());
                    System.out.println(data);
                }
                System.out.println();
                out.close();
            }catch (IOException e)      { e.printStackTrace(); }
        }
    }

    static class ReadTask {
        public void read(PipedInputStream input) {
            try {
                System.out.println("开始读入:");
                byte[] bArray = new byte[20];
                //若没有数据可读,则读线程会阻塞在此处
                int readLength = input.read(bArray);

                while(readLength != -1) {
                    String curReadData = new String(bArray, 0, readLength);
                    System.out.println(curReadData);
                    readLength = input.read(bArray);
                }
                System.out.println();
                input.close();
            }catch (IOException e)      { e.printStackTrace(); }
        }
    }

    static class WriteThread extends Thread {
        private WriteTask writeTask;
        private PipedOutputStream out;

        public WriteThread(WriteTask writeTask, PipedOutputStream out) {
            this.writeTask = writeTask;
            this.out = out;
        }

        @Override
        public void run() {
            writeTask.write(out);
        }
    }

    static class ReadThread extends Thread {
        private ReadTask readTask;
        private PipedInputStream input;

        public ReadThread(ReadTask readTask, PipedInputStream input) {
            this.readTask = readTask;
            this.input = input;
        }

        @Override
        public void run() {
            readTask.read(input);
        }
    }

    public static void main(String[] args) throws Exception{
        WriteTask writeTask = new WriteTask();
        ReadTask readTask = new ReadTask();


        PipedOutputStream out = new PipedOutputStream();    //PipedReader
        PipedInputStream input = new PipedInputStream();    //PipedWriter

        out.connect(input);     //或者input.connect(out)

        new ReadThread(readTask, input).start();
        Thread.sleep(2000);
        new WriteThread(writeTask, out).start();
    }
}

点击并拖拽以移动

参考资料

深入浅出Java多线程