多线程基础学习四:线程间的通信

前段时间学习了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

这是书上的例子。
这是我理解的执行过程:

  1. first线程先启动了,并且一秒以后才启动的second线程,所以first线程首先获取lock对象的锁,执行了代码,输出了first flag is true @2017-09-28 15:34:41;
  2. 当first线程执行lock.wait方法后,释放了lock的锁,并且当前线程会一直处于等待状态,直到收到通知为止。
  3. 因为first线程处于等待状态,只有second线程,此时second线程会拿到lock对象的锁。
  4. second线程拿到锁后,开始执行代码,输出了second hold lock notifyall @2017-09-28 15:34:42;
  5. 输出完后,second线程执行了notifyAll通知全部等待线程,准备抢lock对象的锁;
  6. 当second的同步代码块执行完毕,lock正式释放,first和second线程开始抢锁,谁抢到了,谁执行;
  7. 从执行结果上可以看到,second线程抢锁成功,执行了代码
  8. 当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());
            }
        }
    }

}

测试结果:

firstnum值=1
secondnum值=1
secondnum值=2
firstnum值=2
firstnum值=3
secondnum值=3
firstnum值=4
secondnum值=4
secondnum值=5
firstnum值=5

从结果上可以看到,初始值是一样的,两个线程没有相互影响。

虽然不知道底层怎么实现这种效果的,但是从用法来看,这种方式适合多个线程共享变量,但是线程操作又不相互影响的场景,目前没有遇到需要使用这种方式的场景。

总结

目前了解了这些通信的方式,有助于以后需要线程通信的业务功能实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值