什么是线程
线程,通常被称为轻量级的进程,是操作系统调度的最小单元。
线程是进程中的一个实体,同一个进程中的多个线程共享该进程的资源,因此会相会影响。
创建和启动线程
创建新执行线程有两种方法。
Thread
一种方法是将类声明为 Thread
的子类。并且,该子类应重写 Thread
类的 run()
方法。
1 | public ThreadClass extends Thread { |
然后,创建并启动一个线程:
1 | ThreadClass thread = new ThreadClass(); |
Runable
另一种方法是声明实现 Runnable
接口的类。然后,该类实现 run()
方法。
1 | public ThreadClass implements Runnable { |
接着,通过实例化一个 Thread
对象,并将自身作为运行目标,启动线程:
1 | Runnable r = new ThreadClass(); |
大多数情况下,如果只想重写
run()
方法,而不重写其他Thread
类的方法,那么应使用Runnable
接口。这很重要,因为除非程序员打算修改或增强类的基本行为,否则不应为该类创建子类。
实际上,Thread
类也实现了 Runnable
接口:
1 | public class Thread implements Runnable { |
start() & run()
start()
使线程开始执行(或者说做好随时被执行的准备),其内部调用了一个名为 start0()
的本地方法,这个本地方法会创建了真正的平台相关的本地线程,最终还会通过 Java 虚拟机调用该线程的 run()
方法。
run()
方法和普通方法没有本质区别,直接调用 run()
方法不会报错,但是却是在当前线程执行,而不会创建一个新的线程。
从方法调用的角度的看,一个线程对象只能 start()
一次,但是 run()
可以反复调用。
1 | public synchronized void start() { |
在 Java 中,每次程序运行至少启动 2 个线程:一个是 main 线程,一个是垃圾回收(GC)线程。
线程状态
这里所说的线程状态都是虚拟机状态,它们并没有反映所有操作系统线程状态。
在给定时间点上,一个线程只能处于一种状态。
线程可以处于下列状态( Thread.State
)之一:
1 | public enum State { |
- NEW(新建)
只是创建了一个Thread
的实例,尚未启动。new Thread(r);
之后进入 NEW 状态。 - RUNNABLE(可运行)
可能正在运行,也可能没有运行。
正在运行的线程肯定是 RUNNABLE 的。
正在等待操作系统资源(比如处理器)的线程,也有可能处于 RUNNABLE 状态。
新建的线程,执行start()
方法之后,进入 RUNNABLE 状态。 - BLOCKED(被阻塞)
受阻塞并且正在等待监视器锁。
当一个线程试图获取其内部的对象锁,但是该锁被其他线程所持有,则进入 BLOCKED 状态。 - WAITING(等待)
无限期地等待另一线程通知调度器。
调用不带超时值的方法,会导致线程处于等待状态,例如:Object.wait()
Thread.join()
LockSupport.park()
- TIMED_WAITING(计时等待)
方法中有超时参数,进入定时等待,直到计时期满。
调用带有超时值的方法,会导致线程处于定时等待状态,例如:Thread.sleep(t)
Object.wait(t)
Thread.join(t)
LockSupport.parkNanos(t)
LockSupport.parkUntil(t)
- TERMINATED(被终止)
有几种情况:run()
方法执行完毕,寿终正寝。run()
方法执行过程中,产生未捕获的异常,意外死亡。- 被
方法蓄意谋杀。stop()
为了更好地理解这些状态,我编了一个“看电影”的例子:
new Thread(r);
相当于你只是有了一个想法,但还没有具体的行动,处于 NEW 状态。thread.start();
则相当于你已经订好电影票,来到电影院门口,做好了看电影的准备,处于 RUNNABLE 状态。- 接下来,如果你要去的影厅,由于上一部电影还没放映完,正在被人占用,那么你就不能进去,此时进入 BLOCKED 状态。
- 当影厅里面的人看完电影离开,影院的工作人员则会在恰当的时机,安排你进入影厅(相当于 OS 调度)。这时候,你就可以坐在观影的位置上,先休息一下,耐心等待放映,如果有人明确告诉你要等多长时间,进入 TIMED_WAITING 状态,否则进入 WAITING 状态。
- 一旦约定等待的时间结束,或者期间有工作人员主动来通知(
notify()
或notifyAll()
),那么,你又可以做好准备,重新进入 RUNNABLE 状态。 - 终于,电影顺利开播,如果没什么特殊情况,电影会顺利放完(
run()
执行完毕),如果出现异常情况,或者中途被人强制终止播放(~~stop()~~
),那么,你都只能拍拍屁股走人了,此时为 TERMINATED 状态。
线程属性
线程名称
创建线程的时候可以指定一个名称(或者通过 setName()
指定 ),如果不指定,则会自动生成一个,自动生成的名称的形式为 “Thread-“ + num ,其中的 num 为整数。
1 | /* For autonumbering anonymous threads. */ |
线程优先级
首先,强烈建议,我们的代码不要依赖于优先级,因为它不可靠。
Java 虚拟机基于兼容性考虑,把线程优先级分为 1-10 级(级别越高,则优先级越高)。但是,有的操作系统,对线程优先级的划分并没有 10 级,例如,只有 5 级,那么,很有可能,你设定的 1 和 2 可能都是对应着 1 级。
1 | /** |
守护线程
可以通过:
1 | t.setDaemon(true); |
将线程标记为守护线程(Daemon Thread)。
不要使用守护线程去访问资源(例如:文件、数据库),因为它随时可能被中断,它只是为其它线程提供服务。
如果只剩下守护线程,虚拟机就退出了(这是一个悲伤的故事:谁来守护守护线程呢:-P)。
线程组
线程组是一个可以统一管理的线程集合。
默认所有线程属于相同的线程组,一般不建议自己单独维护线程组。
常用方法
Thread.yield()
静态方法。暂停当前正在执行的线程对象,并执行其他线程。yield()
一般被称为:“让步” ,也就是使当前运行的线程让出 CPU 资源,回到 RUNNABLE 状态。
需要注意的是:仅仅是做出让步,但是具体让给谁是不确定的,甚至有可能被再次选中执行。
Thread.sleep()
静态方法。在指定的时间内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。sleep()
一般被称为:“休眠” ,也会使当前运行的线程让出 CPU 资源,以便其他线程得以执行,当休眠计时结束后,线程会苏醒,进入 RUNNABLE 状态。
join()
join()
一般被称为:“合并” ,在 API 中描述为:等待该线程终止。不是很好理解。
举个例子:
如果在线程 A 中调用线程 B 的 join()
方法,那么,线程 A 会等到线程 B 执行完毕后,才会继续执行。
看上去好像 B 成为了 A 的一部分。
线程的合并,其本质是通过 Object
类 的 wait()
方法实现的。
1 | public final synchronized void join(long millis) |
同步
多线程可以充分利用多核处理器的资源,但是,这又带来了另外一个问题:多个线程对同一个资源的竞争问题。
为了解决这个问题 Java 引入了同步机制。
使用方法很简单,就是使用 synchronized
关键字,它会给方法或者是变量自动提供一个 锁 。
Java 中每个对象都有且仅有一个内置锁。
当程序运行到 synchronized
同步方法或代码块时(只能同步方法或代码块,不能同步变量和类),该对象锁才起作用。
如果一个线程获得该锁,就没有其他线程可以获得锁,也就是说任何其他线程都不能进入该对象上的 synchronized
方法或代码块,直到该锁被释放。
持锁的线程退出了 synchronized
同步方法或代码块,该对象的锁就被释放了。
线程休眠时,它所持的锁不会释放。
对于静态同步方法,锁是针对这个类的,锁对象是该类的 Class
对象。
静态和非静态方法的锁互不干预。
Object
类提供了几个同步相关的方法:wait()
、notify()
、notifyAll()
,必须在同步方法或代码块中调用它们。
当在对象上调用 wait()
方法时,执行该代码的线程会立即释放它在对象上的锁。notifyAll()
方法,只是起到一个通知的作用,不释放锁,也不获取锁。相当于告诉该对象上等待的所有线程:“都醒醒,准备去搬砖啦”。
还需要注意的是:不要在同步方法或代码块中调用 sleep()
或者 yield()
方法。
因为休眠和让步操作都不会释放对象锁,这样故作姿态,只会影响效率。
volatile
我们已经知道:如果一个变量可能会被多个线程访问,那么就必须要考虑同步的问题。
但是,有时候,如果只为了一两个变量的同步就使用 synchronized
,在性能上,又显得不划算。
如果有更轻量级的解决方案就好了?而 volatile
就是我们想要的。volatile
为实例变量的同步访问提供了一种免锁机制,它不会引起线程上下文的切换和调度,因此执行成本更低。
简单来说,就是:如果一个实例变量声明为 volatile
,那么,虚拟机就会知道这个变量可能会被多个线程并发访问,它应该对所有线程都是可见的。
并且,当一个线程更新了这个变量的值,虚拟机立刻就会把最新的值刷新到主存中,以便其它线程能够读取到最新的值。
java.util.concurrent 包
JDK 1.5 新增了 java.util.concurrent
包,它的引入简化了并发编程的工作。
一般情况下,应优先使用 java.util.concurrent
包提供的工具类, synchronized
次之,前者都不能满足时,才考虑使用 java.util.concurrent.locks
包的 Lock
和 Condition
,以减少出错。