首页 > 代码库 > Java多线程编程核心技术读书笔记(3)-线程通信

Java多线程编程核心技术读书笔记(3)-线程通信

  线程是操作系统中独立的个体,但是这些个体如果无法经过特殊的处理就不能成为一个整体。线程间通信可以实现线程间的信息互换、相互唤起等功能,是系统的交互性更加强大,大大提高CPU的利用率,同时还能让开发者对各个线程任务有清晰的把控和监督,最常用的线程通信方法就是——等待/通知机制。

  一、等待/通知机制

  1、wait() / notify() 

  等待/通知机制在生活中比比皆是,例如:厨师/服务员的菜品传递台、生产者/消费者模式,JDK中通过Object里面的两个方法 wait() / notify() 来实现了等待/通知机制;

  方法wait()的作用是使当前执行代码的线程进行等待,将其置入“预执行队列”,并且在wait()所在代码处停止执行,马上放开所持有的锁,直到接到通知或被中断为止。wait()方法必须置于同步方法或代码块内,否者运行时会抛出IllegalMonitorStateException;

  方法wait(long)是等待某一段时间,如果超过这个时间则线程自动唤醒;

  方法notify()也需要置于同步方法或代码块内执行,否者运行时会抛出IllegalMonitorStateException,该方法用于通知那些可能等待该对象所持的锁的其他线程,如果有多个等待线程,则随机挑选一个呈wait状态的线程,使其获得该对象的锁,但是并不会马上释放所持的锁,而是等到该线程执行完同步方法或者同步代码块之后,才会释放锁

  方法notifyAll()作用和notify()是一样的,只不过它会唤醒所有等待该资源的等待线程,进入到可运行状态,具体由哪个线程获得锁,取决于JVM实现,可能是优先级最高的或者随机;

  用wait() / notify()实现 生产者/消费者模式(用操作栈存储共享资源):

  操作栈实现:MyStack.java

public class MyStack {
    private List list = new ArrayList<>();

    synchronized public void push() {
        try {
            //为防止多生产者而导致list.size()>1的情况,不应该使用if,而是while,下同
            while(list.size() == 1) {
                System.out.println("push操作中的:" + Thread.currentThread().getName() + " 线程呈wait状态");
                this.wait();
            }
            list.add("antString=" + Math.random());
            //为防止多生产者时,唤醒的是同类不是异类而导致的假死,应该用notifyAll(),下同
            this.notifyAll();
            System.out.println("push=" + list.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public String pop() {
        String temp = "";
        try {
            while(list.size() == 0) {
                System.out.println("pop操作中的:" + Thread.currentThread().getName() + " 线程呈wait状态");
                this.wait();
            }
            temp = "" + list.get(0);
            list.remove(0);
            this.notifyAll();
            System.out.println("pop=" + list.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return temp;
    }
}

  生产者实现:Producer.java

public class Producer {
    private MyStack myStack;

    public Producer(MyStack myStack) {
        super();
        this.myStack = myStack;
    }

    public void pushService() {
        myStack.push();
    }
}

  生产者线程实现:ProducerThread.java

public class ProducerThread extends Thread {
    private Producer producer;

    public ProducerThread(Producer producer) {
        super();
        this.producer = producer;
    }

    @Override
    public void run() {
        while(true) {
            producer.pushService();
        }
    }
}

  消费者实现:Customer.java

public class Customer {
    private MyStack myStack;

    public Customer(MyStack myStack) {
        super();
        this.myStack = myStack;
    }

    public void popService() {
        myStack.pop();
    }
}

  消费者线程实现:CustomerThread.java

public class CustomerThread extends Thread {
    private Customer customer;

    public CustomerThread(Customer customer) {
        super();
        this.customer = customer;
    }

    @Override
    public void run() {
        while(true) {
            customer.popService();
        }
    }
}

  运行类:Run.java

//单生产者-单消费者
public class Run {
    public static void main(String[] args) {
        MyStack myStack = new MyStack();
        Producer producer = new Producer(myStack);
        Customer customer = new Customer(myStack);
        ProducerThread producerThread1 = new ProducerThread(producer);
        CustomerThread customerThread1 = new CustomerThread(customer);
        producerThread1.start();
        customerThread1.start();
    }
}

  2、管道通信

  Java中提供了各种各样的输入/输出流Stream,其中管道流(PipeStream)是一种特殊的流,可以用于在不同的线程间直接传送数据。

  在Java的JDK中提供了两种流-4个类来使线程间可以进行通信:

  1、字节流(PipedInputStream 和 PipedOutputStream)

  2、字符流(PipedReader 和 PipedWriter)

  代码Demo暂略。。。

  二、join()的使用

  1、join()

  很多情况,主线程创建并启动子线程,如果子线程执行时间很长,主线程会早于子线程结束。如果想让主线程等待子线程结束后再结束,就可以用到join()方法了。

  示例代码如下:Test.java

class MyThread3 extends Thread {
    @Override
    public void run() {
        try {
            System.out.println(System.currentTimeMillis() + "   我是子线程,我需要sleep一段时间!");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        try{
            MyThread3 myThread = new MyThread3();
            myThread.start();
            myThread.join();
            System.out.println(System.currentTimeMillis() + "   我是主线程,在子线程结束后才结束的!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  输出:

1493215918686   我是子线程,我需要sleep一段时间!
1493215923705   我是主线程,在子线程结束后才结束的!

  可以看出主线程确实是等待子线程sleep()了5s后,才结束的!

  2、join(long) 和 sleep(long) 的区别

  方法join(long)的功能是指当前线程至少等待该线程多久,如果该线程在时间前结束,那该方法和sleep气的作用是一样的。但是方法join(long)的功能在内部是使用wait(long)方法来实现的,所以join(long)方法具有释放锁的特点,可以看下join(long)的实现代码:

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;
            }
        }
    }

  三、ThreadLocal / InheritableThreadLocal的使用

  1、ThreadLocal

  静态变量可以让所有线程共同使用,ThreadLocal可以让每个线程都维护一个自己的值。可以将ThreadLocal比喻成全局存放数据的盒子,盒子中可以存放每个线程的私有数据。

  2、InheritableThreadLocal

  功能类似于ThreadLocal,但是InheritableThreadLocal可以让子线程可以继承父线程放在其中的值,同时也可以进行修改,但不会影响父线程。

Java多线程编程核心技术读书笔记(3)-线程通信