线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作,最典型的例子就是生产者-消费者问题。虽然通常每个子线程只需要完成自己的任务,但是有时我们希望多个线程一起工作来完成一个任务,这就涉及到线程间通信。线程通信主要可以分为三种方式,分别为共享内存、消息传递和管道流:
(1)
共享内存
1)volatile 关键字
2)synchronized 关键字
3)Lock 锁(2)
消息传递
1)Object 的 join()/notify() 机制;
2)Lock 和 Condition 的 await()/signal() 机制;
3)join() 方法
4)CountdownLatch;
5)CyclicBarrier;(3)
管道通信
线程同步可以通过 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 就实现了 通信。
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();
}
}
结果如下:
可以看到线程 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 线程开始执行;
}
}
thread1.join() 阻塞父线程(主线程),使父线程(主线程)必须等待其执行完再继续执行;thread1 线程执行完后,父线程(主线程)从 thread1.join() 处返回再继续执行 thread2.start(); 即让 thread2 线程开始执行。
因此可以看到 join() 方法会让线程 zj 一直等待直到线程 lhj 运行完毕再去执行。
在基于“锁”的方式中,线程需要不断地去尝试获得锁,如果失败了,再继续尝试,这可能会耗费服务器资源。而等待/通知机制是另一种方式。
Java 多线程的等待/通知机制是基于 Object 类的 wait() 方法、notify() 和 notifyAll() 方法来实现的。
(1)wait() :
在执行 wait() 方法前,当前线程必须已获得锁
。调用它时会立即阻塞当前线程,在当前 wait() 处暂停线程进入等待状态
。同时,wait() 方法执行后,会立即释放获得的对象锁
。
(2)notify():在执行 notify() 方法前,当前线程也必须已获得锁
。调用 notify() 方法后,会通知一个执行了 wait() 方法的阻塞等待线程,使该等待线程重新获取到对象锁,然后继续执行 wait() 后面的代码。
(3)如果有多个等待状态的线程,则需多次调用 notify() 方法,通知到线程顺序则根据执行 wait() 方法的先后顺序进行通知。如果是想通知所有等待状态的线程,可使用 notifyAll() 方法,就能唤醒所有线程。
注:线程执行 notify() 后不会立即释放对象锁,而是等该线程执行结束后才会释放锁,所以接收通知的 wait() 线程也不会立即获得锁,也需要等待执行 notify() 方法的线程释放锁后再获取锁
。
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用的不是同一把锁。
Condition 是在 java 1.5中出现的,它用来替代传统的 Object 的 wait()/notify() 实现线程间的协作,它的使用依赖于 Lock,Condition、Lock 和 Thread 三者之间的关系如下图所示。
相比使用 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();
}
}
运行结果:
上述代码中,有两个线程 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();
}
}
结果:
由于 Thread-2 多次调用 signal() 方法,因此 Thread-0 和 Thread-1 均被唤醒继续执行。
在调用 await() 方法前线程必须获得重入锁,调用 await() 方法后线程会释放当前占用的锁
。同理在调用signal()方法时当前线程也必须获得相应重入锁
,调用 signal() 方法后系统会从 condition.await() 等待队列中唤醒一个线程。当线程被唤醒后,它就会尝试重新获得与之绑定的重入锁,一旦获取成功将继续执行。所以调用 signal() 方法后一定要释放当前占用的锁,这样被唤醒的线程才能有获得锁的机会,才能继续执行
。
管道流是是一种使用比较少的线程间通信方式,Java 中管道输入/输出流主要包括 4 种具体的实现:PipedOutputStrean、PipedInputStrean、PipedReader 和 PipedWriter,前两种面向字节,后两种面向字符。
Java 的管道的输入和输出实际上使用的是一个循环缓冲数组来实现的
,默认为 1024,输入流从这个数组中读取数据,输出流从这个数组中写入数据,当这个缓冲数组已满的时候,输入流所在的线程就会被阻塞,当向这个缓冲数组为空时,输出流所在的线程就会被阻塞
。
buffer:缓冲数组,默认为1024;
out:从缓冲数组中读数据;
in:从缓冲数组中写数据;