前言
线程对于每个学习Android的人,都很熟悉,可以说是日常任务。但是真的有那么熟悉吗?比如说线程有哪些状态,怎样控制状态的跳转,恐怕很多人还不太清晰。本文就和大家一块学习Android源码中的Thread类(api26)。
开始
线程是一个程序的执行单元,JVM允许应用并发执行多个线程。
关于并发(Concurrency)和并行(Parallelism)网上有这样的区别:
- 并发:在一时间段内执行多个任务
- 并行:同一时刻执行多个任务
假如有A和B两个任务,则:
并发:ABABAB...
并行:AAAAAA...
BBBBBB...
每个线程有一个优先级,高优先级优先执行。每个线程都可以标记为daemon线程。在一个线程中创建另一个线程时,新线程和原线程享有相同的优先级和daemon标记。
JVM启动后,通常只有一个非daemon线程,即执行main方法所在的线程。JVM在以下任意条件发生时将不再执行线程:
- Runtime.exit()方法在合法的情况下被执行。
- 所有非daemon线程死亡。线程死亡可以是run方法执行完毕,或者抛出异常引起。
创建线程有两种方法
- 继承Thread类,覆写run方法。如下:
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();
需要注意的是,启动线程必须执行start方法,直接执行run方法并不会启动新的线程!!
- 实现Runnable接口,然后作为参数传递给Thread类执行,如下:
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();
每个线程有一个名称方便识别,多个线程可能有相同的名字。新创建的线程如果没有指定名称,会自动生成一个。
线程状态
线程状态指的是线程在JVM中的状态,每个时刻仅对应以下状态中的一个。
- NEW——新建状态。线程未start之前处于该状态。
- RUNNABLE——可运行状态。此时线程可以被JVM执行,但可能在等待操作系统的资源,比如处理器。
- BLOCKED——阻塞状态。此时线程等待进入synchronized代码块/方法,或者调用
Object.wait()
方法后,需要重入synchronized代码块/方法。 - WAITING——等待状态。以下三种方式可以触发进入该状态:
- 调用
Object.wait()
无参方法 - 调用
Thread.join()
无参方法 - 调用
LockSupoort.park()
方法
处于等待状态的线程会等待其他线程执行特定的操作。比如,调用Object.wait()
方法的线程会等待其他线程调用Object#notify()
或者Object.notifyAll()
方法,调用Thread.join()
的线程会等待其他线程终结。
- TIMED_WAITING——和WAITING状态的区别是会有一个等待时间。调用以下方法触发进入该状态:
Thread.sleep
Object.wait(long)
Thread.join(long)
LockSupport.parkNanos
LockSupport.parkUntil
- TERMINATED——死亡状态。线程执行完毕。
以上为源码中线程的六种状态。而通常我们所说的线程状态有五种:新建、就绪、运行、阻塞、死亡。对应关系为:
- 新建——NEW
- 就绪、运行——RUNNABLE
- 阻塞——BLOCKED、WAITING、TIMED_WAITING
- 死亡——TERMINATED
源码中之所以没有区分就绪和运行状态,我个人的观点是CPU执行任务并不是连续的,在上面讲并发的时候提到,A、B两个任务是ABABAB这样交替执行的,所以对于A或者B来说会在就绪和运行之间反复切换,不好区分,所以统称为RUNNABLE。(如有误,欢迎指正)
线程的方法
public synchronized void start()
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
// Android-changed: throw if 'started' is true
if (threadStatus != 0 || started)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
started = false;
try {
nativeCreate(this, stackSize, daemon);
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 */
}
}
}
start之前,线程处于NEW状态,start之后就处于RUNNABLE状态。
start方法使JVM开始调用线程的run方法。需要注意的是,start只能调用一次。再次调用会抛出IllegalThreadStateException
异常。
public void interrupt()
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
nativeInterrupt();
b.interrupt(this);
return;
}
}
nativeInterrupt();
}
中断该线程。如果调用该方法所在的线程不是所要中断的线程,会通过checkAccess()
方法判断是否有权限,可能抛出SecurityException
异常。
- 如果所要中断的线程通过调用
Object.wait
、Thread.join
、Thread.sleep
被阻塞,interrupt方法将会清除中断状态,并抛出InterruptedException
。 - 如果所要中断的线程通过
java.nio.channels.InterruptibleChannel
被阻塞在一个I/O操作中,那么这个channel会被关闭,这个线程会被设置中断状态,并且收到java.nio.channels.ClosedByInterruptException
异常。 - 如果所要中断的线程在
java.nio.channels.Selector
中被阻塞,那么这个线程会被设置中断状态,并且从selection操作中立刻返回,可能是一个0值,就像调用了java.nio.channels.Selector#wakeup
方法。
如果以上场景都不是,那么这个线程将会被设置中断状态。
public static native boolean interrupted()
判断该线程是否处于中断状态。该方法会在调用后清除中断状态,所以如果连续调用两次,第二次必定返回false,除非两次调用之间被再次中断。
public native boolean isInterrupted()
判断该线程是否处于中断状态。不会影响中断状态。
public final boolean isAlive()
判断线程是否存活。线程存活是指线程被start,并且没有死亡。即除NEW和TERMINATED外,其他状态均为存活。
public final void join() throws InterruptedException
public final void join(long millis) throws InterruptedException {
synchronized(lock) {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
lock.wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
lock.wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
}
如A、B两个线程,在A的run方法中调用B.join(),则A进入阻塞状态(lock.wait
),直到B死亡(!isAlive()
),A才会重新进入RUNNABLE状态。
public static native void yield()
该方法会使调用线程由运行状态进入就绪状态,即暂时让出CPU资源给其他任务执行。
该方法告知调度程序当前线程可以让出处理器资源,调度程序可以自由选择是否忽略这个通知。
public static void sleep(long millis) throws InterruptedException
使当前线程暂停一段时间,不会释放所拥有的资源,如对象锁等。线程由运行态进入阻塞态,暂停时间到,重新进入就绪态。
UncaughtExceptionHandler接口
可以通过public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh)
为线程设置一个UncaughtExceptionHandler
,在线程因未捕获的异常导致死亡时,JVM会调用该接口的void uncaughtException(Thread t, Throwable e)
方法。
在Android应用开发过程中,如果需要自定义异常处理,可以通过非静态方法setUncaughtExceptionHandler()
设置单个线程的UncaughtExceptionHandler
(记为A),或者通过静态方法setDefaultUncaughtExceptionHandler()
设置所有线程的默认UncaughtExceptionHandler
(记为B)。只有在A不存在时,才会调用B。
Thread类有个与ThreadLocal相关的成员变量ThreadLocal.ThreadLocalMap threadLocals = null
。关于ThreadLocal的知识可以参考深入学习理解java-ThreadLocal。
这里总结一下:
- ThreadLocal不是为了解决多线程同步的问题,而是为了记录各个线程的状态。比如可以用ThreadLocal记录线程的id。
- 每个Thread都维护了一个Map,ThreadLocal是这个Map的Key。ThreadLocal的
get set
方法本质是在操作当前线程的Map。
下面的例子是源码中的。
ThreadId
类为每个线程提供一个唯一的ID值。不同的线程调用get()
方法返回值不同,同一个线程调用get()
方法始终返回同一个值。
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal threadId =
new ThreadLocal() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}