1、概述
1.1 进程与线程
进程是程序运行时,操作系统进行资源分配的最小单位,包括 CPU、内存空间、磁盘 IO 等。从另一个角度讲,进程是程序在设备(计算机、手机等)上的一次执行活动,或者说是正在运行中的程序。
一个程序进入内存运行时,它就变成一个进程。进程是处于运行中的程序,它拥有自己独立的资源和地址空间,在没有经过进程本身允许的情况下,进程不可以直接访问其他进程的地址空间。同时多个进程可以在单个处理器上并发执行,多个进程之间互不影响。
线程是进程的一个实体,是 CPU 调度的最小单位。自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
使用多线程解决了多任务同时运行的问题,并且,共享变量使得线程间的通信要比进程间通信更有效、更容易。此外,在一些操作系统中,与进程相比,线程更“轻量级”,创建、撤销一个线程比启动新进程的开销要小得多。
当然,线程太多,来回切换也会导致执行效率降低。
其实应用程序的执行都是 CPU 在做着快速的切换完成的,这个切换是随机的。在某一个时刻,CPU 只在执行一个线程,但是由于它的执行速度非常快,在毫秒级别,因此人无法感知到它在一个时间片中其实只在执行一个任务,在时间片结束后又去切换执行另一个任务。
1.2 并行与并发
并发是指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果;而并行是指在同一时刻,有多条指令在多个处理器上同时执行。
1.3 JVM 中的多线程解析
JVM 启动时就启动了多个线程,至少有两个线程可以分析的出来:
- 执行 main 方法的线程,即主线程。该线程的任务代码都定义在 main 方法中。主线程执行完任务,其所在进程也就关闭了。
- 负责垃圾回收的线程。系统会自己决定何时回收垃圾,你也可以通过 System.gc() 通知垃圾回收器来回收。但是这个也不是立即回收垃圾,也是在调用之后的一定时间内。另外 Object 中还有一个 finalize() 方法进行垃圾回收。
实际上,JVM 启动的完整线程有以下这些:
- main:main线程,用户程序入口
- Reference Handler:清除Reference的线程
- Finalizer:调用对象finalize方法的线程
- Signal Dispatcher:分发处理发送给JVM信号的线程
- Attach Listener:内存 dump,线程 dump,类信息统计,获取系统属性等
- Monitor Ctrl-Break:监控 Ctrl-Break 中断信号的
此外,多线程运行时的示意图如下:
说明:
- 当前有3个线程:main 线程、Thread-1、Thread-2,它们每个都维护了自己的方法栈(run 方法在栈底)。在哪个线程调用了方法,这个方法就会进入哪个线程。
- 如果 main 线程先于另外两个线程执行完,JVM 不会结束,而是等所有线程都运行完。
- 在3个线程都运行的前提下,如果在某个线程中发生了异常导致该线程停止,不会影响其它线程,其它线程该怎么执行就怎么执行
2、使用
2.1 创建线程
创建线程的目的是为了开启一条执行路径,去运行指定的代码(即该线程的任务)和其他代码实现同时运行。
创建线程的方法有两种:继承 Thread 类和实现 Runnable 接口,实现 Callable 接口严格讲是属于第二种方式,不能单独作为一种方法。
我们先来了解下 Callable 的基本用法再解释上述观点的原因。
Callable 的用法
Callable 是一个函数式接口,有泛型限制,该泛型参数类型与作为线程执行体的 call() 返回值类型相同。call() 比 Runnable 的 run() 功能要更强大,因为它可以有返回值,并且可以声明抛出异常。
虽然 call() 也是线程执行体,但是由于 Callable 接口不是 Runnable 的子接口,所以 Callable 并不能直接作为 Thread 的 target,而是要借助 Future 接口。
Future 接口代表 call() 的返回值,而且 Future 的实现类 FutureTask 也实现了 Runnable 接口,可以作为 Thread 的 target。Future 中定义了如下的公共方法控制与它关联的 Callable 任务:
创建并启动有返回值的线程的步骤:
示例如下:
// Callable 后的参数类型为 String,意味着 call() 的返回值类型为 String
public class CallableTest implements Callable<String> {
public static void main(String[] args) {
new CallableTest().test();
}
/**
* Callable 的 call() 类似于 Runnable 的 run(),只不过前者有返回值而前者没有。
*
* 此外,Future 接口可以控制 Runnable/Callable 取消执行任务、查询任务是否完成、
* 获取任务执行结果(通过阻塞方法 get 获取结果)。
*
* 由于 Future 接口不能直接实例化,所以一般都是使用 FutureTask,它实现了 RunnableFuture
* 接口,RunnableFuture 又继承了 Runnable 和 Future,所以它既可以作为 Runnable 被线程执行,
* 又可以作为Future得到Callable的返回值。
*/
private void test() {
// 将 Callable 包装进 FutureTask 后交给 Thread
FutureTask<String> futureTask = new FutureTask<>(this);
new Thread(futureTask).start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
// 任务是读取文件内容,并发它转换成字符串作为返回值
@Override
public String call() throws Exception {
StringBuffer stringBuffer = new StringBuffer();
try (FileInputStream fileInputStream = new FileInputStream("filepath");
FileChannel inChannel = fileInputStream.getChannel()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(256);
while (inChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
Charset charset = Charset.forName("GBK");
CharBuffer charBuffer = charset.decode(byteBuffer);
stringBuffer.append(charBuffer);
byteBuffer.clear();
}
}
return stringBuffer.toString();
}
}
原因解释
我们结合 Thread 的源码解释下原因。首先,Thread 的构造方法中并没有 Callable 作为参数的:
只有空参和接收 Runnable 的构造方法:
public class Thread implements Runnable {
/* What will be run. */
private Runnable target;
public Thread() {
// nextThreadNum() 初始为 0,可以看到线程编号在创建线程时就已经确定
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
// init() 会将参数 target 赋值给成员变量的 target
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public void run() {
if (target != null) {
target.run();
}
}
}
关注运行任务的 run():
- 如果使用继承 Thread 的方式创建线程,那么 run() 被重写,执行任务时就按照 Thread 子类的 run() 去执行。
- 如果使用实现 Runnable 的方式创建线程,run() 在判断 target 不为空之后会运行 target 的 run()。可以认为 Runnable 就是对线程的任务进行了对象的封装。
因为 FutureTask -> RunnableFuture -> Runnable & Future,而 Callable 需要交给 FutureTask 才能执行,所以实现 Callable 接口这种创建线程的方式在实现 Runnable 接口的范畴内,不能作为一种单独的创建方式。
两种创建线程的方式对比
两种方式各有优缺点,通常还是使用实现 Runnable 接口的方式:
- 继承 Thread 类方式编写简单如果要访问当前线程无须使用 Thread.currentThread(),直接使用 this 即可。劣势是不能再继承其它父类。
- 实现 Runnable 接口方式则可以继承其它类,并且多个线程可以共享同一个 target 对象,非常适合多个相同线程来处理同一份资源,从而将 CPU、代码和数据分开,形成清晰的模型。缺点就是编程稍复杂,必须使用 Thread.currentThread() 访问当前线程。
2.2 停止线程
线程会因为如下两个原因之一被停止:
- run() 正常退出而自然死亡
- 因为一个没有捕获的的遗产终止了 run() 而意外死亡
推荐使用 Thread 的 interrupt() 配合 isInterrupted()/interrupted() 来停止线程。
interrupt()
interrupt() 会请求线程停止,注意是请求,而不是立即停止线程。该方法会将线程中的中断状态标记位
置位,等待线程通过 isInterrupted()/interrupted() 检查该标记位来进行响应。isInterrupted() 和 interrupted() 都会在标记位被置位的情况下返回 true,不同点在于前者是对象方法,而后者是一个静态方法,并且在调用后会将标记位改写为 false。
在使用上述方法时需要注意,如果在中断标记位为 true 的情况下执行阻塞方法(如 Thread.sleep()、Thread.join()、Object.wait()),这些阻塞方法会抛出 InterruptedException,并且在抛出异常后立即将线程的中断标示位清除重置为 false:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Thread is running...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().isInterrupted());
e.printStackTrace();
// Thread.currentThread().interrupt();
}
}
}
});
thread.start();
Thread.sleep(1000);
thread.interrupt();
System.out.println("interrupt in main!");
}
如果不打开 catch 中被注释掉的 Thread.currentThread().interrupt(),线程是无法被中断的,因为 sleep() 如果发现中断标记位为 true 会抛出异常并将其清除为 false:
Thread is running...
Thread is running...
Thread is running...
Thread is running...
interrupt in main!
false
Thread is running...
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.demo.thread.multi.InterruptDemo$1.run(InterruptDemo.java:13)
at java.lang.Thread.run(Thread.java:748)
Thread is running...
Thread is running...
// 线程继续运行输出 Thread is running...
从这里也能看到子线程其实是在主线程结束后才消亡的,一定注意要让线程能执行完,否则这个线程会阻止已经运行完的主线程所在的进程结束。
在抛出 InterruptedException 的 catch 代码块中调用 interrupt() 中断线程是基本操作。
自定义标记位
interrupt() 基本上是我们推荐的,唯一的中断线程的方法。或许有人会问,自己在线程中定义一个中断标记位不是也能实现线程中断嘛,像这样:
class StopThread implements Runnable {
private boolean flag = true;
public void run() {
while (flag) {
System.out.println(Thread.currentThread().getName() + "......++++");
}
}
// 外部调用注入方法控制标记位进而停止线程
public void setFlag(boolean flag) {
this.flag = flag;
}
}
一般情况下确实可以,但如果线程中执行的代码有类似 wait() 这样的阻塞方法,那么该线程就会进入等待状态,在线程池中等待唤醒,在没有其它线程唤醒它的情况下,它就无法通过标记位的方式结束线程:
public synchronized void run() {
while (flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "......++++");
}
}
在这种情况下,使用 interrupt() 会更好,因为:
- 一般的阻塞方法,如 sleep()、wait() 等本身就支持中断的检查
- 检查中断标记位和自定义的标志位没什么区别,用中断标记位还可以避免声明自定义的标志位,减少资源的消耗
- interrupt() 方法会将线程从阻塞状态强制恢复到运行状态中来,让线程具备 cpu 的执行资格,但是强制动作会发生 InterruptedException,需要处理
中断异常是如何被抛出的
我们以 Thread.sleep() 为例,进入源码看下中断异常是如何被抛出的:
/**
* sleep 期间,线程不会失去已经获取到的同步锁。
*
* @throws InterruptedException
* 如果任何线程中断了当前线程,那么会抛出这个异常并且
* 清除掉当前线程的中断状态。
*/
public static native void sleep(long millis) throws InterruptedException;
想要查看 sleep() 的 native 源码,要先在 src/share/native/java/lang/Thread.c 文件中,找到 sleep() 在 JVM 中对应的方法 JVM_Sleep:
#include "jni.h"
#include "jvm.h"
#include "java_lang_Thread.h"
#define THD "Ljava/lang/Thread;"
#define OBJ "Ljava/lang/Object;"
#define STE "Ljava/lang/StackTraceElement;"
#define STR "Ljava/lang/String;"
#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))
static JNINativeMethod methods[] = {
{"start0", "()V", (void *)&JVM_StartThread},
{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
{"isAlive", "()Z", (void *)&JVM_IsThreadAlive},
{"suspend0", "()V", (void *)&JVM_SuspendThread},
{"resume0", "()V", (void *)&JVM_ResumeThread},
{"setPriority0", "(I)V", (void *)&JVM_SetThreadPriority},
{"yield", "()V", (void *)&JVM_Yield},
{"sleep", "(J)V", (void *)&JVM_Sleep},
{"currentThread", "()" THD, (void *)&JVM_CurrentThread},
{"countStackFrames", "()I", (void *)&JVM_CountStackFrames},
{"interrupt0", "()V", (void *)&JVM_Interrupt},
{"isInterrupted", "(Z)Z", (void *)&JVM_IsInterrupted},
{"holdsLock", "(" OBJ ")Z", (void *)&JVM_HoldsLock},
{"getThreads", "()[" THD, (void *)&JVM_GetAllThreads},
{"dumpThreads", "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
{"setNativeName", "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};
#undef THD
#undef OBJ
#undef STE
#undef STR
// 注册 methods[] 中的 native 方法
JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}
然后去 jvm.cpp 文件中找到这个方法:
可以看到如果线程的中断标记位已经为 true,调用 sleep 方法就会抛出 InterruptedException。在抛出 InterruptedException 之前,中断标记位会被清除为 false。
3、线程的状态
3.1 状态定义与状态转移
线程状态也被称为生命周期,指的是 JVM 中的线程状态,而不是操作系统的。Thread 中定义的枚举类 State 规定了线程的 6 种状态:
- 初始(NEW):新创建了一个线程对象,但还没有调用 start()。这时仅仅由虚拟机分配内存并初始化变量值。如果此时错误地调用了 run(),该线程就不再处于初始状态,不能再调用 start()。
- 可运行(RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种状态统称为“可运行”。
线程对象创建后,其他线程(如主线程)调用了该对象的 start() 后会进入就绪状态,虚拟机会创建方法调用栈和程序计数器。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 CPU 的使用权。即线程可以运行,但是尚未运行。
就绪状态的线程在获得 CPU 时间片后,开始执行 run() 就变为运行中状态(running)。 - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(如通知或中断)。
- 等待超时(TIMED_WAITING):该状态不同于 WAITING,它可以在指定的时间后自行返回,也就是计时等待。
- 终止(TERMINATED):表示该线程已经执行完毕,包括 run() 或 call() 执行完成,线程正常结束;线程抛出未捕获的 Exception 或 Error;直接调用了 stop() 结束线程(容易死锁,不推荐)。
状态转移图如下所示:
说明:
- 主线是初始->运行->终止,new 创建的新线程要调用 start() 才能进入运行状态。运行状态内部又分为运行中(具备执行权)和就绪(具备执行资格但没有执行权)两种状态。注意 Java 中是把运行中和就绪统一视为运行状态,但是在操作系统的观点中,认为这两个状态是分开的、两个独立的状态。
- 运行状态的线程可以通过调用 Object.wait() 或 Thread.sleep() 等方法进入等待状态,方法上加时间参数的会进入等待超时状态。等待状态下的线程没有执行资格,需要通过 notify()、notifyAll() 等方法唤醒。
- 阻塞状态(具备执行资格但无执行权)只有一种情况,就是等待获取 synchronized 锁,拿到锁之后就变成了运行状态(阻塞式 IO 方法应该是这种情况?)。注意使用显式锁 Lock 等待锁时,进入的是等待/等待超时状态,因为其底层使用的是 LockSupport 类实现的。因此系统中能让线程进入阻塞状态的有且仅有 synchronized 关键字。
- 阻塞状态是一种被迫的等待(因为拿不到锁只能等着),而等待状态是一种主动的等待(主动调用 wait() 或 sleep())。
此外还有几点注意事项:
- 主线程结束并不会影响其它线程。
- isAlive() 可以测试某个线程是否存活,就绪、运行、阻塞状态返回 true,新建、死亡返回 false。
- 不要对已经死亡的线程调用 start(),也不要对新建的线程调用两次 start(),否则会引发 ILLegalThreadStateException。
针对以上情况,当发生如下特定情况时可以解除上面的阻塞,使线程重新进入就绪状态:
3.2 涉及的方法介绍
join()
可以通过 join() 控制线程的执行顺序,哪个线程执行到了 join() 就释放执行权并冻结在线程池中,等调用了 join() 的线程执行完后,才恢复可执行状态,与其它线程争夺执行权。比如说:
public static void main(String[] args) throws Exception
{
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
t1.join();//t1线程要申请加入进来,运行。临时加入一个线程运算时可以使用join方法。
for(int x=0; x<50; x++)
{
System.out.println(Thread.currentThread()+"....."+x);
}
}
}
主线程开启了 t1、t2 两个线程,在执行 t1.join() 之前,是三个线程在轮番运行的。在主线程中执行了 t1.join() 后,主线程释放执行权,冻结在线程池。等待 t1 执行完毕后,恢复可执行状态,与 t2 争夺执行资格,轮番运行。
如果在 A 线程中调用了 B 线程的 join() 方法,那么 A 线程将被阻塞直到 B 线程执行完。它有三种重载形式:
第三种形式很少被用到,因为程序、操作系统和计算机硬件都无法精确到纳秒。
sleep()
sleep() 用于线程睡眠,调用该方法可以让线程暂停一段时间进入等待状态,它有两种重载形式:
同样是因为程序、操作系统和硬件设备无法精确到纳秒,因此第二个方法很少被使用。
处于睡眠时间内的线程不会获得执行的机会,即使系统中没有其它可执行的线程,处于 sleep() 中的线程也不会执行;已经获得锁的线程如果执行了 sleep(),只会释放执行权,但不会释放锁。
yield()
yield() 用于线程让步,它也可以让当前正在运行的线程暂停,但它不会使线程进入等待状态,只是将该线程转入可运行状态,然后让系统的线程调度器重新调度一次。完全可能的情况是:某个线程调用了 yield() 暂停之后,调度器又将其调度出来重新执行。
实际上,当 A 线程调用了 yield() 之后,只有优先级大于等于 A 的处于可运行状态的线程才会获得执行机会。
暂停当前正在执行的线程对象,释放执行权,然后让包括自己在内的所有线程再次争夺执行权。这样做会更和谐,不会一直在执行同一个线程。
sleep() 与 yield() 的区别:
其它方法
Object.wait() 与 Thread.sleep() 的对比:
- wait() 可以指定时间也可以不指定,sleep() 必须指定时间。
- 在同步中时,对 cpu 的执行权和锁的处理不同。wait() 释放执行权,释放锁;sleep() 释放执行权,不释放锁。
class Demo {
void show() {
synchronized(this) {
wait();//t0 t1 t2
}
}
void method() {
synchronized(this)//t4
{
notifyAll();
}//t4
}
}
同步中谁有锁谁执行,如果 t0 t1 t2 都卡在 wait() 并被 t4 唤醒,虽然 3 个都活了,但是只有一个能持锁执行代码。
此外,我们经常会调用 Thread 的 toString() 输出线程信息,线程的字符串表现形式,包括线程名称、优先级和线程组:
public String toString() {
ThreadGroup group = getThreadGroup();
if (group != null) {
return "Thread[" + getName() + "," + getPriority() + "," +
group.getName() + "]";
} else {
return "Thread[" + getName() + "," + getPriority() + "," +
"" + "]";
}
}
4、线程属性
4.1 优先级
每个 Java 线程都有一个优先级,可以通过 Thread.setPriority() 将线程优先级设置在 MIN_PRIORITY(数值为1)和 MAX_PRIORITY(数值为10)之间,默认优先级为 MIN_PRIORITY(数值为5)。
线程调度器会优先选择优先级高的线程来运行。但是线程优先级是高度依赖于系统的。当 JVM 依赖于宿主机平台的线程实现机制时,Java 线程的优先级会先被映射到宿主机平台的优先级上。例如 Windows 有 7 个优先级,而在 Oracle 为 Linux 提供的 JVM 中,线程的优先级被忽略——所有线程具有相同的优先级。
如果确实要使用优先级,需要注意,如果高优先级的线程没有进入非活动状态(阻塞或等待),低优先级的线程可能永远也得不到执行,发生线程饥饿的情况(线程饥饿就是指低优先级的线程,总是拿不到执行时间)。
4.2 守护线程
守护线程也称为后台线程、精灵线程,它是在后台运行的线程,任务是为其它线程提供服务,JVM 的垃圾回收线程就是典型的守护线程。此外,守护线程也可用于发送计时信号或清空过时的高速缓存。
可以使用 Thread 的 setDeamon(true) 将一个线程设置为守护线程(但是必须在该线程启动之前,否则会引发 ILLegalThreadStateException),isDeamon() 用来判断是否是守护线程。
主线程默认是前台线程,但不是所有线程默认都是前台线程,规则是:前台线程创建的子线程默认是前台线程,守护线程创建的线程默认是后台线程。
守护线程也会去争抢同步锁。
守护线程的特征为当所有前台线程死亡后,虚拟机会退出并通知后台线程死亡。假如在主线程中开启了一个执行耗时操作的守护线程,那么很有可能守护线程的任务并不会执行完,因为主线程不会等待守护线程,只要主线程跑完了,守护线程也会自动消亡。但如果在主线程中启动一个非守护线程,那么主线程会等待该子线程执行完任务。
永远不要让守护线程去访问文件、数据库这样的固有资源,因为他会在任何时候甚至在一个操作的中间被中断。
4.3 线程组与未处理的异常
Java 允许程序直接对 ThreadGroup 进行控制。如果没有显式指定一个线程属于哪个线程组,那么它就属于默认线程组,即与创建它的线程在同一线程组。线程中途不能改变它所属的线程组。
以下构造方法用来指定新创建的线程属于哪个线程组:
以上方法也可以指定线程组的名字,这个名字也不能中途更改。
常用的操作线程组的方法:
ThreadGroup 实现了一个接口 Thread.UncaughtExceptionHandler,该接口用于处理未捕获的异常。
线程的 run() 不能抛出任何受查异常(刨去非受查异常剩余的异常),而非受查异常(所有派生于 Error 或 RuntimeException 的异常)会导致线程终止。
线程因为异常终止之前,会将异常传递到未捕获异常的处理器中,该处理器必须实现 Thread.UncaughtExceptionHandler 接口,并且通过 Thread 的 setUncaughtExceptionHandler() 为某一个线程设置处理器,或者用静态的 setDefaultUncaughtExceptionHandler() 为所有线程设置一个默认的处理器。如果没有调用以上方法给线程设置处理器,那么线程的处理器就是该线程的 ThreadGroup 对象。
UncaughtExceptionHandler 接口的唯一方法 uncaughtException() 会按照如下优先顺序操作:
- 如果该线程组有父线程组,则调用父线程组的 uncaughtException()
- 如果 Thread 的 getDefaultUncaughtExceptionHandler() 返回一个非空处理器,则调用该处理器
- 如果 Throwable 是 ThreadDeath 的一个实例,就什么都不做,否则,就将线程名字以及 Throwable 的栈轨迹输出到 System.err 上。
其中,最后一点的栈轨迹就是应用发生崩溃时我们看到的调用栈信息。