线程与任务(一)
并发的本质是多个线程同时处理某个任务1,不同于进程,线程可以访问同一共享资源(临界资源),当程序不够健壮时,使用多线程就可能带来问题,这是要反复讨论并发的原因之一。
在Java中,必须明白一点:线程由Thread类启动,但Thread类并不执行任何操作,它只是驱动赋予它的任务。因此将线程与任务的概念区分开,有利于理解并发。
实际上,开发过程中线程与任务(的联系)被隔离的更加明显,往往不需要显式地声明(创建)线程,然后将任务(声明任务是必须的)分配给线程,并由线程负责驱动( allocate task to thread to execute ),这一过程通常由线程池完成 。
任务 #
任务是由线程驱动的,因此声明任务然后将其交给线程即可。
可以使用Runnable2接口来声明任务,Runnable是一个函数式接口,定义了一个run()方法,因此常见的创建线程的方式就是:
1new Thread(()->{
2 //do some thing
3})
Thread
类实际上实现了Runnable
接口。
将其还原为普通类,那就是一个实现了Runnable接口的类可以作为任务分配给线程,重要的是你需要定义好“任务要做什么”——重写run()方法:
1class LiftOff implements Runnable {
2 private static int taskCount = 0;
3 protected int countDown = 10;
4 private final int id = taskCount++;
5
6 public LiftOff() {
7 }
8
9 public LiftOff(int countDown) {
10 this.countDown = countDown;
11 }
12
13 public String status() {
14 return "#" + id + "(" + (countDown > 0 ? countDown : "LiftOff!") + "), ";
15 }
16
17 // 线程运行的核心代码
18 @Override
19 public void run() {
20 while (countDown-- > 0) {
21 System.out.print(status());
22 // 线程调度
23 Thread.yield();
24 }
25 }
26}
上例中的LiftOff类实现了Runnable接口,但是你无法再将其转化为lambda,因为其是一个“更为丰富的类”:有区分实例的id,有构造器以及实例方法。
通常,run()方法被设计为某种形式的循环甚至无限循环。
Thread.yield()
是Java的线程调度机制之一,它声明“当前线程可以让出CPU时间,其他线程需要运行的就去运行吧”,遗憾的是它仅仅是一个建议,其他线程不一定真的会获取CPU时间并运行。
因此,从Runnable导出的类,除了必须声明run()方法之外,其不会产生任何的线程能力,要实现线程行为,必须显式地将其分配给线程。
new LiftOff().run()
可以直接调用,但这并不会开启一个单独线程,而是在当前线程中顺序执行的。可以将
Runnable
接口理解为必需声明的任务。
线程 #
Thread即线程。将Runnable转为 工作任务 的传统方法就是将其提交给Thread类构造器:
1private static void single() {
2 Thread t = new Thread(new LiftOff());
3 t.start();
4 System.out.println("waiting for liftoff");
5}
6/* output:
7waiting for liftoff
8#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(LiftOff!),
9*///:~
从输出可以看到,start()
迅速地返回了,而由start()
开启的新线程的工作任务还在执行,此例中,main线程(主线程)与LiftOff.run()线程“同时”执行。
可以很容易地利用循环创建多个线程去驱动更多任务3:
1static void multi() {
2 for (int i = 0; i < 5; i++) {
3 new Thread(new LiftOff()).start();
4 }
5 System.out.println("waiting for liftoff");
6}
7/* output:(sample)
8#1(9), #4(9), waiting for liftoff
9#3(9), #2(9), #0(9), #2(8), #0(8), #3(8), #4(8), #1(8), #4(7), #3(7), #4(6), #2(7), #0(7), #2(6), #4(5), #3(6), #1(7), #3(5), #4(4), #2(5), #0(6), #2(4), #4(3), #3(4), #1(6), #3(3), #4(2), #2(3), #0(5), #2(2), #4(1), #3(2), #1(5), #3(1), #4(LiftOff!), #2(1), #0(4), #2(LiftOff!), #3(LiftOff!), #1(4), #0(3), #0(2), #1(3), #0(1), #1(2), #0(LiftOff!), #1(1), #1(LiftOff!),
10*///:~
可以看到,不同任务的执行时混乱无序的,这是由线程调度自动控制的。
线程生命周期 #
生命周期 | 描述 |
---|---|
NEW | 线程被创建。 |
RUNNABLE | 调用start() 方法之后,这个线程可能在或不在运行,因为其要等等CPU时间。 |
BLOCKED | 当一个线程尝试获取 |
WAITING | 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。如调用Object.wait() 、Thread.join() 、Thread.sleep() 方法时,或等待Lock/Condition时。这个状态下的线程响应中断。 |
TIMED_WAITING | 带有超时参数的方法调用时会让线程进入超时等待。这个状态下的线程响应中断。 |
TERMINATED | 1)run() 方法正常退出,2)没有捕获的异常终止了run() 方法。 |
线程优先级 #
线程的优先级将线程的重要性传递给调度器,尽管CPU处理线程的顺序是不确定的,但是调度器倾向于优先让优先级高的线程执行4。
Java语言中,每个线程都有一个优先级,默认情况下,一个线程的优先级继承自其父线程。
在绝大多数时间里,线程都应该以默认优先级在运行,试图利用优先级操纵线程是愚蠢的行为。
1public class SimplePrioroites implements Runnable {
2 private int countDown = 2;
3 private volatile double d;
4 private int priority;
5
6 public SimplePrioroites(int priority) {
7 this.priority = priority;
8 }
9
10 @Override
11 public String toString() {
12 return Thread.currentThread() + ": " + countDown;
13 }
14
15 @Override
16 public void run() {
17 Thread.currentThread().setPriority(priority);
18 while (true) {
19 for (int i = 0; i < 100000; i++) {
20 // 耗时操作
21 d += (Math.PI + Math.E) / (double) i;
22 if (i % 1000 == 0) {
23 Thread.yield();
24 }
25 }
26 System.out.println(this);
27 if (--countDown == 0) {
28 return;
29 }
30 }
31 }
32
33 public static void main(String[] args) {
34 ExecutorService executorService = Executors.newCachedThreadPool();
35 for (int i = 0; i < 5 ; i++) {
36 executorService.execute(new SimplePrioroites(Thread.MIN_PRIORITY));
37 }
38 executorService.execute(new SimplePrioroites(Thread.MAX_PRIORITY));
39 executorService.shutdown();
40 }
41}
42/* output:
43Thread[pool-1-thread-2,1,main]: 2
44Thread[pool-1-thread-5,1,main]: 2
45Thread[pool-1-thread-3,1,main]: 2
46Thread[pool-1-thread-1,1,main]: 2
47Thread[pool-1-thread-4,1,main]: 2
48Thread[pool-1-thread-6,10,main]: 2
49Thread[pool-1-thread-3,1,main]: 1
50Thread[pool-1-thread-2,1,main]: 1
51Thread[pool-1-thread-5,1,main]: 1
52Thread[pool-1-thread-1,1,main]: 1
53Thread[pool-1-thread-4,1,main]: 1
54Thread[pool-1-thread-6,10,main]: 1
55*///:~
事实上,尽管设置了线程优先级,并且使用了10w次浮点运算来尝试让线程调度优先选择优先级高的线程[^8],实际上却没有收到预期效果,说明线程优先级并不能准确地调度线程。
守护线程 #
有些地方称之为后台(daemon)线程,一般在程序运行时在后台提供通用服务,守护线程在程序开发中并不是必不可少的。
当所有的非后台线程终止时,程序也会终止,同时也会杀死所有的守护线程。
不要误用守护线程,不应该使用守护线程去访问资源——一旦主程序结束,守护线程也会被杀死。
在守护线程里创建的线程一定也是守护线程。
可以使用setDaemon(true)
在start()
之前将线程设置为守护线程,同时可以使用isDaemon()
查看线程是否为守护线程:
1public class Daemons {
2 public static void main(String[] args) {
3 Thread t =new Thread(new Daemon());
4 t.setDaemon(true);
5 t.start();
6 try {
7 TimeUnit.MILLISECONDS.sleep(1);
8 } catch (InterruptedException e) {
9 e.printStackTrace();
10 }
11 }
12
13 static class Daemon implements Runnable {
14 private List<Thread> threads = new ArrayList<>();
15
16 @Override
17 public void run() {
18 for (int i = 0; i < Integer.MAX_VALUE; i++) {
19 threads.add(i, new Thread(new ThreadSpawn()));
20 threads.get(i).start();
21// System.out.println("ThreadSpawn " + i + " started");
22 System.out.println("thread["+i+"].isDaemon: "
23 + threads.get(i).isDaemon());
24 }
25// while (true) Thread.yield();
26 }
27 }
28
29 static class ThreadSpawn implements Runnable {
30 @Override
31 public void run() {
32 Thread.yield();
33 }
34 }
35}
36/* output: (sample)
37thread[0].isDaemon: true
38thread[1].isDaemon: true
39thread[2].isDaemon: true
40thread[3].isDaemon: true
41thread[4].isDaemon: true
42*///:~
上例中,Daemon
被设置为守护线程,其派生出的许多线程虽然没有被显示的声明为守护线程,其也确实是守护线程。注意到,Daemon
线程的run()
方法是一个“很大”的循环,但实际上只循环了几次,那是因为主线程结束了,守护线程于是也被杀死了。
守护线程不会执行
finally
子句,这是因为守护线程被设计为“强制关闭“的,一旦所有的非守护线程终止,守护线程就会”突然“关闭,不允许存在执行finally
块这样”优雅“的行为。The Java Virtual Machine exits when the only threads running are all daemon threads.
当只有守护线程在运行时,JVM就会退出。所以,上述示例在主线程结束休眠后立刻结束运行了。
GC垃圾收集器就是使用了守护线程的特性。
线程的中断状态 #
这是一个比较晦涩的概念。
当run()
方法正常返回或遇到异常时,线程终止运行,除此之外,无法强制终止线程5。
但是,线程有一个中断状态( interrupted state ),调用Thread.interrup()
方法时,线程的中断状态将被设置( interrupt status will be set )。
若线程调用
wait()
、join()
、sleep()
、park()
等及其重载方法进入等待,在此线程上调用interrupt()
方法将抛出中断异常(InterruptedException),并且线程不会设置中断状态。若线程先调用
intercerupt()
设置中断状态,再调用wait()
、join()
、sleep()
、park()
及其重载方法,同样会抛出中断异常 ,线程的中断状态会被清除。
下例演示了中断和休眠的关系:
1public class InterruptAndSleep {
2 public static void main(String[] args) {
3 Thread apple = new Thread(new InnerThread(), "apple");
4 Thread google = new Thread(new InnerThread(), "google");
5 apple.start();
6 google.start();
7 apple.interrupt();
8 }
9
10 static class InnerThread implements Runnable {
11 private static int count = 0;
12 private final int id = count++;
13 private int countDown = 2;
14
15 public InnerThread() {
16 }
17
18 public void info() {
19 System.out.println("id(" + id
20 + " " +
21 Thread.currentThread() + ") ");
22 }
23
24 @Override
25 public void run() {
26 try {
27 while (countDown-- > 0) {
28 // Thread.sleep(100);
29 // Java SE5 or later style
30 TimeUnit.MILLISECONDS.sleep(100);
31 info();
32 }
33 } catch (InterruptedException e) {
34 System.out.println("id(" + id
35 + " "+
36 Thread.currentThread() + ") is" + " interrupted");
37 }
38 }
39 }
40}
41/* output:
42id(0 Thread[apple,5,main]) is interrupted
43id(1 Thread[google,5,main])
44id(1 Thread[google,5,main])
45*///:~
上例说明了线程被中断(调用interrupted()方法)之后,再调用sleep()方法会抛出 InterruptedException。
但是线程被中断并不意味线程终止了,其还有再次运行的能力,将上例中run()方法的循环稍作修改:
1// try this
2while (countDown-- > 0) {
3 try {
4 TimeUnit.MILLISECONDS.sleep(100);
5 info();
6 } catch (InterruptedException e) {
7 System.out.println("id(" + id + " "
8 + Thread.currentThread() + ") is" + " interrupted");
9 }
10}
11/* output:
12id(0 Thread[apple,5,main]) is interrupted
13id(0 Thread[apple,5,main])
14id(1 Thread[google,5,main])
15id(1 Thread[google,5,main])
16*///:~
这说明,尽管调用sleep()
抛出中断异常,线程并没有终止,并且线程的中断状态还被清除了,再次循环时程序正常运行。
同样地,当线程休眠(TIMED_WAITING)时尝试中断线程的表现和上面差不多:
1public class InterruptAndSleep {
2 public static void main(String[] args) {
3 Thread apple = new Thread(new InnerThread(), "apple");
4 apple.start();
5 try {
6 Thread.sleep(200);
7 } catch (InterruptedException e) {
8 e.printStackTrace();
9 }
10 System.out.println(apple.getState());
11 apple.interrupt();
12 }
13
14 static class InnerThread implements Runnable {
15 private static int count = 0;
16 private final int id = count++;
17 private int countDown = 3;
18
19 public InnerThread() {
20 }
21
22 public void info() {
23 System.out.println("id(" + id + " " + Thread.currentThread() + ") ");
24 }
25
26 @Override
27 public void run() {
28 while (countDown-- > 0) {
29 try {
30 // Thread.sleep(100);
31 // Java SE5 or later style
32 TimeUnit.MILLISECONDS.sleep(100);
33 info();
34 } catch (InterruptedException e) {
35 // e.printStackTrace();
36 System.out.println("id(" + id
37 + " " +
38 Thread.currentThread() + ") is" + " interrupted");
39 }
40 }
41 }
42 }
43}
44/* output:
45id(0 Thread[apple,5,main])
46TIMED_WAITING
47id(0 Thread[apple,5,main]) is interrupted
48id(0 Thread[apple,5,main])
49*///:~
可以看到,当线程休眠时,调用interrupted()方法也会抛出异常,并且清除中断状态。
使用
isInterrupted()
和interrupted()
方法都可以获取线程的中断状态,二者的区别在于isInterrupted()
方法不会清除线程的中断状态( interrupted status of the thread is unaffected );但interrupted()
方法会清除线程的中断状态,且该方法是静态方法。