Java核心技术Ι(1)——并发之线程

基本概念

    大家应该都很熟悉操作系统中的多任务(multitasking):在同一时刻运行多个线程的能力。今天人们大多拥有单台拥有多个CPU的计算机,但是,并发执行的线程数目并不是由CPU数目制约的。操作系统将CPU的时间片分配给每一个进程,给人并行处理的感觉。
    多线程程序在较低的层次上扩展了多任务的概念:一个程序同时执行多个任务。通常每一个任务称为一个线程(thread),它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序(multithreaded)。

多线程与多进程的区别:

  1. 每个进程拥有自己的一整套变量,而线程则共享数据,共享数据风险较高,也使得线程之间的通信比进程之间的通信更有效、更容易;
  2. 在有些操作系统中,与进程相比较,线程更“轻量级”,创建、撤销一个线程比启动新进程的开下要小得多。

要想在一个单独的线程中执行一个任务,可以使用Runnable接口,接口定义非常简单:

public interface Runnable {
    void run();
}

由于Runnable是一个函数式接口,可以用lambda表达式建立一个实例:

Runnable r = () -> { task code };

Runnable创建一个Thread对象:

Thread t = new Thread(r);

启动线程:

t.start();

也可以通过构建一个 Thread 类的子类定义一个线程, 如下所示:

public myThread extends Thread {
    public void run() {
        task code
    }
}

然后构造一个子类的对象,并调用 start 方法。 不过这种方法已不再推荐,应该将要并行运行的任务与运行机制解耦合。如果有很多任务,要为每个任务创建一个独立的线程所付出的代价太大了,可以使用线程池来解决这个问题。
注意:不要调用 Thread 类或继承了 Runnable 对象的 run方法。直接调用 run方法,只会执行同一个线程中的任务,而不会启动新线程。应该调用Thread.start() 方法。这个方法将创建一个执行 run方法的新线程。

中断线程

    当线程的 run 方法执行方法体中最后一条语句后,并经由执行 return 语句返回时,或者出现了在方法中没有捕获的异常时,线程将终止。 在 Java 的早期版本中, 还有一个 stop 方法, 其他线程可以调用它终止线程,但是这个方法现在已经被弃用了。—TODO 原因。没有可以强制线程终止的方法,interrupt 方法可以用来请求终止线程,当对一个线程调用 interrupt 方法的时候,线程的中断状态将被置位,中断状态位是每个线程都具有的boolean标志,每个线程都应该不时的检查这个标志,以判断线程是否被中断。要想弄清中断状态是否被置位,首先调用静态的Thread.currentThread方法获得当前线程,然后调用isInterrupted方法:

while(!Thread.currentThread().isInterrupted() && more work to do)
{
    do more work
}

如果线程被阻塞就无法检测中断状态。这是产生 InterruptedException 异常的地方,当在一个被阻塞的线程(调用 sleepwait) 上调用 interrupt 方法时,阻塞调用将会被 InterruptedException 异常中断。没有任何语言方面的需求要求一个被中断的线程应该终止,中断一个线程不过是引起它的注意,被中断的线程可以决定如何响应中断。某些非常重要线程可以处理完异常继续执行,不进行中断。但是更普遍的情况是,线程简单地将中断作为一个终止的请求,这种线程的run方法具有如下形式:

Runnable r = () -> {
    try {
        ...
        while(!Thread.currentThread().isInterrupted() && more work to do)
        {
            do more work
        }
    } catch (InterruptedException e) {
        // thread was interrupted during sleep or wait
    } finally {
        cleanup, if required
    }
    // exiting the run method terminates the thread
}

如果在每次工作迭代之后都调用 sleep 方法(或者其他的可中断方法),islnterrupted 检测既没有必要也没有用处。如果在中断状态被置位时调用 sleep 方法,它不会休眠。相反,它将清除这一状态并抛出 InterruptedException 。因此,如果你的循环调用 sleep ,不会检测中断状态。相反,要如下所示捕获 InterruptedException 异常:

Runnable r = () -> {
    try {
        ...
        while(more work to do)
        {
            do more work
            Thread.sleep(delay);
        }
    } catch (InterruptedException e) {
        // thread was interrupted during sleep
    } finally {
        cleanup, if required
    }
    // exiting the run method terminates the thread
}

【java.lang.Thread】

  1. void interrupt()
    向线程发送中断请求。线程的中断状态将被设置为 true,如果目前该线程被一个 sleep 调用阻塞,那么,InterruptedException 异常被抛出。
  2. static boolean interrupted()
    是一个静态方法,它检测当前的线程是否被中断。而且,调用 interrupted 方法会清除该线程的中断状态;
  3. boolean islnterrupted()
    是一个实例方法,可用来检验是否有线程被中断。调用这个方法不会改变中断状态。

在很多代码中 InterruptedException 直接被忽视了,而没有处理,更好的方式是:

void mySubTask {
    ...
    try { sleep(delay); }
    catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    ...
}

或者,更好的选择是,用 throws InterruptedException 标记你的方法, 不采用 try 语句块捕获异常。于是调用者(或者, 最终的 run 方法)可以捕获这一异常。

void mySubTask throws InterruptedException {
    ...
    sleep(delay);
    ...
}

线程状态

线程可以有如下 6 种状态:

  • New (新创建)
  • Runnable (可运行)
  • Blocked (被阻塞)
  • Waiting (等待)
  • Timed waiting (计时等待)
  • Terminated (被终止)

线程状态转移如图所示:
Java核心技术Ι(1)——并发之线程_第1张图片
上图展示了线程可以具有的状态以及从一个状态到另一个状态可能的转换。当一个线程被阻塞或等待时(或终止时)另一个线程被调度为运行状态。当一个线程被重新激活(例
如, 因为超时期满或成功地获得了一个锁,) 调度器检查它是否具有比当前运行线程更高的优先级,如果是这样,调度器从当前运行线程中挑选一个, 剥夺其运行权,选择一个新的线程运行。

新创建线程

    当用 new 操作符创建一个新线程时,如 new Thread(r), 该线程还没有开始运行。这意味着它的状态是 new。当一个线程处于新创建状态时,程序还没有开始运行线程中的代码。在线程运行之前还有一些基础工作要做。

可运行线程

    一旦调用 start 方法,线程处于 runnable 状态。一个可运行的线程可能正在运行也可能没有运行, 这取决于操作系统给线程提供运行的时间。(Java 的规范说明没有将它作为一个单独状态,一个正在运行中的线程仍然处于可运行状态。)
    一旦一个线程开始运行,它不必始终保持运行。事实上,运行中的线程被中断,目的是为了让其他线程获得运行机会。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行权, 并给另一个线程运行机会。当选择下一个线程时, 操作系统考虑线程的优先级。
注意:在任何给定时刻,二个可运行的线程可能正在运行也可能没有运行

被阻塞线程和等待线程

    当线程处于被阻塞或等待状态时,它暂时不活动,它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它,细节取决于它是怎样达到非活动状态的。

  • 当一个线程试图获取一个内部的对象锁(而不是 java.util.concurrent 库中的锁)而该锁被其他线程持有,则该线程进人阻塞状态,当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
  • 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用Object.wait 方法或 Thread.join 方法, 或者是等待 java.util.concurrent 库中的 LockCondition 时, 就会出现这种情况。实际上,被阻塞状态与等待状态是有很大不同的。
  • 有几个方法有一个超时参数,调用它们导致线程进入计时等待(timed waiting ) 状
    态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有
    Thread.sleepObject.waitThread.joinLock.tryLock 以及 Condition.await 的计时版。

被终止的线程

    线程因如下两个原因之一而被终止:

  • 因为 run 方法正常退出而自然死亡。
  • 因为一个没有捕获的异常终止了 run 方法而意外死亡。
    特别是, 可以调用线程的 stop 方法杀死一个线程。 该方法抛出 ThreadDeath 错误对象,
    由此杀死线程。但是 stop 方法已过时, 不要在自己的代码中调用这个方法

【java.lang.Thread】

  1. void join()
    等待终止指定的线程。
  2. void join(long millis)
    等待指定的线程死亡或者经过指定的毫秒数。
  3. Thread.state.getState()
    得到这一线程的状态:NEW、RUNNABLE、BLOCKED、 WAITING、TIMED_WAITING或 TERMINATED 之一。

线程属性

线程优先级

    在 Java 程序设计语言中,每一个线程有一个优先级。默认情况下, 一个线程继承它的父线程的优先级。可以用 setPriority 方法提高或降低任何一个线程的优先级。可以将优先级设置为在 MIN_PRIORITY (在 Thread 类中定义为 1 ) 与 MAX_PRIORITY (定义为 10 ) 之间的任何值。NORM_PRIORITY 被定义为 5。每当线程调度器有机会选择新线程时, 它首先选择具有较高优先级的线程。但是线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时,Java 线程的优先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。
注意: 如果确实要使用优先级, 应该避免初学者常犯的一个错误。如果有几个高优先级的线程没有进入非活动状态, 低优先级的线程可能永远也不能执行。每当调度器决定运行一个新线程时,首先会在具有高优先级的线程中进行选择, 尽管这样会使低优先级的线程完全饿死。

【java.lang.Thread】

  1. void setPriority(int newPriority)
    设置线程的优先级,优先级必须在Thread.MIN_PRIORITY 与 Thread.MAX_PRIORITY之间。一般使用Thread.NORM_RIORITY 优先级。
  2. static void yield()
    导致当前执行线程处于让步状态。如果有其他的可运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度。注意这是一个静态方法。

守护线程

    可以通过调用 t.setDaemon(true);将线程转换为守护线程(daemon thread) 这样一个线程没有什么神奇。守护线程的唯一用途是为其他线程提供服务。计时线程就是一个例子,它定时地发送“计时器嘀嗒” 信号给其他线程或清空过时的高速缓存项的线程。当只剩下守护线程时, 虚拟机就退出了,由于如果只剩下守护线程, 就没必要继续运行程序了。
    守护线程有时会被初学者错误地使用, 他们不打算考虑关机(shutdown) 动作,但是这是很危险的。守护线程应该永远不去访问固有资源, 如文件、 数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

【java.lang.Thread】
void setDaemon( boolean isDaemon )
标识该线程为守护线程或用户线程。这一方法必须在线程启动之前调用。

未捕获异常处理器

    线程的 run 方法不能抛出任何受查异常, 但是非受査异常会导致线程终止,在这种情况下,线程就死亡了。但是,不需要任何 catch 子句来处理可以被传播的异常。相反,就在线程死亡之前异常被传递到一个用于未捕获异常的处理器。该处理器必须属于一个实现 Thread.UncaughtExceptionHandler 接口的类,这个接口只有—个方法。

void uncaughtException(Thread t, Throwable e);

可以用 setUncaughtExceptionHandler 方法为任何线程安装一个处理器。也可以用 Thread 类的静态方法 setDefaultUncaughtExceptionHandler 为所有线程安装一个默认的处理器。替换处理器可以使用日志 API 发送未捕获异常的报告到日志文件。如果不安装默认的处理器, 默认的处理器为空。但是, 如果不为独立的线程安装处理器,此时的处理器就是该线程的 ThreadGroup 对象。线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组, 但是, 也可能会建立其他的组。现在引入了更好的特性用于线程集合的操作,所以建议不要在自己的程序中使用线程组。ThreadGroup 类实现 Thread.UncaughtExceptionHandler 接口。它的 uncaughtException 方法做如下操作:

  1. 如果该线程组有父线程组, 那么父线程组的 uncaughtException 方法被调用。
  2. 否则, 如果 Thread.getDefaultExceptionHandler 方法返回一个非空的处理器, 则调用该处理器。
  3. 否则,如果 ThrowableThreadDeath 的一个实例, 什么都不做。
  4. 否则,线程的名字以及 Throwable 的栈轨迹被输出到 System.err 上。

这是你在程序中肯定看到过许多次的栈轨迹。

【java.lang.Thread】

  1. static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)
    static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()
    设置或获取未捕获异常的默认处理器
  2. void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)
    Thread.UncaughtExceptionHandler getUncaughtExceptionHandler()
    设置或获取未捕获异常的处理器。如果没有安装处理器,则将线程组对象作为处理器。

【java.lang.Thread.UncaughtExceptionHandler】
void uncaughtException(Thread t, Throwable e)
当一个线程因未捕获的异常而终止,按规定要将客户报告记录到日志中。
参数:
t——由于未捕获异常而终止的线程
e——未捕获的异常对象

【java.lang.ThreadGroup】
void uncaughtException(Thread t, Throwable e)
如果有父线程组,调用父线程组的这一方法;或者,如果Thread类有默认处理器,调用该处理器,否则,输出栈轨迹到标准错误流上(但是,如果e是一个ThreadDeath对象,栈轨迹是被禁用的。不过ThreadDeath对象由stop方法产生,而该方法已经过时)。

参考

《Java核心技术 卷1》

你可能感兴趣的:(多线程,java)