多线程基础

多线程是java并发模型的一种

计算机一个任务就是一个进程,浏览器,word,进程内部子任务就是线程,如word一边打字一边拼写检查。一个进程至少包含一个线程。
进程和线程是包含关系,多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。
和多线程相比,多进程的缺点在于:
创建进程比创建线程开销大,尤其是在Windows系统上;
进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
多进程的优点在于:
多进程稳定性比多线程高,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

进程——资源分配和调度的一个独立单位
进程作为资源分配的基本单位
线程作为资源调度的基本单位,是程序的执行单元,执行路径(单线程:一条执行路径,多线程:多条执行路径)。是程序使用CPU的最基本单位。
并行是针对进程的,并发是针对线程 。

创建新线程

java程序启动(启动JVM进程)——>JVM启动主线程执行main方法——>main方法中启动其他线程
创建新线程:
Thread t = new Thread(); t.start();//启动新线程 新线程执行指定代码的方法:

  • 方法一,从Thread派生出类MyThread,然后覆写run()方法:
//main方法里
Thread t = new MyThread();
t.start();//start会在内部自动调用实例的run方法

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}
  • 方法二:创建Thread实例时,传入一个Runnable实例:(推荐使用方法二,因为java只允许单继承,但是允许实现多个接口,所以不要继承Thread类来实现多线程,万一这个类已经继承别的类呢)
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start(); // 启动新线程
    }
}
//Runnable是一个接口,它只有一个run()方法
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

线程的优先级

设定优先级的方法是:

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


优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁(但是不能保证优先级高的线程一定先执行)

线程的状态

• New:新创建,尚未执行;
• Runnable:运行中,正在执行run()代码
• Blocked:运行中的线程被阻塞(可能是IO原因等)
• Wating:运行中的线程在等待
• Time Waiting:运行中的线程执行sleep()在计时
• Terminated:线程已终止(run()执行完毕)

线程终止的原因

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

等待另一个线程直到运行结束,例如main线程启动t线程后,通过t.join()等待t线程结束后再继续运行。
t线程已经结束,对实例t调用join()会立刻返回。join(long)的重载方法也可以指定一个等待时间。

中断线程

其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。(例如中断下载进程)

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1000);
        t.interrupt(); // 中断t线程
        t.join(); // 等待t线程结束
        System.out.println("end");
    }
}

class MyThread extends Thread {
    public void run() {
        Thread hello = new HelloThread();
        hello.start(); // 启动hello线程
        try {
            hello.join(); // 等待hello线程结束
        } catch (InterruptedException e) {
            System.out.println("interrupted!");
        }
        hello.interrupt();
    }
}

class HelloThread extends Thread {
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n++;
            System.out.println(n + " hello!");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

main线程通过调用t.interrupt()从而通知t线程中断,而此时t线程正位于hello.join()的等待中,interrupt()方法会立刻结束等待并抛出InterruptedException。由于我们在t线程中捕获了InterruptedException,就准备结束t进程。在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出。

另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位(volatile修饰,线程间共享的变量)来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束。

对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException;
目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程;

volatile关键字

被volatile修饰的共享变量,就具有了以下两点特性:
1 . 保证了不同线程对该变量操作的内存可见性;
2 . 禁止指令重排序(指令冲排序:处理器为了提高程序运行效率,对输入代码进行优化,不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的,考虑数据依赖性)
指令重排序不会影响单个线程的执行,但是会影响线程并发执行的正确性。

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。

在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!(所有需要volatile关键字保证可见性)

java什么时候使用多进程 java为什么很少用多进程_多进程

守护线程

守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。创建守护线程:调用start()前,调用setDaemon(true)标记为守护线程:

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

守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

线程同步

当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。如果多个线程同时读写共享变量,会出现数据不一致的问题。
对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。

保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:

synchronized(lock) {
    n = n + 1;
}

例如

synchronized(Counter.lock) { // 获取锁
    ...
} // 释放锁
public class Main {
    public static void main(String[] args) throws Exception {
        var add = new AddThread();
        var dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static final Object lock = new Object();
    public static int count = 0;
}

class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.count += 1;
            }
        }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.count -= 1;
            }
        }
    }
}

用Counter.lock实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { … }代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized语句块结束会自动释放锁。这样一来,对Counter.count变量进行读写就不可能同时进行。
使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。
使用synchronized(无论是否有异常都会在synchronized结束处正确释放锁):
找出修改共享变量的线程代码块;
选择一个共享实例作为锁;
使用synchronized(lockObject) { … }。

JVM规范定义了几种原子操作:
基本类型(long和double除外)赋值,例如:int n = m;
引用类型赋值,例如:List list = anotherList。
long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。

单条原子操作的语句不需要同步。如果是多行赋值语句,就必须保证是同步操作,

同步方法

如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe)。Java标准库的java.lang.StringBuffer也是线程安全的。还有一些不变类,例如String,Integer,LocalDate,它们的所有成员变量都是final,多线程同时访问时只能读不能写,这些不变类也是线程安全的。
最后,类似Math这些只提供静态方法,没有成员变量的类,也是线程安全的。

大部分类,例如ArrayList,都是非线程安全的类,我们不能在多线程中修改它们。如果所有线程都只读取,不写入,那么ArrayList是可以安全地在线程间共享的。
没有特殊说明时,一个类默认是非线程安全的。

public synchronized void add(int n) { // 锁住this
    count += n;
} // 解锁

用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。

对一个静态方法添加synchronized修饰符:

public synchronized static void test(int n) {
    ...
}

对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的Class实例。

死锁

Java的线程锁是可重入的锁。
JVM允许同一个线程重复获取同一个锁(比如add方法内部调用了dec方法,两个方法都获取this锁),这种能被同一个线程反复获取的锁,就叫做可重入锁。
由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。

在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。
两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
避免死锁的方法是多线程获取锁的顺序要一致。

使用wait和notify

//对于一个任务管理器,多个线程同时往队列中添加任务,可以用synchronized加锁:
//同步方法addTask和getTask都需要获得this锁才可以执行
class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
    	this.queue.add(s);
    	this.notify(); // 唤醒在this锁等待的线程
    }
    //获取队列的第一个任务
	public synchronized String getTask() {
   	 	while (queue.isEmpty()) {
        	// 在条件不满足时,线程进入等待状态:释放this锁。
        	//wait()方法必须在当前获取的锁对象上调用,这里获取的是this锁,因此调用this.wait()。
        	//调用wait()方法后,线程进入等待状态,直到将来被其他线程唤醒,wait()方法才会返回继
        	this.wait();
        	// 重新获取this锁才能继续执行后面的代码
    }
   		return queue.remove();
	}
}

wait()方法必须在当前获取的锁对象上调用,这里获取的是this锁,因此调用this.wait()。
wait()方法的执行机制非常复杂。它不是一个普通的Java方法,而是定义在Object类的一个native方法,也就是由JVM的C代码实现的。必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。

使用notifyAll()将唤醒所有当前正在this锁等待的线程,而notify()只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。这是因为可能有多个线程正在getTask()方法内部的wait()中等待,使用notifyAll()将一次性全部唤醒。通常来说,notifyAll()更安全。

要始终在while循环中wait(),并且每次被唤醒后拿到this锁就必须再次判断:
while (queue.isEmpty()) {
this.wait();
}

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

使用ReentrantLock

从Java 5开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁。

public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count;

    public void add(int n) {
        lock.lock();
        try {
            count += n;
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock是可重入锁,ReentrantLock可以尝试获取锁:

if (lock.tryLock(1, TimeUnit.SECONDS)) {//最多等待1秒,仍未获得锁,tryLock()返回false
    try {
        ...
    } finally {
        lock.unlock();
    }
}

使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。

使用Condition

ReentrantLock锁使用Condition对象来实现wait和notify的功能。

class TaskQueue {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private Queue<String> queue = new LinkedList<>();

    public void addTask(String s) {
        lock.lock();
        try {
            queue.add(s);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String getTask() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                condition.await();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }
}

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

  • await()会释放当前锁,进入等待状态;
  • signal()会唤醒某个等待线程;
  • signalAll()会唤醒所有等待线程;
  • 唤醒线程从await()返回后需要重新获得锁。

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

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

使用ReadWriteLock

允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待。
ReadWriteLock保证:

  • 只允许一个线程写入(其他线程既不能写入也不能读取);
  • 没有写入时,多个线程允许同时读(提高性能)。
public class Counter {
    private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
    private final Lock rlock = rwlock.readLock();
    private final Lock wlock = rwlock.writeLock();
    private int[] counts = new int[10];

    public void inc(int index) {
        wlock.lock(); // 加写锁
        try {
            counts[index] += 1;
        } finally {
            wlock.unlock(); // 释放写锁
        }
    }

    public int[] get() {
        rlock.lock(); // 加读锁
        try {
            return Arrays.copyOf(counts, counts.length);
        } finally {
            rlock.unlock(); // 释放读锁
        }
    }
}

使用ReadWriteLock可以提高读取效率:
ReadWriteLock只允许一个线程写入;
ReadWriteLock允许多个线程在没有写入时同时读取;
ReadWriteLock适合读多写少的场景。

使用StampedLock

Java 8引入了新的读写锁:StampedLock。——读的过程也允许获取写锁后写入,需要检测出不一致的数据再读一遍。

public class Point {
    private final StampedLock stampedLock = new StampedLock();

    private double x;
    private double y;

    public void move(double deltaX, double deltaY) {
        long stamp = stampedLock.writeLock(); // 获取写锁
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            stampedLock.unlockWrite(stamp); // 释放写锁
        }
    }

    public double distanceFromOrigin() {
        long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
        // 注意下面两行代码不是原子操作
        // 假设x,y = (100,200)
        double currentX = x;
        // 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
        double currentY = y;
        // 此处已读取到y,如果没有写入,读取是正确的(100,200)
        // 如果有写入,读取是错误的(100,400)
        if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
            stamp = stampedLock.readLock(); // 获取一个悲观读锁
            try {
                currentX = x;
                currentY = y;
            } finally {
                stampedLock.unlockRead(stamp); // 释放悲观读锁
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

和ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取。注意到首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。

可见,StampedLock把读锁细分为乐观读和悲观读,能进一步提升并发效率。但这也是有代价的:一是代码更加复杂,二是StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁。

使用Concurrent集合

java什么时候使用多进程 java为什么很少用多进程_java_02


尽量使用Java标准库提供的并发集合,避免自己编写同步代码。

使用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。

使用java.util.concurrent.atomic提供的原子操作可以简化多线程编程:

原子操作实现了无锁的线程安全;

适用于计数器,累加器等。

使用线程池

Java标准库提供了ExecutorService接口表示线程池,它的典型用法如下:

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

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

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

创建这些线程池的方法都被封装到Executors这个类中。

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池:
        ExecutorService es = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 6; i++) {
            es.submit(new Task("" + i));
        }
        // 关闭线程池:
        es.shutdown();
    }
}

class Task implements Runnable {
    private final String name;

    public Task(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println("start task " + name);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        System.out.println("end task " + name);
    }
}

放入的任务数>线程池的线程数,同时执行线程池数量的任务,其他任务等线程空闲才会执行。shutdown()关闭线程池会先等待执行的任务完成。shutdownNow()会立即停止正在执行的任务,waitTermination()则会等待指定的时间让线程池关闭。

ScheduledThreadPool

放入ScheduledThreadPool的任务可以定期反复执行。(比如每秒刷新证券价格)
heduledThreadPool仍然是通过Executors类:ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);

// 1秒后执行一次性任务:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
// 2秒后开始执行定时任务,每3秒执行:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
// 2秒后开始执行定时任务,以3秒为间隔执行:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);

java什么时候使用多进程 java为什么很少用多进程_多进程_03


Java标准库还提供了一个java.util.Timer类,这个类也可以定期执行任务,但是,一个Timer会对应一个Thread,所以,一个Timer只能定期执行一个任务,多个定时任务必须启动多个Timer,而一个ScheduledThreadPool就可以调度多个定时任务,所以,我们完全可以用ScheduledThreadPool取代旧的Timer。

使用Future

在执行多个任务的时候,使用Java标准库提供的线程池是非常方便的。我们提交的任务只需要实现Runnable接口,就可以让线程池去执行。Runnable接口有个问题,它的方法没有返回值。如果任务需要一个返回结果,那么只能保存到变量,还要提供额外的方法读取,非常不便。所以,Java标准库还提供了一个Callable接口,和Runnable接口比,它多了一个返回值:

class Task implements Callable<String> {
    public String call() throws Exception {
        return longTimeCalculation(); 
    }
}

Callable接口是一个泛型接口,可以返回指定类型的结果。
ExecutorService.submit()方法返回了一个Future类型

ExecutorService executor = Executors.newFixedThreadPool(4); 
// 定义任务:
Callable<String> task = new Task();
// 提交任务并获得Future:
Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果:
String result = future.get(); // 可能阻塞

提交一个Callable任务后,获得一个Future对象,在主线程某个时刻调用Future对象的get()方法,就可以获得异步执行的结果。在调用get()时,如果异步任务已经完成,直接获得结果。如果异步任务还没有完成,那么get()会阻塞,直到任务完成后才返回结果。

一个Future接口表示一个未来可能会返回的结果,它定义的方法有:

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

使用CompletableFuture(没学懂,日后填坑)

使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,主线程会被迫等待。
Java8引入了CompletableFuture,针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
以获取股票价格为例,看看如何使用CompletableFuture:

// CompletableFuture
import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) throws Exception {
        // 创建异步执行任务:
        CompletableFuture<Double> cf = CompletableFuture.supplyAsync(Main::fetchPrice);
        // 如果执行成功:
        cf.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 如果执行异常:
        cf.exceptionally((e) -> {
            e.printStackTrace();
            return null;
        });
        // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
        Thread.sleep(200);
    }

    static Double fetchPrice() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        if (Math.random() < 0.3) {
            throw new RuntimeException("fetch price failed!");
        }
        return 5 + Math.random() * 20;
    }
}

创建一个CompletableFuture是通过CompletableFuture.supplyAsync()实现的,它需要一个实现了Supplier接口的对象:

public interface Supplier {
 T get();
 }

这里我们用lambda语法简化了一下,直接传入Main::fetchPrice,因为Main.fetchPrice()静态方法的签名符合Supplier接口的定义(除了方法名外)。

紧接着,CompletableFuture已经被提交给默认的线程池执行了,我们需要定义的是CompletableFuture完成时和异常时需要回调的实例。完成时,CompletableFuture会调用Consumer对象:

public interface Consumer {
 void accept(T t);
 }

异常时,CompletableFuture会调用Function对象:

public interface Function<T, R> {
 R apply(T t);
 }

CompletableFuture的优点是:

  • 异步任务结束时,会自动回调某个对象的方法;
  • 异步任务出错时,会自动回调某个对象的方法;
  • 主线程设置好回调后,不再关心异步任务的执行。

如果只是实现了异步回调机制,我们还看不出CompletableFuture相比Future的优势。CompletableFuture更强大的功能是,多个CompletableFuture可以串行执行,例如,定义两个CompletableFuture,第一个CompletableFuture根据证券名称查询证券代码,第二个CompletableFuture根据证券代码查询证券价格,这两个CompletableFuture实现串行操作如下:

// CompletableFuture
import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) throws Exception {
        // 第一个任务:
        CompletableFuture<String> cfQuery = CompletableFuture.supplyAsync(() -> {
            return queryCode("中国石油");
        });
        // cfQuery成功后继续执行下一个任务:
        CompletableFuture<Double> cfFetch = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice(code);
        });
        // cfFetch成功后打印结果:
        cfFetch.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
        Thread.sleep(2000);
    }

    static String queryCode(String name) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        return "601857";
    }

    static Double fetchPrice(String code) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        return 5 + Math.random() * 20;
    }
}

除了串行执行外,多个CompletableFuture还可以并行执行。例如,我们考虑这样的场景:

同时从新浪和网易查询证券代码,只要任意一个返回结果,就进行下一步查询价格,查询价格也同时从新浪和网易查询,只要任意一个返回结果,就完成操作:

// CompletableFuture
import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) throws Exception {
        // 两个CompletableFuture执行异步查询:
        CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> {
            return queryCode("中国石油", "https://finance.sina.com.cn/code/");
        });
        CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> {
            return queryCode("中国石油", "https://money.163.com/code/");
        });

        // 用anyOf合并为一个新的CompletableFuture:
        CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163);

        // 两个CompletableFuture执行异步查询:
        CompletableFuture<Double> cfFetchFromSina = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice((String) code, "https://finance.sina.com.cn/price/");
        });
        CompletableFuture<Double> cfFetchFrom163 = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice((String) code, "https://money.163.com/price/");
        });

        // 用anyOf合并为一个新的CompletableFuture:
        CompletableFuture<Object> cfFetch = CompletableFuture.anyOf(cfFetchFromSina, cfFetchFrom163);

        // 最终结果:
        cfFetch.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
        Thread.sleep(200);
    }

    static String queryCode(String name, String url) {
        System.out.println("query code from " + url + "...");
        try {
            Thread.sleep((long) (Math.random() * 100));
        } catch (InterruptedException e) {
        }
        return "601857";
    }

    static Double fetchPrice(String code, String url) {
        System.out.println("query price from " + url + "...");
        try {
            Thread.sleep((long) (Math.random() * 100));
        } catch (InterruptedException e) {
        }
        return 5 + Math.random() * 20;
    }
}

java什么时候使用多进程 java为什么很少用多进程_多进程_04


除了anyOf()可以实现“任意个CompletableFuture只要一个成功”,allOf()可以实现“所有CompletableFuture都必须成功”,这些组合操作可以实现非常复杂的异步流程控制。

最后我们注意CompletableFuture的命名规则:

xxx():表示该方法将继续在已有的线程中执行;

xxxAsync():表示将异步在线程池中执行。

java什么时候使用多进程 java为什么很少用多进程_多线程_05

使用ForkJoin

Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行。Fork/Join任务的原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务。
我们来看如何使用Fork/Join对大数据进行并行求和:

import java.util.Random;
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws Exception {
        // 创建2000个随机数组成的数组:
        long[] array = new long[2000];
        long expectedSum = 0;
        for (int i = 0; i < array.length; i++) {
            array[i] = random();
            expectedSum += array[i];
        }
        System.out.println("Expected sum: " + expectedSum);
        // fork/join:
        ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
        long startTime = System.currentTimeMillis();
        Long result = ForkJoinPool.commonPool().invoke(task);
        long endTime = System.currentTimeMillis();
        System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
    }

    static Random random = new Random(0);

    static long random() {
        return random.nextInt(10000);
    }
}

class SumTask extends RecursiveTask<Long> {
    static final int THRESHOLD = 500;
    long[] array;
    int start;
    int end;

    SumTask(long[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if (end - start <= THRESHOLD) {
            // 如果任务足够小,直接计算:
            long sum = 0;
            for (int i = start; i < end; i++) {
                sum += this.array[i];
                // 故意放慢计算速度:
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                }
            }
            return sum;
        }
        // 任务太大,一分为二:
        int middle = (end + start) / 2;
        System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
        SumTask subtask1 = new SumTask(this.array, start, middle);
        SumTask subtask2 = new SumTask(this.array, middle, end);
        invokeAll(subtask1, subtask2);
        Long subresult1 = subtask1.join();
        Long subresult2 = subtask2.join();
        Long result = subresult1 + subresult2;
        System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
        return result;
    }
}

Fork/Join线程池在Java标准库中就有应用。Java标准库提供的java.util.Arrays.parallelSort(array)可以进行并行排序,它的原理就是内部通过Fork/Join对大数组分拆进行并行排序,在多核CPU上就可以大大提高排序的速度。
ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction。

使用ThreadLocal

多线程是Java实现多任务的基础,Thread对象代表一个线程,我们可以在代码中调用Thread.currentThread()获取当前线程。例如,打印日志时,可以同时打印出当前线程的名字:

public class Main {
    public static void main(String[] args) throws Exception {
        log("start main...");
        new Thread(() -> {
            log("run task...");
        }).start();
        new Thread(() -> {
            log("print...");
        }).start();
        log("end main.");
    }

    static void log(String s) {
        System.out.println(Thread.currentThread().getName() + ": " + s);
    }
}

Web应用程序就是典型的多任务应用,每个用户请求页面时,我们都会创建一个任务,类似:

public void process(User user) {
 checkPermission();
 doWork();
 saveStatus();
 sendResponse();
 }


然后,通过线程池去执行这些任务。
process()方法内部需要调用若干其他方法,如何在一个线程内传递状态?
process()方法需要传递的状态就是User实例
在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。
给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User对象就传不进去了。

Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。
ThreadLocal实例通常总是以静态字段初始化如下:

static ThreadLocal<User> threadLocalUser = new ThreadLocal<>(); void processUser(user) {
 try {
 threadLocalUser.set(user);
 step1();
 step2();
 } finally {
 threadLocalUser.remove();
 }
 }


通过设置一个User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例:

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);
 }

注意到普通的方法调用一定是同一个线程执行的,所以,step1()、step2()以及log()方法内,threadLocalUser.get()获取的User对象是同一个实例。

实际上,可以把ThreadLocal看成一个全局Map<Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key:

ThreadLocal一定要在finally中清除:

try {
 threadLocalUser.set(user);
 …
 } finally {
 threadLocalUser.remove();
 }

小结
ThreadLocal表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的;

ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递);

使用ThreadLocal要用try … finally结构,并在finally中清除。