Java漫游笔记-09-多线程

什么是线程

线程,通常被称为轻量级的进程,是操作系统调度的最小单元。
线程是进程中的一个实体,同一个进程中的多个线程共享该进程的资源,因此会相会影响。

创建和启动线程

创建新执行线程有两种方法。

Thread

一种方法是将类声明为 Thread 的子类。并且,该子类应重写 Thread 类的 run() 方法。

1
2
3
4
5
public ThreadClass extends Thread {
public void run(){
// some code
}
}

然后,创建并启动一个线程:

1
2
ThreadClass thread = new ThreadClass();
thread.start();

Runable

另一种方法是声明实现 Runnable 接口的类。然后,该类实现 run() 方法。

1
2
3
4
5
public ThreadClass implements Runnable {
public void run(){
// some code
}
}

接着,通过实例化一个 Thread 对象,并将自身作为运行目标,启动线程:

1
2
3
Runnable r = new ThreadClass();
Thread thread = new Thread(r);
thread.start();

大多数情况下,如果只想重写 run() 方法,而不重写其他 Thread 类的方法,那么应使用 Runnable 接口。这很重要,因为除非程序员打算修改或增强类的基本行为,否则不应为该类创建子类。

实际上,Thread 类也实现了 Runnable 接口:

1
2
3
public class Thread implements Runnable {

}

start() & run()

start() 使线程开始执行(或者说做好随时被执行的准备),其内部调用了一个名为 start0() 的本地方法,这个本地方法会创建了真正的平台相关的本地线程,最终还会通过 Java 虚拟机调用该线程的 run() 方法。

run() 方法和普通方法没有本质区别,直接调用 run() 方法不会报错,但是却是在当前线程执行,而不会创建一个新的线程。

从方法调用的角度的看,一个线程对象只能 start() 一次,但是 run() 可以反复调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public synchronized void start() {

if (threadStatus != 0)
throw new IllegalThreadStateException();

group.add(this);

boolean started = false;
try {
// 调用本地方法启动线程
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */

}
}
}

private native void start0();

public void run() {
if (target != null) {
target.run();
}
}

在 Java 中,每次程序运行至少启动 2 个线程:一个是 main 线程,一个是垃圾回收(GC)线程。

线程状态

这里所说的线程状态都是虚拟机状态,它们并没有反映所有操作系统线程状态。
在给定时间点上,一个线程只能处于一种状态。
线程可以处于下列状态( Thread.State )之一:

1
2
3
4
5
6
7
8
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
  • 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() 方法蓄意谋杀。

线程状态转换

为了更好地理解这些状态,我编了一个“看电影”的例子:

  1. new Thread(r); 相当于你只是有了一个想法,但还没有具体的行动,处于 NEW 状态。
  2. thread.start(); 则相当于你已经订好电影票,来到电影院门口,做好了看电影的准备,处于 RUNNABLE 状态。
  3. 接下来,如果你要去的影厅,由于上一部电影还没放映完,正在被人占用,那么你就不能进去,此时进入 BLOCKED 状态。
  4. 当影厅里面的人看完电影离开,影院的工作人员则会在恰当的时机,安排你进入影厅(相当于 OS 调度)。这时候,你就可以坐在观影的位置上,先休息一下,耐心等待放映,如果有人明确告诉你要等多长时间,进入 TIMED_WAITING 状态,否则进入 WAITING 状态。
  5. 一旦约定等待的时间结束,或者期间有工作人员主动来通知( notify()notifyAll() ),那么,你又可以做好准备,重新进入 RUNNABLE 状态。
  6. 终于,电影顺利开播,如果没什么特殊情况,电影会顺利放完( run() 执行完毕),如果出现异常情况,或者中途被人强制终止播放( ~~stop()~~ ),那么,你都只能拍拍屁股走人了,此时为 TERMINATED 状态。

线程属性

线程名称

创建线程的时候可以指定一个名称(或者通过 setName() 指定 ),如果不指定,则会自动生成一个,自动生成的名称的形式为 “Thread-“ + num ,其中的 num 为整数。

1
2
3
4
5
6
7
8
9
/* For autonumbering anonymous threads. */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}

public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}

线程优先级

首先,强烈建议,我们的代码不要依赖于优先级,因为它不可靠
Java 虚拟机基于兼容性考虑,把线程优先级分为 1-10 级(级别越高,则优先级越高)。但是,有的操作系统,对线程优先级的划分并没有 10 级,例如,只有 5 级,那么,很有可能,你设定的 1 和 2 可能都是对应着 1 级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
+ The minimum priority that a thread can have.
*/

public final static int MIN_PRIORITY = 1;

/**
+ The default priority that is assigned to a thread.
*/

public final static int NORM_PRIORITY = 5;

/**
+ The maximum priority that a thread can have.
*/

public final static int MAX_PRIORITY = 10;

守护线程

可以通过:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public final synchronized void join(long millis)
throws InterruptedException {

long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
// wait() 方法会使当前线程立即释放它在该对象上的锁
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

同步

多线程可以充分利用多核处理器的资源,但是,这又带来了另外一个问题:多个线程对同一个资源的竞争问题。
为了解决这个问题 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 包的 LockCondition ,以减少出错。