打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
趣谈并发(1):全面认识 Thread

读完本文你将了解到:

线程简介

现在操作系统在运行一个程序时,会自动为其创建一个进程,不论是 PC 还是 Android。

一个进程内可以有多个线程,这些线程作为操作系统调度的最小单元,负责执行各种各样的任务,这些线程都拥有各自的计数器、堆栈、局部变量等属性,并且可以访问共享内存

想象一下,如果你的电脑里只有一条线程在执行任务,一旦遇到 I/O 密集的任务,CPU 只能长时等待,效率很低。

如果把一个进程比作一个外卖公司,CPU 就是外卖公司拥有的主要资源(可以当做电动车),那线程(Thread)就是外卖公司中的一位送餐员,Runnable 就是送餐员要执行的任务(一般情况下都是送饭)。

线程创建的三种方式

送餐员最重要的任务就是送餐,我们以代码来演示创建一个送餐员的三种方式:

1.实现 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(); } }}
  • 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

注意,上述代码中调用的是送餐员线程的 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(); }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

运行结果:

main: 您的外卖已送达
main: 您的外卖已送达
main: 您的外卖已送达
main: 您的外卖已送达

2.继承 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
  • 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

运行结果:

外卖任务1: 您的外卖已送达
外卖任务3: 您的外卖已送达
外卖任务0: 您的外卖已送达
外卖任务2: 您的外卖已送达

为什么直接继承 Thread 也可以在子线程中执行任务呢?

Thread 源码中我们可以看到, Thread 其实也实现了 Runnable

public class Thread implements Runnable
  • 1

它内部也有一个 Runnable 的引用,我们调用 start() 方法后送餐员小张就蓄势以待准备出发了,之所以没说“立即出发送餐”,是因为此时可能电动车(CPU)正在被别人使用。

线程 start() 后操作系统会给他分配相关的资源,包括单独的程序计数器(可以理解为送餐员的任务本,上面记录了当前送餐任务的地址和下一个任务的地址)和栈,操作系统会把这个线程作为一个独立的个体进行调度,分配时间片让它执行。

等线程被 CPU 调度后就会执行线程中的 run() 方法,因此我们通过重写 Threadrun() 方法就可以达到在子线程执行任务的目的。

3.实现 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); }}
  • 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

第三种创建线程的方式与前两种的不同之处在于,以 Callable 作为任务,而不是 Runnable,这种方式的好处是可以获得结果响应中断

Callable, FutureFutureTask 的内容我会另开一篇文章专门介绍。

运行结果:

送餐员 3:您的外卖已送达
送餐员 1:您的外卖已送达
送餐员 0:您的外卖已送达
送餐员 2:您的外卖已送达
1487998155430 得到结果:
送餐员 0 送达时间:1487998155076
送餐员 1 送达时间:1487998150453
送餐员 2 送达时间:1487998155430
送餐员 3 送达时间:1487998149779

线程的基本属性

1.优先级

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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

线程优先级只是对操作系统分配时间片的建议。
虽然 Java 提供了 10 个优先级别,但不同的操作系统的优先级并不相同,不能很好的和 Java 的 10 个优先级别对应。>所以我们应该使用 MAX_PRIORITY、MIN_PRIORITY 和 NORM_PRIORITY 三个静态常量来设定优先级,这样才能保证程序最好的可移植性。

2.守护线程

Java 中,线程也分三六九等。守护线程相当于小弟,做一些后台调度、支持性工作,比如 JVM 的垃圾回收、内存管理等线程都是守护线程。

Thread 中有个布尔值标识当前线程是否为守护线程:

private boolean daemon = false;
  • 1

同时也提供了设置和查看当前线程是否为守护线程的方法:

public final void setDaemon(boolean on) { checkAccess(); if (isAlive()) { throw new IllegalThreadStateException(); } daemon = on;}public final boolean isDaemon() { return daemon;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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(); }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

上述代码中将线程设置为守护线程,由于 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 终止 执行完毕或者被其他线程杀死;不是活着的

有几点注意:

  • Java 中的 Thread 运行状态没有 RUNNING 这一步,运行中的线程状态是 RUNNABLE
  • 三个让线程进入 WAITING 状态的方法
    • Object.wait()
    • Thread.join()
    • LockSupport.park()
    • Lock.lock()

Java 中关于“线程是否活着”的定义

Thread 中有个判断是否为活着的方法:

public final native boolean isAlive()
  • 1

Java 中线程除了 NEW 和 TERMINITED 状态,其他状态下调用 isAlive() 方法均返回 true,也就是活着的。

线程的关键方法

1.Thread.sleep()

Thread.sleep() 是一个静态方法:

public static native void sleep(long millis) throws InterruptedException;
  • 1

sleep() 方法:

  • 使当前所在线程进入阻塞
  • 只是让出 CPU ,并没有释放对象锁
  • 由于休眠时间结束后不一定会立即被 CPU 调度,因此线程休眠的时间可能大于传入参数
  • 如果被中断会抛出 InterruptedException

注意上面的第一条!由于 sleep 是静态方法,它的作用时使当前所在线程祖舍。因此最好在线程内部直接调用 Thread.sleep(),如果你在主线程调用某个线程的 sleep() 方法,其实阻塞的是主线程!

2.Object.wait()

Thread.sleep() 容易混淆的是 Object.wait() 方法。

Object.wait() 方法:

  • 让出 CPU,释放对象锁
  • 在调用前需要先拥有某对象的锁,所以一般在 synchronized 同步块中使用
  • 使该线程进入该对象监视器的等待队列

3.Thread.yield()

Thread. yield() 也是一个静态方法:

public static native void yield();
  • 1

“Thread.yield() 表示暂停当前线程,让出 CPU 给优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程。

  • 和 sleep() 方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。
  • yield() 方法只是让当前线程暂停一下,重新进入就绪的线程池中。

yield() 一般使用较少。

4.Thread.join()

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 ');
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

我们在主线程调用了 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);}
  • 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

通过源码可以发现,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); }}
  • 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

上面的代码中线程 InterruptThread 有一个标志位 running,当这个标志位为 true 时才可以运行。
因此我们可以通过修改这个标志位为 false 来中断该线程。

其实 Thread 内部也为我们提供了同样的机制 :

方法名 方法介绍
public void interrupt() 试图中断调用线程,设置中断标志位为 false
public boolean isInterrupted() 返回调用线程是否被中断
public static boolean interrupted() 返回当前线程是否被中断的状态值,同时将中断标志位复位(设为 false)

1.public void interrupt()

它的作用是设置标志位为 false,能否达到中断调用线程的效果,还取决于该线程是否可以响应中断(说直白些就是吃不吃这套),比如 Runnablerun() 方法就无法响应中断。

因此我们对执行 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); }}
  • 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

这两种方式创建的线程,在调用 thread.interrupt() 方法后仍然会继续执行!

这时就需要用到上面 Thread 提供的第二个关于中断的方法 isInterrupted() 了。

2.public boolean 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); }}
  • 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

上述代码中 两个线程都使用 isInterrupted 作为循环执行任务的条件,其中 SleepThread 方法调用了 Thread.sleep,这个方法的会响应中断,抛出异常。

运行结果如下:

可以看到:

  • 线程中,在抛出 InterruptedException 前 JVM 的确会重置中断标志位为 false
  • 这将导致以 isInterrupted 方法作为循环执行任务的线程无法正确中断

3.public static boolean interrupted()

Thread.interrupted() 方法是一个静态方法,它会返回调用线程(而不是被调用线程)的中断标志位,返回后重置中断标志位。

因此 Thread.interrupted() 第二次调用永远返回 false。

源码:

public static boolean interrupted() { return currentThread().isInterrupted(true);}
  • 1
  • 2
  • 3

总结

这篇文章总结了 线程的基本概念和关键方法,还有一些不建议使用的方法没有介绍,是因为它们有很多副作用,比如 suspend() 方法在调用后虽然线程会进入休眠状态,却不会释放资源,很容易引发死锁问题;同样,stop() 方法终结一个线程时无法保证这个线程有机会释放资源,也会导致一些不确定问题。

我们可以通过下面的图片整体分析线程的生命周期和主要方法:

Thanks

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
new Thread(new Runnable(){})
Java 如何实现线程间通信?
线程-1、创建线程的方式及实现
Java并发编程之线程的创建
一个JDK线程池BUG引发的GC机制思考
【JAVA并发第二篇】Java线程的创建与运行,线程状态与常用方法
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服