现在操作系统在运行一个程序时,会自动为其创建一个进程,不论是 PC 还是 Android。
一个进程内可以有多个线程,这些线程作为操作系统调度的最小单元,负责执行各种各样的任务,这些线程都拥有各自的计数器、堆栈、局部变量等属性,并且可以访问共享内存。
想象一下,如果你的电脑里只有一条线程在执行任务,一旦遇到 I/O 密集的任务,CPU 只能长时等待,效率很低。
如果把一个进程比作一个外卖公司,CPU 就是外卖公司拥有的主要资源(可以当做电动车),那线程(Thread)就是外卖公司中的一位送餐员,Runnable 就是送餐员要执行的任务(一般情况下都是送饭)。
送餐员最重要的任务就是送餐,我们以代码来演示创建一个送餐员的三种方式:
Runnable
接口public class ThreadTest0 {
/**
* 1.实现 Runnable 接口,在 run() 方法中写要执行的任务
*/
static class Task implements Runnable{
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(300
));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 您的外卖已送达");
}
}
public static void main(String[] args) {
for (int i = 0; i < 4; i++) {
//2.创建一个送餐员线程,然后将任务传递给他,同时起个名
Thread shixinzhang = new Thread(new Task(), "外卖任务 " + i);
//3.命令送餐员出发!
shixinzhang.start();
}
}
}
注意,上述代码中调用的是送餐员线程的 start()
方法,然后线程会调用 Task 对象的 run()
方法执行任务。运行结果如下:
外卖任务 3: 您的外卖已送达 外卖任务 1: 您的外卖已送达 外卖任务 0: 您的外卖已送达 外卖任务 2: 您的外卖已送达
可以看到执行任务的是各个线程。如果在 main()
方法中直接调用 run
方法,就相当于主线程直接执行任务,没有在子线程中进行。
直接在 main
中调用 run()
:
public static void main(String[] args) {
for (int i = 0; i < 4; i++) {
//2.创建一个送餐员线程,然后将任务传递给他,同时起个名
Task task = new Task();
Thread shixinzhang = new Thread(task, "外卖任务 " + i);
//3.直接执行任务
task.run();
}
}
运行结果:
main: 您的外卖已送达 main: 您的外卖已送达 main: 您的外卖已送达 main: 您的外卖已送达
Thread
,重写其 run
方法public class ThreadTest1 {
/**
* 继承 Thread,重写 run 方法,在 run 方法中写要执行的任务
*/
static class DeliverThread extends Thread{
public DeliverThread(String name) {
super(name);
}
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(300
));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 您的外卖已送达");
}
}
public static void main(String[] args) {
for (int i = 0; i < 4; i++) {
//2.创建一个送餐员线程,同时起个名
DeliverThread shixinzhang = new DeliverThread("外卖任务" + i);
shixinzhang.start();
}
}
}
运行结果:
外卖任务1: 您的外卖已送达 外卖任务3: 您的外卖已送达 外卖任务0: 您的外卖已送达 外卖任务2: 您的外卖已送达
为什么直接继承 Thread
也可以在子线程中执行任务呢?
从 Thread
源码中我们可以看到, Thread
其实也实现了 Runnable
:
public class Thread implements Runnable
它内部也有一个 Runnable
的引用,我们调用 start()
方法后送餐员小张就蓄势以待准备出发了,之所以没说“立即出发送餐”,是因为此时可能电动车(CPU)正在被别人使用。
线程 start()
后操作系统会给他分配相关的资源,包括单独的程序计数器(可以理解为送餐员的任务本,上面记录了当前送餐任务的地址和下一个任务的地址)和栈,操作系统会把这个线程作为一个独立的个体进行调度,分配时间片让它执行。
等线程被 CPU 调度后就会执行线程中的 run()
方法,因此我们通过重写 Thread
的 run()
方法就可以达到在子线程执行任务的目的。
Callable
接口,重写 call()
方法,用 FutureTask
获得结果public class CallableTest {
/**
* 实现 Callable 接口
*/
static class DeliverCallable implements Callable<String> {
/**
* 执行方法,相当于 Runnable 的 run, 不过可以有返回值和抛出异常
* @return
* @throws Exception
*/
@Override
public String call() throws Exception {
Thread.sleep(new Random().nextInt(10000));
System.out.println(Thread.currentThread().getName() + ":您的外卖已送达");
return Thread.currentThread().getName() + " 送达时间:" + System.currentTimeMillis() + "\n";
}
}
/**
* Callable 作为参数传递给 FutureTask,FutureTask 再作为参数传递给 Thread(类似 Runnable),然后就可以在子线程执行
* @param args
*/
public static void main(String[] args) {
List<FutureTask<String>> futureTasks = new ArrayList<>(4);
for (int i = 0; i < 4; i++) {
DeliverCallable callable = new DeliverCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
futureTasks.add(futureTask);
Thread thread = new Thread(futureTask, "送餐员 " + i);
thread.start();
}
StringBuilder results = new StringBuilder();
futureTasks.forEach(futureTask -> {
try {
//获取线程返回结果,没返回就会阻塞
results.append(futureTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
System.out.println(System.currentTimeMillis() + " 得到结果:\n" + results);
}
}
第三种创建线程的方式与前两种的不同之处在于,以 Callable
作为任务,而不是 Runnable
,这种方式的好处是可以获得结果和响应中断。
Callable
,Future
和FutureTask
的内容我会另开一篇文章专门介绍。
运行结果:
送餐员 3:您的外卖已送达 送餐员 1:您的外卖已送达 送餐员 0:您的外卖已送达 送餐员 2:您的外卖已送达 1487998155430 得到结果: 送餐员 0 送达时间:1487998155076 送餐员 1 送达时间:1487998150453 送餐员 2 送达时间:1487998155430 送餐员 3 送达时间:1487998149779
Thread
有个优先级字段:private int priority
操作系统采用时间片(CPU 单次执行某线程的时间)的形式来调度线程的运行,线程被 CPU 调用的时间超过它的时间片后,就会发生线程调度。
线程的优先级可以在一定程度上影响它得到时间片的多少,也就是被处理的机会。
Java 中 Thread 的优先级为从 1 到 10 逐渐提高,默认为 5。
有长耗时操作的线程,一般建议设置低优先级,确保处理器不会被独占太久; 频繁阻塞(休眠或者 I/O)的线程建议设置高优先级。
public final static int MIN_PRIORITY = 1;
//线程的默认优先级
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
线程优先级只是对操作系统分配时间片的建议。 虽然 Java 提供了 10 个优先级别,但不同的操作系统的优先级并不相同,不能很好的和 Java 的 10 个优先级别对应。>所以我们应该使用 MAX_PRIORITY、MIN_PRIORITY 和 NORM_PRIORITY 三个静态常量来设定优先级,这样才能保证程序最好的可移植性。
Java 中,线程也分三六九等。守护线程相当于小弟,做一些后台调度、支持性工作,比如 JVM 的垃圾回收、内存管理等线程都是守护线程。
Thread
中有个布尔值标识当前线程是否为守护线程:
private boolean daemon = false;
同时也提供了设置和查看当前线程是否为守护线程的方法:
public final void setDaemon(boolean on) {
checkAccess();
if (isAlive()) {
throw new IllegalThreadStateException();
}
daemon = on;
}
public final boolean isDaemon() {
return daemon;
}
Daemon 属性需要在调用线程的
start()
方法之前调用。
一个进程中,如果所有线程都退出了,Java 虚拟机就会退出。注意了,这里的“所有”就不包括守护线程,也就是说,当除守护线程外的其他线程都结束后,Java 虚拟机就会退出,然后将守护进程终止。
这里需要注意的是,由于上述特性,Java 虚拟机退出后,在守护线程中的 finally 块中的代码不一定执行。
举个例子:
public class DaemonTreadTest0 {
static class DaemonThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName() + " finally is called!");
}
}
}
public static void main(String[] args) {
DaemonThread thread = new DaemonThread();
thread.setDaemon(true);
thread.start();
}
}
上述代码中将线程设置为守护线程,由于 main 线程启动 DaemonThread
后就结束,此时虚拟机中没有非守护线程,虚拟机也会退出,守护进程被终止,但是它的 finally 块中的内容却没有被调用。
如果将setDaemon
方法注释掉,就会发现有运行结果:
Thread-0 finally is called!
因此,守护线程中不能依靠 finally 块进行资源关闭和清理。
线程具有如下几个状态:
线程状态 | 介绍 | 备注 |
---|---|---|
NEW | 新创建 | 还未调用 start() 方法;还不是活着的 (alive) |
RUNNABLE | 就绪的 | 调用了 start() ,此时线程已经准备好被执行,处于就绪队列;是活着的(alive) |
RUNNING | 运行中 | 线程获得 CPU 资源,正在执行任务;活着的 |
BLOCKED | 阻塞的 | 线程阻塞于锁或者调用了 sleep;活着的 |
WAITING | 等待中 | 线程由于某种原因等待其他线程;或者的 |
TIME_WAITING | 超时等待 | 与 WAITING 的区别是可以在特定时间后自动返回;活着的 |
TERMINATED | 终止 | 执行完毕或者被其他线程杀死;不是活着的 |
有几点注意:
WAITING
状态的方法 Java 中关于“线程是否活着”的定义
Thread
中有个判断是否为活着的方法:
public final native boolean isAlive()
Java 中线程除了 NEW 和 TERMINITED 状态,其他状态下调用 isAlive()
方法均返回 true,也就是活着的。
Thread.sleep()
是一个静态方法:
public static native void sleep(long millis) throws InterruptedException;
sleep()
方法:
InterruptedException
注意上面的第一条!由于 sleep 是静态方法,它的作用时使当前所在线程阻塞。因此最好在线程内部直接调用 Thread.sleep(),如果你在主线程调用某个线程的 sleep() 方法,其实阻塞的是主线程!
与 Thread.sleep()
容易混淆的是 Object.wait()
方法。
Object.wait()
方法:
Thread. yield()
也是一个静态方法:
public static native void yield();
“Thread.yield() 表示暂停当前线程,让出 CPU 给优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程。
yield() 一般使用较少。
Thread.join()
表示线程合并,调用线程会进入阻塞状态,需要等待被调用线程结束后才可以执行。
线程的合并的含义就是将几个并发执行线程的线程合并为一个单线程执行。
比如下述代码:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread is running!");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
thread.join();
System.out.println("main thread ");
我们在主线程调用了 thread.join()
方法,该线程会在输出一句话后休眠 5 秒,等该线程结束后主线程才可以继续执行,输出最后一句结果:
thread is running! main thread
Thread.join
源码:
//无参方法
public final void join() throws InterruptedException {
join(0);
}
//有参方法,表示等待 millis 毫秒后自动返回
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(delay);
now = System.currentTimeMillis() - base;
}
}
}
//有参方法,表示等待 millis + (nanos - 50000) 毫秒后结束
public final synchronized void join(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
}
通过源码可以发现,Thread.join
是通过 synchronized + Object.wait() 实现的。
Thread.join
的应用场景是:当一个线程必须等待其他线程执行完毕才能继续执行,比如合并计算。
有时候我们需要中断一个正在运行的线程,一种很容易想到的方法是在线程的 run()
方法中加一个循环条件:
public class ThreadInterruptTest1 {
static class InterruptThread extends Thread{
private boolean running;
public InterruptThread(boolean running) {
this.running = running;
}
public boolean isRunning() {
return running;
}
public void setRunning(boolean running) {
this.running = running;
}
@Override
public void run() {
while (running){
System.out.println(Thread.currentThread().getName() + " is running");
}
}
}
public static void main(String[] args) throws InterruptedException {
InterruptThread thread = new InterruptThread(true);
thread.start();
Thread.sleep(5000);
thread.setRunning(false);
}
}
上面的代码中线程 InterruptThread
有一个标志位 running
,当这个标志位为 true 时才可以运行。
因此我们可以通过修改这个标志位为 false 来中断该线程。
其实 Thread 内部也为我们提供了同样的机制 :
方法名 | 方法介绍 |
---|---|
public void interrupt() | 试图中断调用线程,设置中断标志位为 false |
public boolean isInterrupted() | 返回调用线程是否被中断 |
public static boolean interrupted() | 返回当前线程是否被中断的状态值,同时将中断标志位复位(设为 false) |
它的作用是设置标志位为 false,能否达到中断调用线程的效果,还取决于该线程是否可以响应中断(说直白些就是吃不吃这套),比如 Runnable
的 run()
方法就无法响应中断。
因此我们对执行 Runnable 任务的线程调用 interrupt()
方法后,该线程也不会中断,举个例子:
public class ThreadInterruptTest2 {
static class UnInterruptThread extends Thread{
public UnInterruptThread(String s) {
setName(s);
}
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " is running!");
}
}
}
static class UnInterruptRunnable implements Runnable{
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " is running!");
}
}
}
public static void main(String[] args) throws InterruptedException {
UnInterruptThread thread = new UnInterruptThread("无法中断的线程");
// Thread thread = new Thread(new UnInterruptRunnable(), "无法中断");
thread.start();
//先让它执行一秒
Thread.sleep(1000);
thread.interrupt();
//不立即退出
Thread.sleep(3000);
}
}
这两种方式创建的线程,在调用 thread.interrupt()
方法后仍然会继续执行!
这时就需要用到上面 Thread 提供的第二个关于中断的方法 isInterrupted()
了。
我们可以通过 isInterrupted()
知道调用线程是否被中断,以此来作为线程是否运行的判断标志。
isInterrupted()
在刚创建时默认为 false 不用多说; 线程有许多方法可以响应中断(比如Thread.sleep()
,Thread.wait()
),这些方法在收到中断请求、抛出InterruptedException
之前,JVM 会先把该线程的中断标志位复位,这时调用isInterrupted
将会返回 false; 线程结束后,线程的中断标志位也会复位为 false。
举个例子:
/**
* 线程中断练习
* Created by zhangshixin on 17/2/25.
* http://blog.csdn.net/u011240877
*/
public class ThreadInterruptTest {
/**
* 调用 Thread.sleep() 方法的线程,线程如果在 sleep 时被中断,会抛出 InterruptedException
* 我们在代码中进行捕获,并且查看 JVM 是否将中断标志位重置
*/
static class SleepThread extends Thread{
public SleepThread(String s) {
setName(s);
}
@Override
public void run() {
while (!isInterrupted()){
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("SleepRunner 在 sleep 时被中断了,此时中断标志位为:" + isInterrupted());
}
}
}
}
/**
* 希望通过这个线程了解:线程运行结束后,中断标志位会重置
*/
static class BusyThread extends Thread{
public BusyThread(String s) {
setName(s);
}
@Override
public void run() {
while (!isInterrupted()){
System.out.println(Thread.currentThread().getName() + System.currentTimeMillis());
}
}
}
public static void main(String[] args) throws InterruptedException {
SleepThread sleepThread = new SleepThread("SleepRunner:");
BusyThread busyThread = new BusyThread("BusyRunner:");
//新创建的线程 中断标志为 false
System.out.println("SleepThread 新创建时的中断标志位:" + sleepThread.isInterrupted());
Thread.sleep(2000);
//启动两个线程
sleepThread.start();
busyThread.start();
//让它们运行一秒
Thread.sleep(1000);
//分别中断两个线程
sleepThread.interrupt();
busyThread.interrupt();
//查看线程的中断标志位
Thread.sleep(2000);
System.out.println("由于中断标志位变为 true 导致运行结束的线程,中断标志位为: " + busyThread.isInterrupted());
Thread.sleep(1000);
}
}
上述代码中 两个线程都使用 isInterrupted
作为循环执行任务的条件,其中 SleepThread
方法调用了 Thread.sleep,这个方法的会响应中断,抛出异常。
运行结果如下:
可以看到:
InterruptedException
前 JVM 的确会重置中断标志位为 falseisInterrupted
方法作为循环执行任务的线程无法正确中断Thread.interrupted()
方法是一个静态方法,它会返回调用线程(而不是被调用线程)的中断标志位,返回后重置中断标志位。
因此 Thread.interrupted()
第二次调用永远返回 false。
源码:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
这篇文章总结了 线程的基本概念和关键方法,还有一些不建议使用的方法没有介绍,是因为它们有很多副作用,比如 suspend()
方法在调用后虽然线程会进入休眠状态,却不会释放资源,很容易引发死锁问题;同样,stop()
方法终结一个线程时无法保证这个线程有机会释放资源,也会导致一些不确定问题。
我们可以通过下面的图片整体分析线程的生命周期和主要方法:
相关阅读: 趣谈并发2:认识并发编程的利与弊
欢迎扫描关注微信公众号“安卓进化论”,向高手进击!