并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务(也被称为子任务)中的每一个都将执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,因此,单个进程可以用于多个并发执行的任务,但是你的程序使得每个任务都好像有自己的CPU一样。其底层机制是切分CPU时间,但通常你不需要考虑它。
1. 线程的生命周期
线程的生命周期主要有五种状态:新建(New)、就绪(Runable)、运行(Running)、阻塞(Blocking)和死亡(Death)。 线程在运行过程中,不可能一直占用CPU,CPU会在多个线程之前来回切换,所以线程的状态也会随时在运行和阻塞状态切换:
- 新建状态,使用 New 关键字创建线程对象,线程就处于新建状态。此时仅由JVM分配内存空间,初始化其变量。
- 就绪状态,调用Thread#start方法,线程处于就绪状态。JVM虚拟机会为其创建方法调用栈和程序计数器,等待CPU调度进入运行状态。
- 运行状态,CPU准备就绪,调用Thread#run方法执行方法体,此时线程处于运行状态。
- 阻塞状态,处于运行状态的线程失去CPU为其分配的资源,线程进入阻塞状态。
- 死亡状态,线程执行正常结束或因抛出Exception或Error而终止,线程进入死亡状态。
2. 终止线程的三个方法
- 通过线程run方法中标志位实现
- 使用Thread#stop方法实现,这个方法不推荐使用。(因为与suspend、resume一样可能出现意料以外的情况。
- 使用Thread#interrupt方法终止线程
3. 线程池(ExecutorService Executors)
Thread.yield()
线程调度器(Java线程机制的一部分,可以将CPU从一个线程转移到另一个线程的一种建议),它在声明“我已执行完生命周期中最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机”
CacheThreadPool:为每一个任务都创建一个线程。
1 | ExecutorService executor = Executors.newCachedThreadPool(); |
FixedThradPool:创建有限的线程集执行任务。
1 | ExecutorService executor = Executors.newFixedThreadPool(5); |
使用FixedThreadPool,可以限制线程的数量,不必为每个任务都固定地付出创建线程的开销。
SingleThreadExecutor:是线程数量为1的FixedThreadPool。
1 | ExecutorService executor = Executors.newSingleThreadExecutor(); |
即便向SingleThreadExecutor中提交了多个任务,这些任务依旧会排队执行,每个任务都会在下一个任务之前运行结束,所有的任务都使用相同的线程。
如何使用线程池执行任务?调用 ExecutorService#execute(Runable) 执行任务, ExecutorService#shutdown() 方法可以防止新任务提交给Executor,当前线程将继续执行 shutdown 方法被调用之前提交的任务,在任务完成时退出。
3.1 ThreadFactory 和 ThreadPoolExecutor 的概念
1 | // 创建线程的工厂 |
1 | public class DaemonThreadPoolExecutor extends ThreadPoolExecutor { |
3.2 执行任务的返回值 Callable<T>
其实Callable 和 Runnable 的区别就是多一个返回值,看一下Callable类:
1 | @FunctionalInterface |
执行任务的返回值调用 ExecutorService#submit(Callable)方法而不是 execute(Runable) 方法,返回值类型如下:
1 | <T> Future<T> submit(Callable<T> task); |
3.3 设置线程的优先级
1 | Thread.currentThread().setPriority(priority); |
常用 Thread.MIN_PRIORITY、Thread.NORM_PRIORITY 和 Thrad.MAX_PRIORITY。
###3.4 让步
Thread.yield() 方法是线程调度机制的一个暗示,表示所在线程的工作已经做得差不多了,可以让别的线程使用CPU了。这个暗示将通过调用yield()方法来作出(不过这只是一个暗示,没有任何机制保证它将会被采纳)。
3.5 后台线程
后台(daemon)线程是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序不可或缺的部分。因此,当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。
1 | Thread daemon = new Thread(new SimpleDaemons()); |
如果一个线程是后台线程,那么由这个线程创建的所有线程都属于后台线程。
后台线程中不执行 finally 语句的情况下就会终止其 run() 方法。
1 | class ADaemon implements Runnable{ |
3.6 join() 方法
当一个线程例如 Joiner 线程中调用另一个线程 Sleeper#join() 方法,那么 Joiner 线程将会被挂起,知道目标线程 Sleeper 执行完毕才恢复(即 Sleeper#isAlive() 方法返回值为 false )。join() 方法可以携带一个超时参数,若指定时间内目标线程没有执行完毕,会返回挂起的线程内执行。调用Thread#interrupt() 方法同样可以中断目标线程回到挂起线程。
3.7 线程的应用
Java使用线程的应用之一就是实现响应式界面,一边可以在线程中计算,一边可以响应用户输入或者操作界面。如下所示:
1 | class UnresponsiveUI{ |
如果使用 UnresponsiveUI 类,那么界面会进入死循环,无法响应用户输入。而对于 ResponsiveUI 类来说,计算耗时操作会放在后台线程中,main 方法负责响应用户输入,在控制台输入任意字符,按下回车程序将自动退出。
3.8 线程异常捕获
线程异常无法通过 try…catch… 语句捕获,线程中的异常会直接输入到控制台,例如
1 | public class ExceptionThread implements Runnable { |
可以通过 Thread#setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler) 方法实现对异常的捕获
1 | class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { |
4. 共享受限资源
Java的自增运算符自身需要多个步骤,并且在递增过程中任务可能会被线程机制挂起——也就是说 Java 的自增运算符不是原子性的操作。
4.1 线程锁
声明 synchronized 方法的方式:
1 | synchronized void f(){/* ... */} |
所有的对象对自动含有单一的锁。当在对象上调用任意 synchronized 方法的时候,此对象会被加锁,这时该对象上的其他 synchronized 方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。对于前面的方法,如果某个任务对象调用了 f() ,对于同一个对象而言,就只有等到 f() 调用结束并释放了锁之后,其他任务才能调用 f() 和 g() 。所以,对于某个特定对象而言,其所有 synchronized 方法共享一个锁,这可以被用来防止多个任务同时访问被编码为对象内存。
注意,在使用并发时,将域设置为 private
是非常重要的,否则,synchronized 关键字就不能防止其他任务直接访问,这样就会产生冲突。
1 | public class EventGenerator extends IntGenerator { |
synchronized 同步控制块,在调用synchronized控制代码段时,必须获取到syncObject对象的锁。如果其他线程已经得到这个锁,那么就得等到这个锁释放之后才可以继续调用。
1 | synchronized(syncObject){ |
使用同步控制块而不是对整个方法进行同步的原因是,同步代码块对象不加锁时间更长,使得其他线程更多地访问(在安全的情况下尽可能多)。
4.2 使用显式的 Lock 对象
Java SE5的java.util.concurrent
类库还包含有定义在java.util.concurrent.locks
中的显式的互斥机制。Lock对象必须被显式地创建、锁定和释放。
1 | public class MutexEventGennerator extends IntGenerator { |
注意在使用synchronized关键字时不能尝试获取锁且最终会获取失败。
4.3 原子性与易变性
关于Java线程,经常的错误性认知“ 原子操作不需要进行同步控制 ”。原子操作是不能被线程调度机制中断的操作。
volatile关键字确保了应用的可见性,如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作都可以看到这个修改。volatile 域会立即被写入到主存中(而不是各个线程的工作线程),而读取操作就发生在主存中。非volatile域上的原子操作不必刷新到主存中去,因此其他读取该域的任务看不到这个新值。同步操作synchronized
也会导致在主存中刷新,因此如果一个域完全由synchronized方法或语句来防护,那就不必将其设置为volatile。
volatile 关键字不适用的场景:
- 当一个域的值依赖于它之前的值时(例如递增一个计数器)
- 当一个域的值受其他域的值的限制(例如Range类的lower和upper边界就必须遵循lower<=upper的限制)
推荐使用synchronized关键字保证并发的正确性,使用volatile关键字并不能保证所有操作的原子性。
4.4 原子类
AtomicInteger、AtomicLong、AtomicReference,可以通过使用AtomicInteger而消除synchronized 关键字。
1 | public class AtomicIntegerTest implements Runnable { |
4.5 线程本地存储
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。因此,如果你有5个线程都要使用变量x所表示的对象,那线程本地存储就会生成5个用于x的不同的存储块。主要是,它们使得你可以将状态与线程关联起来。创建和管理线程本地存储可以由java.lang.ThreadLocal
类来实现,如下所示:
1 | class Accessor implements Runnable{ |
ThreadLocal对象通常用作静态域存储。在创建ThreadLocal时,你只能通过get() 和 set() 来访问该对象的内容。
5 终结任务
5.2 在阻塞时终结
线程状态:新建、就绪、阻塞、死亡
参考资料