Java - 多线程 - interrupt & sleep

  1. 并发编程模型概述
    11. 对象与共享的内存:Java对象类似C++中在堆空间new出来的结构体的指针或引用,多线程在处理这样的一个变量时,就是在操作一块共享的内存。共享内存这个词本身是一个进程间通信ICP的概念,但进程也好,线程或协程也好,本质原理是一样的,就像Linux里进程与线程的结构体是同一种类型。对象默认是共享的,除非是仅被局部变量持有的对象。同步,或者说加锁,造成的竞争等待太耗时。完全非共享的局部变量又会造成没必要的资源占用。幸好还有一种ThreadLocal类,它在初始化的时候指定泛型参数继而指定要持有何种类型以及应对如何初始化(通过withInitial(supplier)方法),在每个线程中,每当第一次执行get方法的时候执行supplier进行对象构建。因为static基本上就是全局共享的,而static的ThreadLocal就提供了一种线程局部的高效率的static变量。
    12. 这里,不得不提多线程共享内存之外的另一种并发通信模型,即Erlang和Go那种基于消息的并发编程模型,尤其是Erlang,其并发体之间并不共享内存。以继承Erlang基于消息的并发模型的Go为例,其goroutine与channel(这里有一个用channel实现斐波那契数列和素数筛的例子,形象地说,channel就像是一个信箱)的语法极其简洁,并发体的管理与通信机制都是被屏蔽掉的,当然,我们也可以直接读Go中实现chan的源码以深入了解其本质,而其本质也就是一种对阻塞队列的封装而已,或者是,一个带锁的动态数组或链表。
    13. 多线程之间的切换是通过线程状态来标识的。对于单线程来说,可以通过状态机来实现这种等待同步的效果,在被等待事件发生时,读取状态并按照驱动表执行对应的过程。
  2. interrupted置位及其OOP设计:Java的打断只是线程的一个flag,运行中被“打断”的线程状态依然还是运行态Runnable的,是否采纳被打断的外部请求进而终止运行的决定权在被“打断”的线程自己。所以,与其说是打断,不如说是被打扰。方法interrupt用于“打断”一个线程,其实就是给被打断的线程对象置位了interrupted。判断一个线程是否被置位“打断”了的方法有两个:静态方法interrupted判断当前线程是否被打断了,它会清除掉interrupted置位;而动态对象方法isInterrupted不会清除置位。可以通过任意线程对象调用静态方法interrupted,但使用线程类方法的方式Thread.interrupted更不容易引起混淆。静态方法清除置位的缘由,是它针对的是当前线程,而当前运行的线程本来就处理这个被打断的讯息,并进而允许被再次打断。而动态对象方法就仅仅只是查看线程对象的状态,它基本就只是一个给其他线程用的一个get接口。Thread.interrupted其实就是Thread.currentThread.isInterrupted外加清除interrupted置位,只不过,基本上应该不会使用Thread.currentThread.isInterrupted这样仅仅获取状态而不进行实际处理的接口,而一旦处理了,一般也就会允许继续被打断。
  3. 线程属性:除了打断的flag外,getId可以获取线程的ID,是一个很好的调试日志的标志。getState则可以获取线程的状态。
  4. sleep(Timed_waiting)与interrupted(interrupt)的互斥及其异常抛出:一句话,interrupted时被sleep、Timed_waiting被interrupt都会抛出InterruptedException异常。sleep也是一个静态方法,同样是作用于当前在运行的线程,Thread.sleep使当前线程以millis为时间单位睡眠,这与interrupted一样是一种当前进程完全自主的行为。睡眠中的线程状态是Timed_waiting的。正在固定时间等待,即睡眠的线程被打断时,正在睡眠的线程会真的被打断,以抛出异常InterruptedException的方式。睡眠状态是自主进入的等待定时醒来的状态,它定时醒来,或提前被打断而醒来。反之,置位被打断的interrupted的运行态Runnable的线程尝试睡眠sleep时,也会抛出该异常。最后,抛出异常的时候,打断状态也会被去置位,这在Timed_waiting睡眠状态被打断而进入Runnable状态的过程中是短暂被置位又迅速被去置位的,而在被打断的Runnable的线程尝试睡眠的时候则只涉及去置位;这些方法,应该也包括会去置位的interrupted接口,都不是原子的,或者至少不是volatile的,即产生异常、抛出异常与去置位一系列操作并非原子性的。抛出异常与去置位的行为是一致的,都提示着,这个讯息是必须处理的。两种抛出异常的互斥行为之间也是一致的,如果睡眠可以被打断,即,被打断这个讯息的处理优先级要高于继续睡眠,那么,在进入睡眠前,自然,也有必要确认没有被打断的讯息。极端情况下,在当前线程确认没有被打断的时候,在进入睡眠的时候,此时,线程被置位打断了,线程进入睡眠以后,就无法再次感知到被打断的这个讯息了。所以,进入睡眠这个过程必须和置位interrupted是互斥的。sleep的时候,加锁,保证interrupted的置位无法被修改,在确认interrupted未置位并进入睡眠状态的之后,才会释放锁,允许后续对该线程的interrupt操作。但就像前面说的,抛出异常、去置位打断和线程状态的切换这些操作则不具有原子性。
  5. 正是因为sleep方法威胁抛出一个checked异常,所以必须提供这个异常的捕获机制,并在设计捕获后对打断进行处理或再次置位打断。但是,如果确认线程并不会被打断而捕获机制无意义的时候,就可以利用强制类型转换为unchecked异常(即像数组越界、空指针引用、找不到指定的类或方法、强制类型转换失败等RuntimeException)来压缩可能抛出的异常,如下所示,接口中的静态方法接受一个可以抛出任何异常的该接口类型,并返回一个不会抛出unchecked异常的Runnable对象。
    interface ThrowableRunnable {
        void run() throws Exception;
        static Runnable toRunnable(ThrowableRunnable throwableRunnable) {
            return () -> {
                try {
                    throwableRunnable.run();
                } catch (Exception e) {
                    throw (RuntimeException) e;
                }
            };
        }
    }
    
posted @ 2022-06-29 02:05  joel-q  阅读(125)  评论(0编辑  收藏  举报