上篇对什么是线程做了简单的介绍,本篇主要是从构造线程、中断线程、终止线程3个方面了解线程。

1. 构造线程

1.1 构造线程的方式

Java构造一个线程有两种方式:

一种是声明子类继承Thread父类并重写父类的run方法。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 线程类
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}

public void run() {
// compute primes larger than minPrime
}
}

//启动线程实例
PrimeThread p = new PrimeThread(143);
p.start();

另一种方式是声明子类实现Runnable接口,并实现run方法。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 线程类
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}

public void run() {
// compute primes larger than minPrime
}
}
//启动线程实例
PrimeRun p = new PrimeRun(143);
new Thread(p).start();

两者其实在本质上是一致的,因为Thread类也继承了Runable接口,所以都实现/重写Runable中的run方法。Java将线程的执行和执行对象抽象开来,JDK中执行的是Thread类,Executor框架,可执行目标有Runable,Callable。对于构造线程的方式第一种方式并不推荐,因为继承Thread类限定了其基本行为,在设计上违反多用组合 少用继承的原则,所以一般构造线程使用第二种方式,实现Runable接口。

1.2 构造线程的属性

构造线程时需要提供线程所需要的属性。一个新的(child)线程对象是由parent线程进行空间分配的,而child线程继承了parent是否为Daemon、优先级、和加载资源的contextClassLoader以及可继承的ThreadLocal等属性,同时分配唯一的ID来表示这个child线程。如果创建线程实例后需要修改线程属性,则可以通过Thread提供的一些修改属性的方法进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
setPriority() // 优先级
getPriority()
setName()。// 线程名
getName()
setDaemon() // 守护线程
isDaemon()
getContextClassLoader()。// 资源加载
setContextClassLoader()
getStackTrace()
getAllStackTraces()
checkAccess()
isCCLOverridden()
auditSubclass()
dumpThreads()
getThreads()
getId()
getState()
setDefaultUncaughtExceptionHandler()
getDefaultUncaughtExceptionHandler()
getUncaughtExceptionHandler()
setUncaughtExceptionHandler()
dispatchUncaughtException()
processQueue()

当然构造线程时有些属性还可以在创建线程实例时就设置,如线程组,栈大小,栈名称,权限控制上下文,可继承的ThreadLocal,下面是JDK初始化一个线程实例时的代码,摘至JDK8

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
*
* @param g
* @param target
* @param name
* @param stackSize 线程的栈大小 根据参数传递过程可以看出默认大小为零,即使用默认的线程栈大小
* @param acc 访问控制权限
* @param inheritThreadLocals // 可继承的ThreadLocal
*
* ThreadGroup 线程组(ThreadGroup)就是由线程组成的管理线程的类,
* 这个类是java.lang.ThreadGroup类。
* 定义一个线程组,通过以下代码可以实现。
* ThreadGroup group=new ThreadGroup("group");
* Thread thread=new Thread(group,"the first thread of group");
* ThreadGroup类中的某些方法,可以对线程组中的线程产生作用。例如,setMaxPriority()方法可以设定线程组中的所有线程拥有最大的优先权。
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {// 线程名,可相同
throw new NullPointerException("name cannot be null");
}

this.name = name;

Thread parent = currentThread();// 获取当前线程,并作为父线程。
SecurityManager security = System.getSecurityManager();// 获取安全策略
if (g == null) {
/* Determine if it's an applet or not */

/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {//如果存在安全管理器,则获取它重写的线程组
g = security.getThreadGroup();
}

/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();//使用父线程组
}
}

/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();

/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}

g.addUnstarted();

this.group = g;
this.daemon = parent.isDaemon();//继承父线程的守护属性
this.priority = parent.getPriority();//继承父线程的优先级
//继承父线程加载资源的contextClassLoader
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
// 继承父线程可继承的ThreadLocal
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;

/* Set thread ID */
tid = nextThreadID();
}

1.3 启动线程

线程对象在初始化完成后,调用start方法就可以启动线程。
注意:启动一个线程,最好为这个线程设置线程名称,这行有助于分析或者排查问题

2. 中断线程

2.1 中断的原理

中断,可以理解为运行中的线程,是否被其他线程进行了中断操作。中断操作包含三个方法。interrupt(),interrupted(),isInterrupted()。下面我们将对这个方法进行详细了解。

2.1.1 interrupt()

中断操作通过调用线程的interrupt()方法进行。例如线程A中断线程B,在线程A的代码中调用ThreadB.interrupt() 即可。

interrupt()方法不是直接将线程终止,而是针对于不同情况进行不同处理。

  1. 除非终止的是当前线程(始终被允许),否则调用checkAccess方法,可能会导致抛出SecurityException异常。
  2. 如果调用Object类的wait(),wait(long)或者wait(long, int)等方法,或者调用此线程的join(),join(long),join(long, int), sleep(long), sleep(long, int)方法,此线程的中断状态将被清除,同时将抛出InterruptedException异常
  3. 如果此线程在可中断通道的IO操作上,通道将被关闭,线程被设置中断状态,同时将抛出ClosedByInterruptException异常。
  4. 如果线程在选择器(Selector)阻塞,线程将被设置中断状态,立即从选择操作中返回,可能带有非零值,就像选择器唤醒方法被调用一样。
  5. 如果以上的条件都不满足,此线程将被设置中断状态。
  6. 中断不存活的线程不会有任何影响。

翻译自Java se7docs

注:Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
注意:
对于会抛出异常的情况,异常一定要处理,一般子线程异常不能抛出非运行时异常,所以子线程我们需要抛出运行时异常,用于给父线程捕获。或者在catch块中使用 Thread.currentThread().interrupt() 重新抛出中断来保证调用栈的高层的代码知道当前线程的中断。

2.1.2 interrupted()

interrupted()主要有以下两个功能:

  1. 测试当前线程是否被中断,清除线程中断状态。换句话说就是,如果当前方法被成功调用两次,则返回false(除非当前线程在第一次调用此方法后第二次调用此方法前被再次中断)。
  2. 线程中断被忽略,在中断时线程处于不活动的状态将被此方法反映返回false。

interrupted()不同于interrupt(),此方法用于测试当前线程是否被中断,并清除中断状态。代码示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
Thread thread = new InterruptThread();
thread.start();
SleepUtils.sleep(1);
thread.interrupt();
}
static class InterruptThread extends Thread{
@Override
public void run() {
int count = 0;
while(true){
count++;
if(Thread.currentThread().interrupted()){//此处仅仅是为了示例
break;
}
}
}
}

2.1.3 isInterrupted()
  1. isInterrupted虽然也是测试线程是否被中断,但是此方法不会更改线程的中断状态。
  2. 线程中断被忽略,在中断时线程处于不活动的状态将被此方法反映返回false。

3. 终止线程

3.1 过期的suspend()、resume()和stop()

在Java API中,suspend()、resume()和stop()三个方法是过期的,不建议使用的。
主要原因是:suspend()在调用后,线程不是放已经占有的资源比如说锁,而是占有着资源进入睡眠,这样容易引发死锁状态。stop()方法在终结一个线程时不会保证线程资源的释放,因此导致线程处于不确定的状态下。

sleep和suspend区别:
相同点:sleep和suspend都会持有占有的资源不释放。
不同点:sleep阻塞(TIMED_WAITING)后,经过一段时间自行恢复运行。而suspend必须使用resume()显示的恢复,如果不使用resume()或者resume()失败,很容易引起资源占用导致的死锁。

3.2 安全的终止线程

在第2小节提到的中断是线程的一个标示位,中断操作是一种简便的线程间交互方式,这种交互方式适合用来取消和停止任务。除了中断,还可以使用同步变量来控制是否停止并终止该线程。
线程的终止不是直接强制线程停止,而是引导线程运行结束。

引用

Java 安全模型介绍
Class Thread
Java线程(1)-Thread类源码
Java并发编程艺术