Java 基础——线程间通信

概述

线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作,最典型的例子就是生产者-消费者问题。虽然通常每个子线程只需要完成自己的任务,但是有时我们希望多个线程一起工作来完成一个任务,这就涉及到线程间通信。线程通信主要可以分为三种方式,分别为共享内存、消息传递和管道流:

(1)共享内存

1)volatile 关键字
2)synchronized 关键字
3)Lock 锁

(2)消息传递

1)Object 的 join()/notify() 机制;
2)Lock 和 Condition 的 await()/signal() 机制;
3)join() 方法
4)CountdownLatch;
5)CyclicBarrier;

(3)管道通信

1、线程同步

线程同步可以通过 synchronized 关键字和 Lock 锁来实现线程间的通信。
这种方式,本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。

// 共享对象
class MyObject {
 synchronized public void methodA() {
 //do something....
 }
 synchronized public void methodB() {
 //do some other thing
 }
}

// 线程 A
class ThreadA extends Thread {
 private MyObject object;
//省略构造方法
 @Override
 public void run() {
 super.run();
 object.methodA();
 }
}

// 线程 B
class ThreadB extends Thread {
 private MyObject object;
//省略构造方法
 @Override
 public void run() {
 super.run();
 object.methodB();
 }
}

// 主类
public class Main {
 public static void main(String[] args) {
 MyObject object = new MyObject();
 //线程A与线程B 持有的是同一个对象:object
 ThreadA a = new ThreadA(object);
 ThreadB b = new ThreadB(object);
 a.start();
 b.start();
 }
}

由于线程 A 和线程 B 持有同一个 MyObject 类的对象 object,尽管这两个线程需要调用不同的方法,但是它们是同步执行的,线程 B 需要等待线程 A 执行完了 methodA() 方法之后,它才能执行 methodB() 方法。这样,线程 A 和线程 B 就实现了 通信。

2、join() 等待执行

thread.join() 方法的作用:阻塞父线程,如果一个线程 A 执行了 thread.join() 语句,则线程 A 被阻塞,线程 A 必须要等待调用该 join() 方法的线程 thread 执行完毕后再从 join() 方法处返回再继续执行父线程 A
该方法常用的场景就是:子线程阻塞主线程(main函数所在线程),必须等子线程执行结束后主线程再去执行。

假设有两个线程,一个是线程 A,另一个是线程 B,两个线程分别依次打印 1-3 三个数字即可。

public class test implements Runnable{
    @Override
    public void run() {
        {
            int i = 0;
            while (i++ < 3) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " print: " + i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        test test1 = new test();
        Thread thread1= new Thread(test1, "lhj");
        Thread thread2= new Thread(test1, "zj");
        thread1.start();
        thread2.start();
    }
}

结果如下:
Java 基础——线程间通信_第1张图片
可以看到线程 lhj 和 zj 是同时打印的。那么,如果我们希望线程 zj 在线程 lhj 全部执行完后再执行,我们可以利用 thread.join() 方法,代码如下:

public class test implements Runnable{
    @Override
    public void run() {
        {
            int i = 0;
            while (i++ < 3) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " print: " + i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        test test1 = new test();
        Thread thread1= new Thread(test1, "lhj");
        Thread thread2= new Thread(test1, "zj");
        thread1.start();
        thread1.join();    // 阻塞父线程(主线程),使父线程(主线程)必须等待其执行完再继续执行;
        thread2.start();	// thread1 线程执行完后,父线程(主线程)再让 thread2 线程开始执行;
    }
}

结果如下:
Java 基础——线程间通信_第2张图片

thread1.join() 阻塞父线程(主线程),使父线程(主线程)必须等待其执行完再继续执行;thread1 线程执行完后,父线程(主线程)从 thread1.join() 处返回再继续执行 thread2.start(); 即让 thread2 线程开始执行。
因此可以看到 join() 方法会让线程 zj 一直等待直到线程 lhj 运行完毕再去执行。

3、wait/notify 机制(等待/通知机制)

在基于“锁”的方式中,线程需要不断地去尝试获得锁,如果失败了,再继续尝试,这可能会耗费服务器资源。而等待/通知机制是另一种方式。
Java 多线程的等待/通知机制是基于 Object 类的 wait() 方法、notify() 和 notifyAll() 方法来实现的。

(1)wait() :在执行 wait() 方法前,当前线程必须已获得锁调用它时会立即阻塞当前线程,在当前 wait() 处暂停线程进入等待状态同时,wait() 方法执行后,会立即释放获得的对象锁
(2)notify():在执行 notify() 方法前,当前线程也必须已获得锁。调用 notify() 方法后,会通知一个执行了 wait() 方法的阻塞等待线程,使该等待线程重新获取到对象锁,然后继续执行 wait() 后面的代码。
(3)如果有多个等待状态的线程,则需多次调用 notify() 方法,通知到线程顺序则根据执行 wait() 方法的先后顺序进行通知。如果是想通知所有等待状态的线程,可使用 notifyAll() 方法,就能唤醒所有线程。
Java 基础——线程间通信_第3张图片
注:线程执行 notify() 后不会立即释放对象锁,而是等该线程执行结束后才会释放锁,所以接收通知的 wait() 线程也不会立即获得锁,也需要等待执行 notify() 方法的线程释放锁后再获取锁

原理图:
Java 基础——线程间通信_第4张图片
wait 与 sleep 区别
Java 基础——线程间通信_第5张图片
示例:

public class test{

    public static void main(String[] args) throws InterruptedException {

        Object lock = new Object();
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println("A 1");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("A 2");
                    System.out.println("A 3");
                }
            }
        });
        Thread B = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println("B 1");
                    System.out.println("B 2");
                    lock.notify();
                    System.out.println("B 3");
                }
            }
        });

        A.start();
        B.start();
    }
}

结果如下:
在这里插入图片描述
上述代码的执行流程如下:首先我们创建一个由 A 和 B 共享的对象锁:Object lock = new Object();当 A 拿到锁时,先打印1,然后调用 lock.wait() 方法进入等待状态,然后释放锁; A 调用 lock.wait() 方法释放锁后 B 获得锁开始执行;B 拿到锁后打印1、2、3,然后调用 lock.notify() 方法唤醒正在等待的 A;A 唤醒后继续打印剩余的 2,3。

从执行结果中可以看到,B 线程执行 notify() 方法后,A 线程没有立即获得锁,notify() 方法并不会立即释放锁。

注意:

必须是 synchronized(lock) 而不能是 synchronized(this),否则会抛出 IllegalMonitorStateException 异常。因为 synchronized(lock) 是定义的共享锁,线程A和B用的是同一把锁,所以可以互相唤醒;而 synchronized(this) 锁定的是调用同步块的当前线程,所以线程A和B用的不是同一把锁。

4、Condition 的 await()/signal()

Condition 是在 java 1.5中出现的,它用来替代传统的 Object 的 wait()/notify() 实现线程间的协作,它的使用依赖于 Lock,Condition、Lock 和 Thread 三者之间的关系如下图所示。
Java 基础——线程间通信_第6张图片
相比使用 Object 的 wait()/notify(),使用 Condition 的 await()/signal() 这种方式能够更加安全和高效地实现线程间协作。Condition 是个接口,基本的方法就是 await()和signal() 方法。

Condition 依赖于 Lock 接口,Condition 对象的获取主要是通过 Lock.newCondition() 方法。一个 Lock 对象可以返回多个 Condition 对象。

必须要注意的是,Condition 的 await()/signal() 使用都必须在 lock 保护之内,也就是说,必须在 lock.lock() 和 lock.unlock 之间才可以使用

原理

condition 是要和 lock 配合使用的,创建一个 condition 对象是通过 lock.newCondition(),而这个方法实际上是会 new 出一个 ConditionObject 对象,该类是 AQS 的一个内部类。我们知道在锁机制的实现上,AQS 内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列,同样的,condition 内部也是使用同样的方式,内部维护了一个等待队列,等待队列是一个单向队列,所有调用 condition.await 方法的线程会加入到等待队列中,并且线程状态转换为等待状态。另外注意到 ConditionObject 中有两个成员变量:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

可以看出来 ConditionObject 通过持有等待队列的头尾指针来管理等待队列,而且 Node 类复用了在 AQS 中的 Node 类。

我们可以多次调用 lock.newCondition() 方法创建多个 condition 对象,也就是一个 lock 可以持有多个等待队列。而在之前使用 Object 的 wait()/notify() 方式实际上是指在对象 Object 对象监视器上只能拥有一个同步队列和一个等待队列,而并发包中的 Lock 拥有一个同步队列和多个等待队列。

(1)await 实现原理

当调用 condition.await() 方法后会使得当前获取 lock 的线程封装成 Node 通过尾插入的方式进入到等待队列,等待队列是一个不带头结点的链式队列。

(2)signal/signalAll 实现原理

调用 condition 的 signal 的前提条件是当前线程已经获取了 lock,调用 condition 的 signal 或者 signalAll 方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得 lock,同步队列是一个带头结点的链式队列。按照等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用 condition 的 signal 方法是将头节点移动到同步队列中,而移入到同步队列后才有机会使得等待线程被唤醒,即从 await 方法中的 LockSupport.park(this) 方法中返回,从而才有机会使得调用 await 方法的线程成功退出。

示例

public class ConditionTest{
    public static ReentrantLock lock=new ReentrantLock();
    public static Condition condition =lock.newCondition();

    public static void main(String[] args) {

        new Thread(){
            @Override
            public void run() {
                lock.lock(); // 必须先请求锁
                try{
                    System.out.println(Thread.currentThread().getName()+"==》进入等待");
                    condition.await(); // 设置当前线程进入等待
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }finally{
                    lock.unlock(); // 必须释放锁
                }
                System.out.println(Thread.currentThread().getName()+"==》继续执行");
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                lock.lock(); // 必须先请求锁
                try{
                    System.out.println(Thread.currentThread().getName()+"==》进入等待");
                    condition.await(); // 设置当前线程进入等待
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }finally{
                    lock.unlock(); // 必须释放锁
                }
                System.out.println(Thread.currentThread().getName()+"==》继续执行");
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                lock.lock(); // 必须请求锁
                try{
                    System.out.println(Thread.currentThread().getName()+"=》进入");
                    Thread.sleep(2000); // 休息2秒
                    condition.signal(); // 唤醒等待队列中的第一个线程
                    System.out.println(Thread.currentThread().getName()+"休息结束");
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }finally{
                    lock.unlock(); // 必须释放锁
                }
            }
        }.start();
    }
}

运行结果:
Java 基础——线程间通信_第7张图片
上述代码中,有两个线程 Thread-0 和 Thread-1 都通过 await() 方法进入等待队列,在 Thread-1 中使用 signal() 方法后会唤醒等待队列中的第一个线程进入同步队列,因此 Thread-0 被唤醒继续执行,由于 signal() 方法只使用一次因此 Thread-1 线程没有被唤醒而继续等待。若要唤醒多个等待队列中的线程需要多次调用 signal() 方法。

public class ConditionTest{
    public static ReentrantLock lock=new ReentrantLock();
    public static Condition condition =lock.newCondition();

    public static void main(String[] args) {

        new Thread(){
            @Override
            public void run() {
                lock.lock(); // 必须先请求锁
                try{
                    System.out.println(Thread.currentThread().getName()+"==》进入等待");
                    condition.await(); // 设置当前线程进入等待
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }finally{
                    lock.unlock(); // 必须释放锁
                }
                System.out.println(Thread.currentThread().getName()+"==》继续执行");
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                lock.lock(); // 必须先请求锁
                try{
                    System.out.println(Thread.currentThread().getName()+"==》进入等待");
                    condition.await(); // 设置当前线程进入等待
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }finally{
                    lock.unlock(); // 必须释放锁
                }
                System.out.println(Thread.currentThread().getName()+"==》继续执行");
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                lock.lock(); // 必须请求锁
                try{
                    System.out.println(Thread.currentThread().getName()+"=》进入");
                    Thread.sleep(2000); // 休息2秒
                    condition.signal(); // 唤醒等待队列中的第一个线程
                    condition.signal(); // 继续唤醒等待队列中的第一个线程
                    System.out.println(Thread.currentThread().getName()+"休息结束");
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }finally{
                    lock.unlock(); // 必须释放锁
                }
            }
        }.start();
    }
}

结果:
Java 基础——线程间通信_第8张图片
由于 Thread-2 多次调用 signal() 方法,因此 Thread-0 和 Thread-1 均被唤醒继续执行。

在调用 await() 方法前线程必须获得重入锁,调用 await() 方法后线程会释放当前占用的锁。同理在调用signal()方法时当前线程也必须获得相应重入锁,调用 signal() 方法后系统会从 condition.await() 等待队列中唤醒一个线程。当线程被唤醒后,它就会尝试重新获得与之绑定的重入锁,一旦获取成功将继续执行。所以调用 signal() 方法后一定要释放当前占用的锁,这样被唤醒的线程才能有获得锁的机会,才能继续执行

4、管道通信

管道流是是一种使用比较少的线程间通信方式,Java 中管道输入/输出流主要包括 4 种具体的实现:PipedOutputStrean、PipedInputStrean、PipedReader 和 PipedWriter,前两种面向字节,后两种面向字符。

Java 的管道的输入和输出实际上使用的是一个循环缓冲数组来实现的,默认为 1024,输入流从这个数组中读取数据,输出流从这个数组中写入数据,当这个缓冲数组已满的时候,输入流所在的线程就会被阻塞,当向这个缓冲数组为空时,输出流所在的线程就会被阻塞

Java 基础——线程间通信_第9张图片

buffer:缓冲数组,默认为1024;
out:从缓冲数组中读数据;
in:从缓冲数组中写数据;

你可能感兴趣的:(Java,并发编程,java,多线程)