线程与任务(一)

线程与任务(一)

并发的本质是多个线程同时处理某个任务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()方法之外,其不会产生任何的线程能力,要实现线程行为,必须显式地将其分配给线程

  1. new LiftOff().run()可以直接调用,但这并不会开启一个单独线程,而是在当前线程中顺序执行的。

  2. 可以将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带有超时参数的方法调用时会让线程进入超时等待。这个状态下的线程响应中断。
TERMINATED1)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()方法会清除线程的中断状态,且该方法是静态方法



  1. 多处理器下尤其如此,单处理器下Java的调度机制是“抢占式”的,谁获取CPU时间片谁运行。 ↩︎

  2. 该系列后续文章会提到,这并不是创建任务的唯一方式。 ↩︎

  3. 此例中,只有一个主线程去创建LiftOff线程,如果有多个主线程去创建LiftOff线程,那么可能就会出现重复id的LiftOff实例。 ↩︎

  4. yield()方法一样,倾向性并不是绝对的。 ↩︎

  5. 早期版本中,可以使用stop()方法终止线程,这个方法已经过时了。 ↩︎