1.进程
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。
某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
2.进程 vs 线程
进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。
具体采用哪种方式,要考虑到进程和线程的特点。
和多线程相比,多进程的缺点在于:

  • 创建进程比创建线程开销大,尤其是在Windows系统上;
  • 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。

而多进程的优点在于:
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
3.Java多线程
Java多线程编程的特点又在于:

  • 多线程模型是Java程序最基本的并发模型;
  • 后续读写网络、数据库、Web开发等都依赖Java多线程模型。

    1.创建新线程

    Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。
    方法一:从Thread派生一个自定义类,然后覆写run()方法: ```java public class Main { public static void main(String[] args) {
    1. Thread t = new MyThread();
    2. t.start(); // 启动新线程
    } }

class MyThread extends Thread { @Override public void run() { System.out.println(“start new thread!”); } }

  1. 方法二:创建Thread实例时,传入一个Runnable实例:
  2. ```java
  3. public class Main {
  4. public static void main(String[] args) {
  5. Thread t = new Thread(new MyRunnable());
  6. t.start(); // 启动新线程
  7. }
  8. }
  9. class MyRunnable implements Runnable {
  10. @Override
  11. public void run() {
  12. System.out.println("start new thread!");
  13. }
  14. }

或者用Java8引入的lambda语法进一步简写为:

  1. public class Main {
  2. public static void main(String[] args) {
  3. Thread t = new Thread(() -> {
  4. System.out.println("start new thread!");
  5. });
  6. t.start(); // 启动新线程
  7. }
  8. }

1.线程的优先级

可以对线程设定优先级,设定优先级的方法是:

  1. Thread.setPriority(int n) // 1~10, 默认值5

优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
Java用Thread对象表示一个线程,通过调用start()启动一个新线程;
一个线程对象只能调用一次start()方法;
线程的执行代码写在run()方法中;
线程调度由操作系统决定,程序本身无法决定调度顺序;
Thread.sleep()可以把当前线程暂停一段时间。

2.线程的状态

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

当线程启动后,它可以在Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。
线程终止的原因有:

  • 线程正常终止:run()方法执行到return语句返回;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

Java线程对象Thread的状态包括:New、Runnable、Blocked、Waiting、Timed Waiting和Terminated;
通过对另一个线程对象调用join()方法可以等待其执行结束;
可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
对已经运行结束的线程调用join()方法会立刻返回。

3.中断线程

对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException;
目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程;
通过标志位判断需要正确使用volatile关键字;
volatile关键字解决了共享变量在线程间的可见性问题。

4.守护线程

守护线程(Daemon Thread)。
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
因此,JVM退出时,不必关心守护线程是否已结束。
如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

  1. Thread t = new MyThread();
  2. t.setDaemon(true);
  3. t.start();

守护线程是为其他线程服务的线程;
所有非守护线程都执行完毕后,虚拟机退出;
守护线程不能持有需要关闭的资源(如打开文件等)。

5.线程同步

原子操作是指不能被中断的一个或一系列操作。
多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待。
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
可见,保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:

  1. synchronized(Counter.lock) { // 获取锁
  2. ...
  3. } // 释放锁

它表示用Counter.lock实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { … }代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized语句块结束会自动释放锁。这样一来,对Counter.count变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是0。
使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。
我们来概括一下如何使用synchronized:

  1. 找出修改共享变量的线程代码块;
  2. 选择一个共享实例作为锁;
  3. 使用synchronized(lockObject) { … }。

在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁:

  1. public void add(int m) {
  2. synchronized (obj) {
  3. if (m < 0) {
  4. throw new RuntimeException();
  5. }
  6. this.value += m;
  7. } // 无论有无异常,都会在此释放锁
  8. }

1.不需要synchronized的操作

JVM规范定义了几种原子操作:

  • 基本类型(long和double除外)赋值,例如:int n = m;
  • 引用类型赋值,例如:List list = anotherList。

long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。
多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
注意加锁对象必须是同一个实例;
对JVM定义的单个原子操作不需要同步。

6.同步方法

  1. public void add(int n) {
  2. synchronized(this) {
  3. count += n;
  4. }
  5. }

synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行。
如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe),Java标准库的java.lang.StringBuffer也是线程安全的。
还有一些不变类,例如String,Integer,LocalDate,它们的所有成员变量都是final,多线程同时访问时只能读不能写,这些不变类也是线程安全的。
最后,类似Math这些只提供静态方法,没有成员变量的类,也是线程安全的。
除了上述几种少数情况,大部分类,例如ArrayList,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么ArrayList是可以安全地在线程间共享的。
没有特殊说明时,一个类默认是非线程安全的。
当我们锁住的是this实例时,实际上可以用synchronized修饰这个方法。下面两种写法是等价的:

  1. public void add(int n) {
  2. synchronized(this) { // 锁住this
  3. count += n;
  4. } // 解锁
  5. }
  1. public synchronized void add(int n) { // 锁住this
  2. count += n;
  3. } // 解锁

因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。
如果对一个静态方法添加synchronized修饰符,对static方法添加synchronized,锁住的是该类的Class实例。

  1. synchronized(Counter.class) {
  2. ...
  3. }

用synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是this;
通过合理的设计和数据封装可以让一个类变为“线程安全”;
一个类没有特殊说明,默认不是thread-safe;
多线程能否安全访问某个非线程安全的实例,需要具体问题具体分析。

7.死锁

ava的线程锁是可重入的锁。
什么是可重入的锁?
JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。

1.死锁

一个线程可以获取一个锁后,再继续获取另一个锁。例如:

  1. public void add(int m) {
  2. synchronized(lockA) { // 获得lockA的锁
  3. this.value += m;
  4. synchronized(lockB) { // 获得lockB的锁
  5. this.another += m;
  6. } // 释放lockB的锁
  7. } // 释放lockA的锁
  8. }
  9. public void dec(int m) {
  10. synchronized(lockB) { // 获得lockB的锁
  11. this.another -= m;
  12. synchronized(lockA) { // 获得lockA的锁
  13. this.value -= m;
  14. } // 释放lockA的锁
  15. } // 释放lockB的锁
  16. }

在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行add()和dec()方法时:

  • 线程1:进入add(),获得lockA;
  • 线程2:进入dec(),获得lockB。

随后:

  • 线程1:准备获得lockB,失败,等待中;
  • 线程2:准备获得lockA,失败,等待中。

此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。
那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA,再获取lockB的顺序,改写dec()方法如下:

  1. public void dec(int m) {
  2. synchronized(lockA) { // 获得lockA的锁
  3. this.value -= m;
  4. synchronized(lockB) { // 获得lockB的锁
  5. this.another -= m;
  6. } // 释放lockB的锁
  7. } // 释放lockA的锁
  8. }

Java的synchronized锁是可重入锁;
死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待;
避免死锁的方法是多线程获取锁的顺序要一致。

8.wait和notify

wait和notify用于多线程协调运行:

  • 在synchronized内部可以调用wait()使线程进入等待状态;
  • 必须在已获得的锁对象上调用wait()方法;
  • 在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程;
  • 必须在已获得的锁对象上调用notify()或notifyAll()方法;
  • 已唤醒的线程还需要重新获得锁后才能继续执行。

    9.ReentrantLock

    从Java 5开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。
    我们知道Java语言直接提供了synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。
    java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁,我们来看一下传统的synchronized代码:

    1. public class Counter {
    2. private int count;
    3. public void add(int n) {
    4. synchronized(this) {
    5. count += n;
    6. }
    7. }
    8. }

    如果用ReentrantLock替代,可以把代码改造为:

    1. public class Counter {
    2. private final Lock lock = new ReentrantLock();
    3. private int count;
    4. public void add(int n) {
    5. lock.lock();
    6. try {
    7. count += n;
    8. } finally {
    9. lock.unlock();
    10. }
    11. }
    12. }

    因为synchronized是Java语言层面提供的语法,所以我们不需要考虑异常,而ReentrantLock是Java代码实现的锁,我们就必须先获取锁,然后在finally中正确释放锁。
    顾名思义,ReentrantLock是可重入锁,它和synchronized一样,一个线程可以多次获取同一个锁。
    和synchronized不同的是,ReentrantLock可以尝试获取锁:

    1. if (lock.tryLock(1, TimeUnit.SECONDS)) {
    2. try {
    3. ...
    4. } finally {
    5. lock.unlock();
    6. }
    7. }

    上述代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。
    所以,使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。
    ReentrantLock可以替代synchronized进行同步;
    ReentrantLock获取锁更安全;
    必须先获取到锁,再进入try {…}代码块,最后使用finally保证释放锁;
    可以使用tryLock()尝试获取锁。

    10.Condition

    使用ReentrantLock比直接使用synchronized更安全,可以替代synchronized进行线程同步。
    但是,synchronized可以配合wait和notify实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock我们怎么编写wait和notify的功能呢?
    答案是使用Condition对象来实现wait和notify的功能。
    我们仍然以TaskQueue为例,把前面用synchronized实现的功能通过ReentrantLock和Condition来实现:

    1. class TaskQueue {
    2. private final Lock lock = new ReentrantLock();
    3. private final Condition condition = lock.newCondition();
    4. private Queue<String> queue = new LinkedList<>();
    5. public void addTask(String s) {
    6. lock.lock();
    7. try {
    8. queue.add(s);
    9. condition.signalAll();
    10. } finally {
    11. lock.unlock();
    12. }
    13. }
    14. public String getTask() {
    15. lock.lock();
    16. try {
    17. while (queue.isEmpty()) {
    18. condition.await();
    19. }
    20. return queue.remove();
    21. } finally {
    22. lock.unlock();
    23. }
    24. }
    25. }

    可见,使用Condition时,引用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例。
    Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的:

  • await()会释放当前锁,进入等待状态;

  • signal()会唤醒某个等待线程;
  • signalAll()会唤醒所有等待线程;
  • 唤醒线程从await()返回后需要重新获得锁。

此外,和tryLock()类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来:

  1. if (condition.await(1, TimeUnit.SECOND)) {
  2. // 被其他线程唤醒
  3. } else {
  4. // 指定时间内没有被其他线程唤醒
  5. }

可见,使用Condition配合Lock,我们可以实现更灵活的线程同步。
Condition可以替代wait和notify;
Condition对象必须从Lock对象获取。

11.ReadWriteLock

允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待:
使用ReadWriteLock可以解决这个问题,它保证:

  • 只允许一个线程写入(其他线程既不能写入也不能读取);
  • 没有写入时,多个线程允许同时读(提高性能)。

用ReadWriteLock实现这个功能十分容易。我们需要创建一个ReadWriteLock实例,然后分别获取读锁和写锁:

  1. public class Counter {
  2. private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
  3. private final Lock rlock = rwlock.readLock();
  4. private final Lock wlock = rwlock.writeLock();
  5. private int[] counts = new int[10];
  6. public void inc(int index) {
  7. wlock.lock(); // 加写锁
  8. try {
  9. counts[index] += 1;
  10. } finally {
  11. wlock.unlock(); // 释放写锁
  12. }
  13. }
  14. public int[] get() {
  15. rlock.lock(); // 加读锁
  16. try {
  17. return Arrays.copyOf(counts, counts.length);
  18. } finally {
  19. rlock.unlock(); // 释放读锁
  20. }
  21. }
  22. }

把读写操作分别用读锁和写锁来加锁,在读取时,多个线程可以同时获得读锁,这样就大大提高了并发读的执行效率。
使用ReadWriteLock时,适用条件是同一个数据,有大量线程读取,但仅有少数线程修改。
例如,一个论坛的帖子,回复可以看做写入操作,它是不频繁的,但是,浏览可以看做读取操作,是非常频繁的,这种情况就可以使用ReadWriteLock。
使用ReadWriteLock可以提高读取效率:

  • ReadWriteLock只允许一个线程写入;
  • ReadWriteLock允许多个线程在没有写入时同时读取;
  • ReadWriteLock适合读多写少的场景。

    12.StampedLock

    Java 8引入了新的读写锁:StampedLock。
    StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。
    乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

    1. public class Point {
    2. private final StampedLock stampedLock = new StampedLock();
    3. private double x;
    4. private double y;
    5. public void move(double deltaX, double deltaY) {
    6. long stamp = stampedLock.writeLock(); // 获取写锁
    7. try {
    8. x += deltaX;
    9. y += deltaY;
    10. } finally {
    11. stampedLock.unlockWrite(stamp); // 释放写锁
    12. }
    13. }
    14. public double distanceFromOrigin() {
    15. long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
    16. // 注意下面两行代码不是原子操作
    17. // 假设x,y = (100,200)
    18. double currentX = x;
    19. // 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
    20. double currentY = y;
    21. // 此处已读取到y,如果没有写入,读取是正确的(100,200)
    22. // 如果有写入,读取是错误的(100,400)
    23. if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
    24. stamp = stampedLock.readLock(); // 获取一个悲观读锁
    25. try {
    26. currentX = x;
    27. currentY = y;
    28. } finally {
    29. stampedLock.unlockRead(stamp); // 释放悲观读锁
    30. }
    31. }
    32. return Math.sqrt(currentX * currentX + currentY * currentY);
    33. }
    34. }

    和ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取。注意到首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。
    可见,StampedLock把读锁细分为乐观读和悲观读,能进一步提升并发效率。但这也是有代价的:一是代码更加复杂,二是StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁。
    StampedLock还提供了更复杂的将悲观读锁升级为写锁的功能,它主要使用在if-then-update的场景:即先读,如果读的数据满足条件,就返回,如果读的数据不满足条件,再尝试写。
    StampedLock提供了乐观读锁,可取代ReadWriteLock以进一步提升并发性能;
    StampedLock是不可重入锁。

    13.Concurrent集合

    通过ReentrantLock和Condition实现了一个BlockingQueue:

    1. public class TaskQueue {
    2. private final Lock lock = new ReentrantLock();
    3. private final Condition condition = lock.newCondition();
    4. private Queue<String> queue = new LinkedList<>();
    5. public void addTask(String s) {
    6. lock.lock();
    7. try {
    8. queue.add(s);
    9. condition.signalAll();
    10. } finally {
    11. lock.unlock();
    12. }
    13. }
    14. public String getTask() {
    15. lock.lock();
    16. try {
    17. while (queue.isEmpty()) {
    18. condition.await();
    19. }
    20. return queue.remove();
    21. } finally {
    22. lock.unlock();
    23. }
    24. }
    25. }

    BlockingQueue的意思就是说,当一个线程调用这个TaskQueue的getTask()方法时,该方法内部可能会让线程变成等待状态,直到队列条件满足不为空,线程被唤醒后,getTask()方法才会返回。
    因为BlockingQueue非常有用,所以我们不必自己编写,可以直接使用Java标准库的java.util.concurrent包提供的线程安全的集合:ArrayBlockingQueue。
    除了BlockingQueue外,针对List、Map、Set、Deque等,java.util.concurrent包也提供了对应的并发集合类。我们归纳一下:

interface non-thread-safe thread-safe
List ArrayList CopyOnWriteArrayList
Map HashMap ConcurrentHashMap
Set HashSet / TreeSet CopyOnWriteArraySet
Queue ArrayDeque / LinkedList ArrayBlockingQueue / LinkedBlockingQueue
Deque ArrayDeque / LinkedList LinkedBlockingDeque

使用这些并发集合与使用非线程安全的集合类完全相同。我们以ConcurrentHashMap为例:

  1. Map<String, String> map = new ConcurrentHashMap<>();
  2. // 在不同的线程读写:
  3. map.put("A", "1");
  4. map.put("B", "2");
  5. map.get("A", "1");

因为所有的同步和加锁的逻辑都在集合内部实现,对外部调用者来说,只需要正常按接口引用,其他代码和原来的非线程安全代码完全一样。即当我们需要多线程访问时,把:

  1. Map<String, String> map = new HashMap<>();

改为:

  1. Map<String, String> map = new ConcurrentHashMap<>();

使用java.util.concurrent包提供的线程安全的并发集合可以大大简化多线程编程:
多线程同时读写并发集合是安全的;
尽量使用Java标准库提供的并发集合,避免自己编写同步代码。

14.Atomic

Java的java.util.concurrent包除了提供底层锁、并发集合外,还提供了一组原子操作的封装类,它们位于java.util.concurrent.atomic包。
我们以AtomicInteger为例,它提供的主要操作有:

  • 增加值并返回新值:int addAndGet(int delta)
  • 加1后返回新值:int incrementAndGet()
  • 获取当前值:int get()
  • 用CAS方式设置:int compareAndSet(int expect, int update)

Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。
如果我们自己通过CAS编写incrementAndGet(),它大概长这样:

  1. public int incrementAndGet(AtomicInteger var) {
  2. int prev, next;
  3. do {
  4. prev = var.get();
  5. next = prev + 1;
  6. } while ( ! var.compareAndSet(prev, next));
  7. return next;
  8. }

CAS是指,在这个操作中,如果AtomicInteger的当前值是prev,那么就更新为next,返回true。如果AtomicInteger的当前值不是prev,就什么也不干,返回false。通过CAS操作并配合do … while循环,即使其他线程修改了AtomicInteger的值,最终的结果也是正确的。
使用java.util.concurrent.atomic提供的原子操作可以简化多线程编程:

  • 原子操作实现了无锁的线程安全;
  • 适用于计数器,累加器等。

    15.线程池

    那么我们就可以把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池。
    简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
    Java标准库提供了ExecutorService接口表示线程池,它的典型用法如下:

    1. // 创建固定大小的线程池:
    2. ExecutorService executor = Executors.newFixedThreadPool(3);
    3. // 提交任务:
    4. executor.submit(task1);
    5. executor.submit(task2);
    6. executor.submit(task3);
    7. executor.submit(task4);
    8. executor.submit(task5);

    因为ExecutorService只是接口,Java标准库提供的几个常用实现类有:

  • FixedThreadPool:线程数固定的线程池;

  • CachedThreadPool:线程数根据任务动态调整的线程池;
  • SingleThreadExecutor:仅单线程执行的线程池。

想创建指定动态范围的线程池,可以这么写:

  1. int min = 4;
  2. int max = 10;
  3. ExecutorService es = new ThreadPoolExecutor(min, max,
  4. 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

1.ScheduledThreadPool

还有一种任务,需要定期反复执行,例如,每秒刷新证券价格。这种任务本身固定,需要反复执行的,可以使用ScheduledThreadPool。放入ScheduledThreadPool的任务可以定期反复执行。
创建一个ScheduledThreadPool仍然是通过Executors类:

  1. ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);

我们可以提交一次性任务,它会在指定延迟后只执行一次:

  1. // 1秒后执行一次性任务:
  2. ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);

如果任务以固定的每3秒执行,我们可以这样写:

  1. // 2秒后开始执行定时任务,每3秒执行:
  2. ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);

如果任务以固定的3秒为间隔执行,我们可以这样写:

  1. // 2秒后开始执行定时任务,以3秒为间隔执行:
  2. ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);

JDK提供了ExecutorService实现了线程池功能:

  • 线程池内部维护一组线程,可以高效执行大量小任务;
  • Executors提供了静态方法创建不同类型的ExecutorService;
  • 必须调用shutdown()关闭ExecutorService;
  • ScheduledThreadPool可以定期调度多个任务。

    16.Future

    Java标准库还提供了一个Callable接口,和Runnable接口比,它多了一个返回值:

    1. class Task implements Callable<String> {
    2. public String call() throws Exception {
    3. return longTimeCalculation();
    4. }
    5. }

    并且Callable接口是一个泛型接口,可以返回指定类型的结果。
    一个Future类型的实例代表一个未来能获取结果的对象。
    一个Future接口表示一个未来可能会返回的结果,它定义的方法有:

  • get():获取结果(可能会等待)

  • get(long timeout, TimeUnit unit):获取结果,但只等待指定的时间;
  • cancel(boolean mayInterruptIfRunning):取消当前任务;
  • isDone():判断任务是否已完成。

对线程池提交一个Callable任务,可以获得一个Future对象;
可以用Future在将来某个时刻获取结果。

17.CompletableFuture

CompletableFuture可以指定异步处理流程:

  • thenAccept()处理正常结果;
  • exceptional()处理异常结果;
  • thenApplyAsync()用于串行化另一个CompletableFuture;
  • anyOf()和allOf()用于并行化多个CompletableFuture。

    18.ForkJoin

    Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行。
    Fork/Join任务的原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务。
    Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。
    ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction。
    使用Fork/Join模式可以进行并行计算以提高效率。

    19.ThreadLocal

    这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。
    Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。
    ThreadLocal实例通常总是以静态字段初始化如下:
    1. static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
    它的典型使用方式如下:
    1. void processUser(user) {
    2. try {
    3. threadLocalUser.set(user);
    4. step1();
    5. step2();
    6. } finally {
    7. threadLocalUser.remove();
    8. }
    9. }
    通过设置一个User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例: ```java void step1() { User u = threadLocalUser.get(); log(); printUser(); }

void log() { User u = threadLocalUser.get(); println(u.name); }

void step2() { User u = threadLocalUser.get(); checkUser(u.id); }

  1. 注意到普通的方法调用一定是同一个线程执行的,所以,step1()、step2()以及log()方法内,threadLocalUser.get()获取的User对象是同一个实例。<br />实际上,可以把ThreadLocal看成一个全局Map<Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key
  2. ```java
  3. Object threadLocalValue = threadLocalMap.get(Thread.currentThread());

因此,ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。
最后,特别注意ThreadLocal一定要在finally中清除:

  1. try {
  2. threadLocalUser.set(user);
  3. ...
  4. } finally {
  5. threadLocalUser.remove();
  6. }

这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。
为了保证能释放ThreadLocal关联的实例,我们可以通过AutoCloseable接口配合try (resource) {…}结构,让编译器自动为我们关闭。例如,一个保存了当前用户名的ThreadLocal可以封装为一个UserContext对象:

  1. public class UserContext implements AutoCloseable {
  2. static final ThreadLocal<String> ctx = new ThreadLocal<>();
  3. public UserContext(String user) {
  4. ctx.set(user);
  5. }
  6. public static String currentUser() {
  7. return ctx.get();
  8. }
  9. @Override
  10. public void close() {
  11. ctx.remove();
  12. }
  13. }

使用的时候,我们借助try (resource) {…}结构,可以这么写:

  1. try (var ctx = new UserContext("Bob")) {
  2. // 可任意调用UserContext.currentUser():
  3. String currentUser = UserContext.currentUser();
  4. } // 在此自动调用UserContext.close()方法释放ThreadLocal关联对象

这样就在UserContext中完全封装了ThreadLocal,外部代码在try (resource) {…}内部可以随时调用UserContext.currentUser()获取当前线程绑定的用户名。
ThreadLocal表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的;
ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递);
使用ThreadLocal要用try … finally结构,并在finally中清除。