线程间的通信
如果你知道进程间的通信,那么对你来说,线程间的通信将会很容易,当你在开发一个产品当时候,如果涉及到两个或多个线程间交换信息,那么线程间的通信知识对你来说,将会非常重要。
下面有三个简单的方法和技巧来让线程间的通信成为可能:
序号 | 方法描述 |
---|---|
1 | public void wait();导致当前线程阻塞,知道另外一个线程执行notify()方法 |
2 | public void notify();唤醒一个对象监听器上正在等待的线程 |
3 | public void notifyAll();唤醒所有在同一个对象上执行过wait()方法的线程 |
这些方法在Object对象中是作为final类型实现的,所以这些方法在所有的类里都可以使用,以上三个方法,只能在synchronized上下文中执行;
以下的这个例子,将向你展示如何在两个线程之间,通过wait()和notify()这两个方法实现通信。
定义Chat类
/**
* Created by choleece on 2019/4/20.
*/
public class Chat {
boolean flag = false;
public synchronized void question(String msg) {
if (flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(msg);
flag = true;
notify();
}
public synchronized void answer(String msg) {
if (!flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(msg);
flag = false;
notify();
}
}
定义T1类
/**
* Created by choleece on 2019/4/20.
*/
public class T1 implements Runnable {
Chat m;
String[] s1 = {"Hi!I am T1", "How are you?", "I am also doing fine!"};
public T1(Chat m1) {
this.m = m1;
new Thread(this, "question").start();
}
@Override
public void run() {
for (int i = 0; i < s1.length; i++) {
m.question(s1[i]);
}
}
}
定义T2类
/**
* Created by choleece on 2019/4/20.
*/
public class T2 implements Runnable {
Chat m1;
String[] s2 = {"Hi!I am T2", "I am good, What about you?", "Great!"};
public T2(Chat m1) {
this.m1 = m1;
new Thread(this, "answer").start();
}
@Override
public void run() {
for (int i = 0; i < s2.length; i++) {
m1.answer(s2[i]);
}
}
}
定义TestThread类
/**
*
* @author choleece
* @date 2019/4/20
*/
public class TestThread {
public static void main(String[] args) {
Chat m = new Chat();
new T1(m);
System.out.println("main thread");
new T2(m);
}
}
查看运行结果:
main thread
Hi!I am T1
Hi!I am T2
How are you?
I am good, What about you?
I am also doing fine!
Great!
接下来,我们一步一步分析,为什么会出现以上打印结果,看看与我们预期的是否一致。
- 首先,实例化一个chat对象,如下:
Chat m = new Chat();
- 接着实例化T1,将chat作为参数,传入构造函数.
new T1(m);
- 在T1的构造函数里,我们看到,会将m对象赋值给t1的Chat属性,紧接着,创建一个线程,然后执行start()方法[注意,此时有可能子线程start()方法还没开始执行,也即子线程没获取到CPU的时间片,函数会继续往下执行,去实例化T2,但是不管如何,都不影响主流程],在子线程执行start()后,函数会进入到run()方法里,如下
@Override
public void run() {
for (int i = 0; i < s1.length; i++) {
m.question(s1[i]);
}
}
- 函数会执行m.question("Hi"),进而进入到Chat类的question(String msg)方法里,如下:
public synchronized void question(String msg) {
if (flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(msg);
flag = true;
notify();
}
- 此时,T1子线程获取到m的对象锁,判断flag,初始时,question()方法直接进行打印及以后操作:
// 此时会打印第一次Hi!(也即t1.s1[0])
System.out.println(msg);
flag = true;
notify();
- 紧接着,将flag置为true,根据上边的描述T2可能已经被实例化了,此时t2执行answer方法,如下:
public synchronized void answer(String msg) {
if (!flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(msg);
flag = false;
notify();
}
由于t1在第一次for循环执行question()后占用了对象m,此时还是占用锁,所以即使t2进入了for循环,也获取不到m对象的锁,只能继续被阻塞,此时t1又获得CPU时间分片,继续执行第二次for循环。
在t1的第一次for循环中,已经将flag设置为true,所以第二次进入的时候,会进去执行wait()操作,从而将自身挂起,并且释放刚才锁占用的对象锁。t2线程再次获取到CPU的时间分片,然后执行第一次for循环,由于此时flag为true,所以t2会执行如下打印语句,并且将flag设置为false,同时执行notify,t1被唤醒:
// 此时会打印Hi! I am T2, 也即t2.s2[0]
System.out.println(msg);
flag = false;
notify();
- 此时,t2还未释放掉m的对象锁,然后进入t2的第二次循环,此时flag=false, t2会执行wait()操作,将自己挂起,同时释放掉m的对象锁,此时t1会获得CPU的时间分片,会继续沿着上次的断点执行,也即打印
System.out.println(t1.s1[1]) -> How are you?
然后执行flag = true,然后执行notify()操作,同步唤醒t2,然后在t1进行第三次for循环后,又将自己wait(),同时释放自身锁。
紧接着t2获取到CPU的时间分片,接着上次挂起的断点执行,t2.answer执行打印操作,打印t2.s2[1] -> I am good, What about you?,然后将flag设置为false,并且执行notify()
t2在第三次循环后,由于flag为false,则会执行wait()操作,将自己挂起,并且释放掉m的对象锁,
t1在被分到CPU时间分片后,接着上次的断点执行,打印t1.s1[2] -> I am also doing fine!,同时将flag=true,然后notify(),t2被唤醒,至此,t1执行完毕,生命终止
t2在获得CPU时间分片后,接着上次的断点执行,答应t2.s2[2] -> Great!,至此,t2的生命终止。程序执行完毕!