前段时间学习了java多线程的基础类,今天就了解一下java线程之间的通信。
五种通信方式
最近翻看《java并发变成艺术》这本书的时候,了解到有五种通信方式:
- volatile和synchronized关键字
- 等待通知
- 管道流
- Thread.join
- ThreadLocal
这四种方式,只简单的用过第一种。其它的看书的时候才知道。
volatile和synchronized关键字
我觉得这种方式最容易理解,两个关键字都是为了保证公共变量的原子性,举个形象的例子,一间卫生间只能有一个人用,所以卫生间有两个状态,有人用,没人用。这两个关键字就是保证卫生间只有一个人用,一旦用人进去了,其他人就会看到有人用的状态。
测试代码:
测试类
public class Test {
public synchronized void say(String name) {
SleepUtil.sleep(2000);
System.out.println(name);
}
}
public class SynchronizedTest {
public static void main (String[] args) {
Test test = new Test();
FirstTestRunnable first = new FirstTestRunnable(test);
FirstTestRunnable second = new FirstTestRunnable(test);
Thread thread = new Thread(first);
thread.setDaemon(true);
thread.start();
thread = new Thread(second);
thread.setDaemon(true);
thread.start();
SleepUtil.sleep(5000);
System.out.println("main线程结束");
}
static class FirstTestRunnable implements Runnable {
private Test test;
public FirstTestRunnable (Test test) {
this.test = test;
}
@Override
public void run () {
test.say(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + "结束");
}
}
}
执行结果:
Thread-0
Thread-0结束
Thread-1
Thread-1结束
main线程结束
从结果可以看到,线程0未结束之前,线程一没有进去。就相当于,一个人进了卫生间,其他人知道了,就没进去。
如果没有synchronized,执行结果是这样的:
Thread-0
Thread-1
Thread-1结束
Thread-0结束
main线程结束
可以看到线程0没结束,线程1就进去了。就相当于,一个人进了卫生间,另外一个人又进去了。
等待通知
先看测试代码:
public class NotifyTest {
private static boolean flag = true;
private static Object lock = new Object();
public static void main (String[] args) {
Thread wait = new Thread(new FirstRunnable(), "first");
wait.start();
SleepUtil.sleep(1000);
Thread notify = new Thread(new SecondRunnable(), "second");
notify.start();
SleepUtil.sleep(2000);
//wait.interrupt();
}
static class FirstRunnable implements Runnable {
@Override
public void run () {
synchronized (lock) {
while (flag) { // 拿到锁并且flag = true 才会执行
try {
System.out.println(Thread.currentThread().getName() + " flag is true @" + TimeUtil.getFullCurrentTime());
lock.wait();//会释放lock的锁
} catch (Exception e) {
e.printStackTrace();
}
}
}
System.out.println(Thread.currentThread().getName() + " flag is false @" + TimeUtil.getFullCurrentTime
());
}
}
static class SecondRunnable implements Runnable {
@Override
public void run () {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " hold lock notifyall @" + TimeUtil
.getFullCurrentTime
());
flag = false;
lock.notifyAll();
SleepUtil.sleep(5000);
} // 执行完释放锁
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " hold lock again @" + TimeUtil.getFullCurrentTime
());
SleepUtil.sleep(5000);
}
}
}
}
执行结果:
first flag is true @2017-09-28 15:34:41
second hold lock notifyall @2017-09-28 15:34:42
second hold lock again @2017-09-28 15:34:47
first flag is false @2017-09-28 15:34:52
这是书上的例子。
这是我理解的执行过程:
- first线程先启动了,并且一秒以后才启动的second线程,所以first线程首先获取lock对象的锁,执行了代码,输出了first flag is true @2017-09-28 15:34:41;
- 当first线程执行lock.wait方法后,释放了lock的锁,并且当前线程会一直处于等待状态,直到收到通知为止。
- 因为first线程处于等待状态,只有second线程,此时second线程会拿到lock对象的锁。
- second线程拿到锁后,开始执行代码,输出了second hold lock notifyall @2017-09-28 15:34:42;
- 输出完后,second线程执行了notifyAll通知全部等待线程,准备抢lock对象的锁;
- 当second的同步代码块执行完毕,lock正式释放,first和second线程开始抢锁,谁抢到了,谁执行;
- 从执行结果上可以看到,second线程抢锁成功,执行了代码
- 当second执行完毕,first拿到了线程,此时flag = false,first也执行完毕。
从上面测试代码可以看到,这种通信方式需要一个公共变量,需要多个线程,还需要同步关键字的支持;还要使用object的wait 、notify 、notifyAll方法。
我记得线程有一个中断状态,我测试了一下,join、wait 与sleep遇到中断时,会抛出InterruptedException异常,然后清除中断状态。
目前,我还没遇到过这种方式的使用场景。
管道流
这种方式完全没听说过,看书的时候才知道有这个东西。
测试代码:
public class PipedTest {
public static void main (String[] args) throws Exception {
PipedReader reader = new PipedReader();
PipedWriter writer = new PipedWriter();
reader.connect(writer);
Thread thread = new Thread(new PipedRunnable(reader), "piped");
thread.start();
int receive = 0;
try {
while ((receive = System.in.read()) != -1) {
writer.write(receive);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
writer.close();
}
}
static class PipedRunnable implements Runnable {
private PipedReader reader;
public PipedRunnable (PipedReader reader) {
this.reader = reader;
}
@Override
public void run () {
int receive = 0;
try {
while ((receive = reader.read()) != -1) {
System.out.print( (char) receive);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
执行结果:
123
123
这种方式感觉就像这样:
底层不知道怎么实现的,个人理解应该是把数据写入到内存中,缓存起来,另外一个线程读取出来。
不过这样的话,应该只能有一个线程读,因为缓存的内容读取之后应该就没了,再读就是新写入的了。
另外管道流类有字符流(PipedReader, PipedWriter)和字节流(PipedInputStream, PipedOutputStream),使用的时候载百度,现在先不管这个了。
Thread.join
这个方法会使当前线程处于等待状态,直到调用join方法的线程结束,才会继续执行。
测试代码:
public class JoinTest {
public static void main (String[] args) {
System.out.println("启动");
Thread thread = new Thread(new JoinRunnable());
try {
thread.start();
thread.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("main线程结束");
}
static class JoinRunnable implements Runnable {
@Override
public void run () {
SleepUtil.sleep(3000);
System.out.println("睡眠三秒结束");
}
}
}
执行结果:
启动
睡眠三秒结束
main线程结束
表面上看,主线程在等待,可是什么会等待呢,不明白它的原理。
这是join方法的源码:
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
millis等于0,所以执行这一块:
while (isAlive()) {
wait(0);
}
这一块代码执行受限于isAlive()方法,看一下这个方法:
/**
* Tests if this thread is alive. A thread is alive if it has
* been started and has not yet died.
*
* @return <code>true</code> if this thread is alive;
* <code>false</code> otherwise.
*/
public final native boolean isAlive();
注释上说,如果一个线程已经start并且还没死掉就是true,也就是说我的代码执行的时候都是true,所以此时是无限循环,一直执行wait方法,可是wait方法会导致调用的线程处于等待状态,main线程里调用的join方法,所以main线程会等待,那谁执行了notifyAll或者notify方法呢,网上百度了一下,发现知乎上有大神说在jvm中执行的,见知乎链接,果然还是大神厉害!
ThreadLocal的使用
ThreadLocal被称为线程变量,感觉就是把原来的变量给每个线程复制了一份,大家各用各的,谁都不影响谁。
测试代码:
public class ThreadLocalTest {
private static ThreadLocal<Long> num = new ThreadLocal<Long>();
public static void main (String[] args) {
Thread one = new FisrtTest("first");
one.setDaemon(true);
Thread two = new FisrtTest("second");
two.setDaemon(true);
one.start();
two.start();
SleepUtil.sleep(5000);
}
static class FisrtTest extends Thread {
private String name;
public FisrtTest (String name) {
this.name = name;
}
@Override
public void run () {
long i = 0;
while (i < 5) {
i++;
num.set(i);
SleepUtil.sleep(1000);
System.out.println( name + " 的num值=" + num.get());
}
}
}
}
测试结果:
first 的num值=1
second 的num值=1
second 的num值=2
first 的num值=2
first 的num值=3
second 的num值=3
first 的num值=4
second 的num值=4
second 的num值=5
first 的num值=5
从结果上可以看到,初始值是一样的,两个线程没有相互影响。
虽然不知道底层怎么实现这种效果的,但是从用法来看,这种方式适合多个线程共享变量,但是线程操作又不相互影响的场景,目前没有遇到需要使用这种方式的场景。
总结
目前了解了这些通信的方式,有助于以后需要线程通信的业务功能实现。