Java并发 - 基本元素 Thread 和 Runnable

2020/03/22 concurrent 共 3699 字,约 11 分钟
Bob.Zhu

基本并发类

Thread 类

该类描述了执行并发 Java 应用程序的所有线程。

Runnable 接口

这是 Java 中创建并发应用程序的另一种方式。

ThreadLocal 类

该类用于存放从属于某一线程的变量。

ThreadFactory 接口

这是实现 Factory 设计模式的基类,你可以用它来创建定制线程。

创建线程

在Java平台中,一个线程就是一一个对象,对象的创建离不开内存空间的分配。创建 一个线程与创建其他类型的Java对象所不同的是,Java虛拟机会为每个线程分配调用栈 ( Call Stack)所需的内存空间。调用栈用于跟踪Java代码(方法)间的调用关系以及Java 代码对本地代码( Native Code,通常是C代码)的调用。另外,Java 平台中的每个线程 可能还有一个内核线程(具体与Java虚拟机的实现有关)与之对应。因此相对来说,创 建线程对象比创建其他类型的对象的成本要高–些。

两种创建方法

Java 使用 Thread 类实现执行线程。你可以使用以下机制在应用程序中创建执行线程:

  • 扩展 Thread 类并重载 run()方法(基于继承 Inheritance 的技术)
  • 实现 Runnable 接口,并将该类的对象传递给 Thread 对象的构造函数(基于组合 Composition 的技术)

一旦有了 Thread 对象,就必须使用 start()方法创建新的执行线程并且执行 Thread 类的 run() 方法。 如果直接调用 run()方法,那么你将调用常规 Java 方法而不会创建新的执行线程。

这两种情况下你都会得到一个 Thread 对象,但是相对于第一种方式来说,更推荐使用第二种,其主要优势如下:

  • Runnable 是一个接口: 你可以实现其他接口并扩展其他类;对于采用 Thread 类的方式,你只能扩展这一个类。
  • 可以通过线程来执行 Runnable 对象,但也可以通过其他类似执行器的 Java 并发对象来执行。这样可以更灵活地更改并发应用程序。
  • 可以通过不同线程使用同一 Runnable 对象。

启动线程

运行一个线程实际上就是让Java虚拟机执行该线程的run方法,从而使相应线程的任 务处理逻辑代码得以执行。为此,我们首先要启动线程。Thread 类的start 方法的作用是启 动相应的线程。启动一个线程的实质是请求Java虚拟机运行相应的线程,而这个线程具 体何时能够运行是由线程调度器(Scheduler)决定的。因此,start方法调用结束并不意 味着相应线程已经开始运行,这个线程可能稍后才被运行,甚至也可能永远不会被运行。

线程属于”一次性用品”,我们不能通过重新调用一个已经运行结束的线程的start方 法来使其重新运行。事实上,start 方法也只能够被调用一次,多次调用同一个Thread实 例的start 方法会导致其抛出IllegalThreadStateException异常。

run 和 start 方法

参考:简书 snoweek - java-线程中start和run的区别

调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

start()

它的作用是启动一个新线程。 通过start()方法来启动的新线程,处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始 调用相应线程的run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run方法 运行结束,此线程随即终止。start()不能被重复调用。用start方法来启动线程,真正实现了多线程运行, 即无需等待某个线程的run方法体代码执行完毕就直接继续执行下面的代码。这里无需等待run方法执行完毕, 即可继续执行下面的代码,即进行了线程切换。

run()

run()就和普通的成员方法一样,可以被重复调用。 如果直接调用run方法,并不会启动新线程,而是在本线程内调用该Runnable对象的run()方法,可以 重复多次调用!程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是 要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。

线程的状态和生命周期

在给定时间内,线程只能处于一个状态。这些状态不能映射到操作系统的线程状态,它们是 JVM 使用的 状态。线程的可能状态如下:

线程状态

NEW

一个Thread 对象已经创建,但是还没有开始执行的线程处于该状态。由于一个线程实例只能够被启动一次, 因此一个线程只可能有一次处于该状态。

RUNNABLE

RUNNABLE 表示 Thread 对象正在 Java 虚拟机中运行。 该状态可以被看成一个复合状态。它包括两个子状态:READYRUNNING。前者表示处于该状态的 线程可以被线程调度器( Scheduler)进行调度而使之处于 RUNNING 状态。后者表示处于该状态的线程 正在运行,即相应线程对象的run方法所对应的指令正在由处理器执行。执行Thread.yield()的线程,其 状态可能会由 RUNNING 转换为 READY。处于READY子状态的线程也被称为活跃线程。

BLOCKED

一个线程发起一个阻塞式I/O ( Blocking I/O )操作后,或者申请一个由其他线程持有的独占资源(比如 锁)时,相应的线程会处于该状态。处于 BLOCKED 状态的线程并不会占用处理器资源。当阻塞式I/O操作 完成后,或者线程获得了其申请的资源,该线程的状态又可以转换为RUNNABLE。

WAITING

Thread 对象正在等待另一个线程的动作。 一个线程执行了某些特定方法之后就会处于这种等待其他线程执行另外一些特定操作的状态。 能够使其执行线程变更为WAITING状态的方法包括:Object.wait()、Thread.join() 和 LockSupport.park(Object)。 能够使相应线程从WAITING变更为RUNNABLE 的相应方法包括: Object.notify()/notifyAll() 和 LockSupport.unpark(Object)。

TIMED_WAITING

该状态和WAITING类似,差别在于处于该状态的线程并非无限制地等待其他线程执行特定操作,而是处于 带有时间限制的等待状态。当其他线程没有在指定时间内执行该线程所期望的特定操作时,该线程的状态 自动转换为RUNNABLE。

TERMINATED

已经执行结束的线程处于该状态。由于一个线程实例只能够被启动一次,因此一个线程也只可能有一次处于 该状态。Thread.run()正常返回或者由于抛出异常而提前终止都会导致相应线程处于该状态。

线程属性

线程的属性除了编号外.其他属性都是可读写的属性,即Thread类提供了相应的 get 方法和 set 方法 用于读取或者设置相应的属性。例如,getName方法可返回线程的名称属性值而setName方法则可以设置 线程的名称属性值。

线程编号ID

类型: long。 用于标识不同的线程,不同的线程拥有不同的编号。

线程名称 Name

类型: String。 面向人(而非机器)的一个属性,用于区分不同的线程。默认值与线程的编号有关, 默认值的格式为:”Thread-线程编号” 如 “Thread-0”。

Java并不禁止我们将不同的线程的名称属性设置为相同的值。尽管如此,设置线程的名称属性有助于 代码调试和问题定位。

线程类别 Daemon

类型: boolean。 值为true表示相应的线程为守护线程,否则表示相应的线程为用户线程。该属 性的默认值与相应线程的父线程的该属性的值相同。

该属性必须在相应线程启动之前设置,即对setDaemon方法的调用必须在对start 方法的调用之前, 否则setDaemon方法会抛出 llegalThreadStateException 异常。负责一些关键任务处理的线程 不适宜设置为守护线程。

在 Java 中,可以创建两种线程,二者之间的区别在于它们如何影响程序的结束。当有下列情形之一时, Java 程序将结束其执行过程:

  • 程序执行 Runtime 类的 exit()方法,而且用户有权执行该方法
  • 应用程序的所有非守护线程均已结束执行,无论是否有正在运行的守护线程

具有这些特征的守护线程通常用在作为垃圾收集器或缓存管理器的应用程序中,执行辅助任务。 你可以 使用 isDaemon()方法检查线程是否为守护线程,也可以使用 setDaemon()方法将某个线程确立为守护 线程。要注意,必须在线程使用 start()方法开始执行之前调用此方法。

线程优先级

类型: int。该属性本质上是给线程调度器的提示,用于表示应用程序希望哪个线程能够优先得以运行。 Java 定义了 1~10 的 10 个优先级。默认值一般为5(表示普通优先级)。对于具体的一个线程而言, 其优先级的默认值与其父线程(创建该线程的线程)的优先级值相等。

线程和代码的关系

  • 任何一段代码都是运行在确定的线程之中
  • 同一段代码可以被多个线程执行
  • 代码可以通过调用Thread.currentThread()来获取其当前执行线程

参考资料

文档信息

Search

    Table of Contents