Java多线程(四)-线程通信


前言

当线程在系统内运行时,程序无法精准控制线程轮换执行,Java提供了一些机制来保证线程协调运行。

synchronized线程通信相关方法

借助Object类提供的wait(),notify(),notifyAll()三个方法(不属于Thread类),但这三个必须由同步监视器调用,,这可以分成以下情况:

  • 对于同步方法,该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
  • 对于同步代码块,同步监视器是synchronized后括号里的对象,所以要使用该对象调用。

关于这三个方法的解释:

  • wait():让当前线程等待,直到其他线程调用该同步监视器的notify()notifyAll()方法来唤醒线程。无参则一直等待,带参则等待long millis毫秒时间自动唤醒。
  • notify():唤醒在此同步监视器上等待的单个线程,如果有多个线程在等待同步监视器,则随机唤醒其中一个线程。只有当前线程主动放弃锁,被唤醒线程才获得执行。
  • notifyAll():唤醒在此同步监视器上等待的所有线程。
使用Condition的线程通信

如果不使用synchronized关键字保证线程同步,而使用Lock对象保证线程同步,则系统中不存在隐式的同步监视器,也就不能使用wait()notify()notifyAll()方法进行线程通信。因此,当使用Lock对象来保证同步时,Java提供了Condition类来保证协调,使用Condition可以让那些已经得到Lock对象却无法执行的线程释放Lock对象,同时也能唤醒其他处于等待的线程。

Condition将同步监视器的方法(wait(),notify(),notifyAll())分成不同的对象,便于与Lock对象结合,Lock代替了同步方法和代码块,Condition代替了同步监视器功能。Condition实例被绑定在Lock对象上,想要获得Lock对象的Condition实例,就要调用Lock对象的newCondition()方法。

Condition类提供如下方法:

  • await():类似隐式同步监视器的wait()方法,导致当前线程等待,直到其他线程调用该Conditionsignal()signalAll()方法来唤醒该线程。衍生的方法有很多,awaitUninterruptibly()awaitUntil(Date deadline)
  • signal():唤醒在此Lock对象上等待的单个线程,若有多个线程等待,则随机唤醒其中一个。
  • signalAll():唤醒所有在此Lock对象等待的所有线程,只有当前线程放弃对该Lock对象的锁定后才可以执行被唤醒的线程。
使用阻塞队列(BlockingQueue)的线程通信

Java5提供了一个BlockingQueue接口,是Queue的子接口,但它的主要途径并不是容器,而是作为线程同步的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞,当消费者线程试图从BlockingQueue中取出元素时,如果该队列为空,则该线程被阻塞。

程序的两个线程交替向BlockingQueue中放入,取出元素,就能控制线程通信。BlockingQueue提供了如下两个方法:

  • put(T t):把T元素放入BlockingQueue中,如果该队列已满,则阻塞该线程。
  • take():从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法。

  • 在队尾插入元素。包括add(T t)offer(T t)put(T t)方法,当队列已满,这三个方法分别会抛出异常,返回false,阻塞队列。
  • 在队首删除并返回元素。包括remove()poll()take()方法,当队列为空时,这三个方法分别会抛出异常,返回false,阻塞队列。
  • 在队首仅仅取出元素。包括element()peek()方法,当队列为空,方法分别抛出异常和返回false。

可用如下表格表示:

抛出异常 返回false 阻塞线程 指定超时时长
队列已满时,队尾插入元素 add() offer() put() offer(e,time,unit)
队列已空时,队首删除元素 remove() poll() take() poll(time,unit)
队列已空时,队首取出元素 element() peek()

BlockingQueue包含以下几个实现类:

  • ArrayBlockingQueue:基于数组实现的BlockingQueue队列。
  • LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
  • SynchronousQueue:同步队列,对该队列的存取必须交替进行。
  • PriorityBlockingQueue:不是标准的阻塞队列,与PriorityQueue类似,该队列调用remove()poll()take()方法取出元素时,并不是取出队列中存在时间最长的元素,而是最小的元素。判断元素大小可根据元素(实现Comparable接口)的本身大小进行自然排序,也可使用Comparator自定义排序。
  • DelayQueue:底层基于PriorityBlockingQueue实现,要求元素都实现Delay接口,接口里只有一个long getDelay()方法,DelayQueue根据元素的getDelay()方法的返回值进行排序。
public class BlockingQueueTest{
    public static void main(Strings[] args) throws Exception{
        BlockingQueue<String> b = new ArrayBlockingQueue<>(2);
        b.put("阻塞队列");
        b.put("阻塞队列");
        b.put("阻塞队列");//队列已满,阻塞线程
    }
}
线程组和未处理异常

Java使用ThreadGroup来表示线程组,它可以对一批线程进行管理,允许程序直接对线程进行控制。如果没有显式指定创建的线程属于哪个线程组,则属于默认线程组。默认情况下,子线程和创建它的线程属于同一线程组。一旦线程加入了指定的线程组,则该线程一直属于该线程组,直至死亡不能改变。

Thread类提供了如下几个构造器来设置创建的线程属于哪个线程组:

  • Thread(ThreadGroup group,Runnable target):以target的run方法作为线程执行体创建新的线程,属于group线程组
  • Thread(ThreadGroup group,Runnable target,String name):同样的,只不过指定了创建的线程名字。
  • Thread(ThreadGroup group,String name):创建新线程,指定线程名。

虽然不能改变所在指定的线程组,但可以通过getTreadGroup()方法返回所属线程组,返回值是ThreadGroup对象。而ThreadGroup类提供了两个构造器创建相应实例:

  • ThreadGroup(String name):以指定的线程组名字来创建新的线程组。
  • ThreadGroup(ThreadGroup parent,String name):以指定的的名字和父线程组创建新的线程组。

通过以上构造器可以发现,线程组必然有一个名字,这个名字可以通过ThreadGroupgetName()方法获取,但是不允许改变线程组的名字,也就没有set方法。除了构造器,ThreadGroup还提供了几个方法来操作线程组里的所有线程:

  • activeCount():返回线程组中活动线程的数目。
  • interrupt():中断此线程组的所有线程。
  • isDaemon():判断该线程组是否是后台线程组。
  • setDaemon():把线程组设置为后台线程组,若后台线程组的最后一个线程死亡,则线程组自动毁灭。
  • setMaxPriority(int p):设置线程组的最高优先级。

此外,线程组对于出现的异常也提供了一个方法void uncaughtException(Thread t,Throwable e),该方法可以处理该线程组内的任意线程所抛出的未处理异常。如果线程抛出一个异常,JVM会在线程结束前自动查找对应的Thread.UncaughtExceptionHandler对象,如果找到该异常处理器对象,则会调用该对象的uncaughtException(Thread t,Throwable e)方法处理异常。

Thread.UncaughtExceptionHandlerThread类的一个静态内部接口,里面只有一个方法uncaughtException(Thread t,Throwable e)t代表异常的线程,e代表抛出的异常。Thread类提供了如下方法来设置异常处理器:

  • static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler h):为该线程类的所有实例设置默认异常处理器。
  • setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler h):为线程实例设置异常处理器。

ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程的线程组都将作为默认的异常处理器。当一个线程抛出异常时,JVM会先查找线程实例指定的异常处理器,否则会调用所属线程组对象的uncaughtException()方法(默认的异常处理器)处理异常。

线程组默认异常处理器处理过程如下:

  • 如果该线程组有父线程组,则调用父线程组的uncaughtException()方法处理。
  • 如果该线程所属的默认线程组有默认异常处理器,那么就调用该异常处理器处理。
  • 如果该异常ThreadDeath的对象,则不做任何处理,否则将异常跟踪栈的信息打印到System.err输出流,并结束该线程。
Class MyHandler implements Thread.UncaughtExceptionHandler{//自定义处理器
    public void uncaughtException(Thread t,Throwable e){
        System.out.println(t+" 出现异常 "+e);
    }
}
public class ExHandler{
    public static void main(String[] args){
        Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler());
        int a = 1/0;//ArithmeticException
        System.out.println("程序已结束!");//不会正常结束,所以不会输出
    }
}

上诉代码虽然捕获了异常,但是出现不会正常结束。因为与try...catch异常捕获不同,异常处理器对异常进行处理后会将异常抛给上一级调用者,而catch捕获异常不会。


文章作者: 江南骚年
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 江南骚年 !
评论
 上一篇
Java多线程(五)-线程池 Java多线程(五)-线程池
线程池-前言系统启动一个线程的成本较高,而使用线程池可以提高性能,尤其在菜鸡大量短期线程时。与数据库连接池类似,线程池在系统启动时创建大量空闲线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会执行他们的run(
2021-01-27
下一篇 
Java-IO流(三)-NIO Java-IO流(三)-NIO
Java-IO流(三)-NIO在前面所介绍的输入输出流都是阻塞式的输入、输出,即当数据源中没有数据时,它会阻塞该线程。传统的输入、输出都是通过字节的移动来处理的,就是输入输出系统一次只能处理一个字节,因此效率并不高。从JDK1.4开始,Ja
2021-01-21
  目录