Java 中,实现 Runnable 接口或继承 Thread 类的类可以创建新线程。线程间的通信由 Object 类提供的 wait()
、notifyAll()
和 notify()
方法实现。
使用同步对象
Semaphore、CountDownLatch、CyclicBarrier、Exchanger 和 Phaser 类提供了同步对象的功能。
Semaphore
Semaphore 类实现了经典的信号量机制,用于控制对共享资源的访问。这个机制使用一个计数器,当某个线程访问共享资源时,如果计数器的值大于 0,则允许它访问,同时计数器的值减一;如果计数器的值小于等于 0,则阻塞当前线程。访问共享资源的线程访问完毕后,计数器的值加一。该操作执行完毕后,如果计数器的值大于 0,同时有想要访问该共享资源并且处于阻塞状态的线程,则该线程可以访问共享资源,同时计数器的值减一。
Semaphore 类的两个构造器如下
// num 表示计数器的初始值,也就是允许同时访问共享资源的线程个数
Semaphore(int num);
// how 为 true 表示处于阻塞状态的多个线程允许访问共享资源时,
// 访问的顺序由线程阻塞的先后顺序决定,也就是先来先服务。为
// false 时,线程访问的顺序不确定。
Semaphore(int num, boolean how);
acquire()
方法获取访问许可,当不允许获取时,挂起调用该方法的线程,直到可以获取访问许可时恢复。有两种形式。
void acquire();
// num 表示获取的访问许可数量
void acquire(int num);
release()
方法释放持有的访问许可。两种形式如下
void release();
// 释放 num 个访问许可
void release(int num);
使用共享资源时,必须先调用 acquire()
方法,然后再使用共享资源。使用完毕后,必须调用 release()
方法释放共享资源。
使用 Semaphore 类实现生产者-消费者。
class Q {
// 存放共享的值
int n;
static Semaphore semProducer = new Semaphore(1);
// 计数值为 0 保证生产者先执行
static Semaphore semConsumer = new Semaphore(0);
// 获取共享值之前,先获取消费者锁。获取了之后,释放生产者锁
public void get() throws InterruptedException {
semConsumer.acquire();
System.out.println("消费了:" + n);
semProducer.release();
}
// 产生共享值之前,获取生产者锁。完毕后,释放消费者锁
public void put(int n) throws InterruptedException {
semProducer.acquire();
this.n = n;
System.out.println("生产了:" + n);
semConsumer.release();
}
}
class Producer implements Runnable {
Q q;
Producer(Q q) {
this.q = q;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
q.put(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class Consumer implements Runnable {
Q q;
Consumer(Q q) {
this.q = q;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
q.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ProducerConsumer {
/**
生产了:1
消费了:1
生产了:2
消费了:2
生产了:3
消费了:3
生产了:4
消费了:4
生产了:5
消费了:5
**/
public static void main(String[] args) {
Q q = new Q();
Producer producer = new Producer(q);
Consumer consumer = new Consumer(q);
new Thread(consumer, "consumer").start();
new Thread(producer, "producer").start();
}
}
CountDownLatch
CountDownLatch 类用于线程等待一或多个事件发生后执行的情况。它有一个计数器,当计数器值为 0 时,打开 latch;值大于 0 时,保持线程阻塞。构造器为
// num 指定了计数器的初始值
CountDownLatch(int num);
调用 await()
方法时,阻塞当前线程,直到与 latch 关联的计数器值为 0 时退出阻塞状态。两种形式如下
// 阻塞当前线程,直到计数器值为 0。如果调用该方法时,计数器值为 0,则立即返回
void await();
// 调用该方法时,计数器值为 0,返回 true;
// 计数器值大于 0,阻塞当前线程,直到计数器值为 0 时,返回 true
// 或者,调用该方法后经过了 wait 时间(时间单位由 tu 决定),则返回 false
boolean await(long wait, TimeUnit tu);
countDown()
方法用于表明事件发生,此时计数器值减 1。形式为
void countDown();
class Thread1 implements Runnable {
CountDownLatch cdl;
Thread1(CountDownLatch countDownLatch) {
this.cdl = countDownLatch;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("事件 " + i + " 发生了");
// 当事件发生后,计数器的值需要减 1
cdl.countDown();
}
}
}
/*
事件 0 发生了
事件 1 发生了
事件 2 发生了
事件 3 发生了
事件 4 发生了
所有事件都发生了,结束
*/
public class CDLDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
new Thread(new Thread1(countDownLatch), "thread1").start();
// 计数器的值大于 0 时当前线程一直保持阻塞,等于 0 时恢复执行
countDownLatch.await();
System.out.println("所有事件都发生了,结束");
}
}
CyclicBarrier
CyclicBarrier 类用于指定数量的线程都执行到某一预定的 barrier point 时,再接着执行的情况。当到达 barrier point 的线程数量小于指定数量时,已到达的线程等待,直到指定数量的线程到达后再执行。构造器如下
// numThreads 指定必须有多少个线程到达 barrier point 后再继续执行
CyclicBarrier(int numThreads);
// action 指定继续执行时,执行的内容
CyclicBarrier(int numThreads, Runnable action);
await()
方法用于暂停当前线程的执行,直到指定数量的线程到达了 barrier point 再恢复执行。该方法的返回值用于表示到达 barrier point 的顺序,第一个到达的线程返回指定数量,最后一个到达的线程返回 0。形式为
int wait();
int wait(long wait, TimeUnit tu);
class Thread2 implements Runnable {
CyclicBarrier cb;
String name;
Thread2(CyclicBarrier cb, String name) {
this.cb = cb;
this.name = name;
}
@Override
public void run() {
System.out.println(name);
try {
// 假定到达了 barrier point,等待其他线程执行完成
cb.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}
}
class After implements Runnable {
@Override
public void run() {
System.out.println("到达 barrier point 后执行");
}
}
/*
(输出前三行顺序可能不同)
a
c
b
到达 barrier point 后执行
*/
public class BarDemo {
public static void main(String[] args) {
// 三个线程到达了 barrier point 后,执行传入的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new After());
new Thread(new Thread2(cyclicBarrier, "a")).start();
new Thread(new Thread2(cyclicBarrier, "b")).start();
new Thread(new Thread2(cyclicBarrier, "c")).start();
}
}
Exchanger
Exchanger 泛型类用于两个线程之间交换数据。声明形式为
// V 表示交换的数据类型
Exchanger<V>
exchange()
方法用于线程之间数据的交换。当两个线程都调用了 exchange()
方法后,数据才会交换,用于同步。形式为
// objRef 引用将要传给另一个线程的数据,返回值为另一个线程传过来的数据
V exchange(V objRef);
// 调用该方法时,如果没有其他线程调用同一个对象上的该方法,经过了指定时间后,抛出异常
// 在指定时间内其他线程调用了该方法,则交换 objRef 引用的数据
V exchange(V objRef, long wait, TimeUnit tu);
class MakeString implements Runnable {
Exchanger<String> exchanger;
String s;
public MakeString(Exchanger<String> exchanger) {
this.exchanger = exchanger;
s = new String();
}
@Override
public void run() {
char ch = 'a';
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 5; j++) {
s += ch++;
}
try {
// 将 s 表示的数据传递给另一个线程
// 另一个线程传递过来的数据赋值给 s
s = exchanger.exchange(s);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class UseString implements Runnable {
Exchanger<String> exchanger;
String s;
public UseString(Exchanger<String> exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
// 将空字符串传递给另一个线程,从另一个线程
// 接收的值赋值给 s
s = exchanger.exchange(new String());
System.out.println(s);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
/*
输出:
abcde
fghij
klmno
*/
public class ExgrDemo {
public static void main(String[] args) {
Exchanger<String> e = new Exchanger<>();
new Thread(new MakeString(e)).start();
new Thread(new UseString(e)).start();
}
}
Phaser
在某些分阶段执行的活动中,任一阶段有一或多个线程执行不同的任务。在每一阶段,该阶段的所有线程执行完毕后,再执行下一阶段的任务,直到所有阶段执行完毕。此时,需要保证每一阶段的线程都执行完毕,再进入下一阶段。Phaser 类用于这种情况。它可以同步同一阶段执行的线程,当每一阶段的线程都执行完毕后,再进入下一阶段。
部分构造器形式如下
// 创建的 Phaser 对象,有 0 个 party 在它上面注册
Phaser();
// 有 num 个 party 在创建的 Phaser 对象上面注册
Phaser(int num)
术语 party 指在 Phaser 上注册的对象。一般情况下,一个 party 表示一个线程。当 Phaser 对象创建之后,当前所处阶段为阶段 0(phase zero)。
通常当 Phaser 对象创建后,再在它上面注册一或多个 party。存在没有执行完毕的 party 时,Phaser 会等待,直到所有注册的 party 执行完毕。执行完毕后,如果 Phaser 还有下一阶段,则移动到下一阶段;否则,终止。party 通过调用 Phaser 提供的 arrive()
和 arriveAndAwaitAdvance()
等方法表明已完成执行。
register()
方法用于在当前 Phaser 所处阶段注册 party。形式为
// 在 Phaser 的当前阶段注册 party,并返回当前阶段号
// 返回负值表示 Phaser 已终止
int register();
arrive()
方法表明 party 到达了当前 Phaser 所处的阶段,不等待其他 party 执行到该阶段。形式为
// 返回当前 Phaser 所处的阶段号
// 返回负值表示 Phaser 已终止
// 未注册的 party 调用该方法抛出异常
int arrive();
arriveAndAwaitAdvance()
方法表明 party 到达了当前 Phaser 所处的阶段,并等待其他所有注册的 party 到达该阶段。形式为
// 返回当前 Phaser 所处的阶段号
// 返回负值表示 Phaser 已终止
// 未注册的 party 调用该方法抛出异常
int arriveAndAwaitAdvance();
arriveAndDeregister()
方法表明 party 到达了当前 Phaser 所处的阶段,并在 Phaser 上注销注册信息。形式为
// 返回当前 Phaser 所处的阶段号
// 返回负值表示 Phaser 已终止
// 未注册的 party 调用该方法抛出异常
int arriveAndDeregister();
getPhase()
方法返回当前 Phaser 所处的阶段号。形式为
final int getPhase();
class PThreadA implements Runnable {
String name;
Phaser phaser;
public PThreadA(String name, Phaser phaser) {
this.name = name;
this.phaser = phaser;
// 创建对象时在 phaser 上注册
phaser.register();
}
@Override
public void run() {
System.out.println("线程 " + name + " 执行完任务");
phaser.arriveAndAwaitAdvance();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程 " + name + " 执行完任务");
phaser.arriveAndAwaitAdvance();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程 " + name + " 执行完任务");
phaser.arriveAndDeregister();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/*
输出为:
线程 a 执行完任务
线程 c 执行完任务
线程 b 执行完任务
阶段 0 已完成
线程 b 执行完任务
线程 c 执行完任务
线程 a 执行完任务
阶段 1 已完成
线程 c 执行完任务
线程 b 执行完任务
线程 a 执行完任务
阶段 2 已完成
*/
public class PhaserDemo {
public static void main(String[] args) {
// 1 表示主线程注册
Phaser phaser = new Phaser(1);
new Thread(new PThreadA("a", phaser)).start();
new Thread(new PThreadA("b", phaser)).start();
new Thread(new PThreadA("c", phaser)).start();
int current = phaser.getPhase();
phaser.arriveAndAwaitAdvance();
System.out.println("阶段 " + current + " 已完成");
current = phaser.getPhase();
phaser.arriveAndAwaitAdvance();
System.out.println("阶段 " + current + " 已完成");
current = phaser.getPhase();
phaser.arriveAndAwaitAdvance();
System.out.println("阶段 " + current + " 已完成");
}
}
onAdvance()
方法在 Phaser 从某个阶段移动到下一个阶段时执行,可以重写该方法来控制这个过程。该方法返回 true 表示终止 Phaser。默认情况下,当没有注册的 party 时,返回 true。重写的目的之一是控制 Phaser 的阶段数量、终止。形式为
// phase 表示当前阶段的阶段号,也就是没有移动到下一阶段时的阶段号
// num 表示此时在 Phaser 上注册的 party 个数
protected boolean onAdvance(int phase, int num);
class PhaserSub extends Phaser {
int phaseNum;
public PhaserSub(int parties, int phaseNum) {
super(parties);
this.phaseNum = phaseNum - 1;
}
// 每次转换切换阶段时执行这个方法
@Override
protected boolean onAdvance(int phase, int registeredParties) {
System.out.println("阶段 " + phase + " 完成了");
if (phase == phaseNum || registeredParties == 0) {
return true;
}
return false;
}
}
class PThreadB implements Runnable {
String name;
Phaser phaser;
public PThreadB(String name, Phaser phaser) {
this.name = name;
this.phaser = phaser;
this.phaser.register();
}
@Override
public void run() {
while (!phaser.isTerminated()) {
System.out.println("线程 " + name + " 执行完阶段 " + phaser.getPhase());
phaser.arriveAndAwaitAdvance();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
/*
输出:
线程 b 执行完阶段 0
线程 c 执行完阶段 0
线程 a 执行完阶段 0
阶段 0 完成了
线程 c 执行完阶段 1
线程 a 执行完阶段 1
线程 b 执行完阶段 1
阶段 1 完成了
线程 c 执行完阶段 2
线程 a 执行完阶段 2
线程 b 执行完阶段 2
阶段 2 完成了
线程 c 执行完阶段 3
线程 b 执行完阶段 3
线程 a 执行完阶段 3
阶段 3 完成了
Phaser 终止了
*/
public class PhaserDemo2 {
public static void main(String[] args) {
PhaserSub phaserSub = new PhaserSub(1, 4);
new Thread(new PThreadB("a", phaserSub)).start();
new Thread(new PThreadB("b", phaserSub)).start();
new Thread(new PThreadB("c", phaserSub)).start();
while (!phaserSub.isTerminated()) {
phaserSub.arriveAndAwaitAdvance();
}
System.out.println("Phaser 终止了");
}
}
使用 Executor
Executor 用于发起和控制线程的执行。Executor 接口的 execute()
方法用于启动特定线程。形式为
// 开始执行 thread 表示的线程
void execute(Runnable thread)
ExecutorService 接口扩展了 Executor 接口,添加了管理和控制线程执行的方法。比如 shutdown()
方法用于停止 ExecutorService 启动的正在执行的线程。ScheduledExecutorService 接口扩展了 ExecutorService 接口,支持线程调度。
ThreadPoolExecutor 类实现了 Executor 和 ExecutorService 接口,提供管理线程池的功能。ScheduledThreadPoolExecutor 类实现了 ScheduledExecutorService 接口,允许调度线程池。ForkJoinPool 类实现了 Executor 和 ExecutorService 接口,用于 Fork/Join 框架。
线程池提供一组线程用于执行不同的任务,而不是为每一个任务创建一个独立的线程,这种方式减少了创建线程的开销。一般情况下,通过使用 Executors 类的静态工厂方法来获得 executor。
// 创建线程池,按需添加线程,尽可能重用已有线程。
static ExecutorService newCachedThreadPool();
// 创建的线程池具有 num 个线程
static ExecutorService newFixedThreadPool(int num);
// 创建的线程池支持线程调度
static ScheduledExecutorService newScheduledThreadPool(int num);
public class SimpExec {
static class ThreadA implements Runnable {
String name;
CountDownLatch countDownLatch;
public ThreadA(String name, CountDownLatch countDownLatch) {
this.name = name;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + " : " + i);
countDownLatch.countDown();
}
}
}
/*
输出:
a : 0
b : 0
a : 1
b : 1
a : 2
a : 3
a : 4
b : 2
c : 0
c : 1
b : 3
c : 2
c : 3
c : 4
b : 4
全部执行完毕
*/
public static void main(String[] args) {
CountDownLatch c = new CountDownLatch(5);
CountDownLatch c2 = new CountDownLatch(5);
CountDownLatch c3 = new CountDownLatch(5);
ExecutorService es = Executors.newFixedThreadPool(2);
es.execute(new ThreadA("a", c));
es.execute(new ThreadA("b", c2));
es.execute(new ThreadA("c", c3));
try {
// 阻塞当前线程(也就是运行 main 方法的线程)
// 直到和 c/c2/c3 关联的线程执行使得 c/c2/c3
// 计数器减为 0 时,恢复当前线程
c.await();
c2.await();
c3.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
es.shutdown();
System.out.println("全部执行完毕");
}
}
Callable 接口表示返回一个值的线程。形式为
// V 表示返回值的类型
interface Callable<V>
它定义了唯一的 call()
方法,形式为
// 线程具体执行的任务在这个方法中定义,最后需要返回一个值
V call();
Callable 任务通常通过调用 ExecutorService 的 submit()
方法执行,其中一种形式为
<T> Future<T> submit(Callable<T> task)
Future 接口用于接收 Callable 对象,表示一个正在执行或已经执行完毕的 Callable 对象。形式为
// V 表示结果类型
interface Future<V>
Future 的 get()
方法用于获取所表示的 Callable 对象的返回值。
public class CallableDemo {
static class Sum implements Callable<Integer> {
int n;
public Sum(int n) {
this.n = n;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i < n; i++) {
sum += i;
}
return sum;
}
}
static class Mul implements Callable<Integer> {
int n;
public Mul(int n) {
this.n = n;
}
@Override
public Integer call() throws Exception {
int sum = 1;
for (int i = 1; i < n; i++) {
sum *= i;
}
return sum;
}
}
/*
输出:
45
24
执行完成
*/
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<Integer> f1 = executorService.submit(new Sum(10));
Future<Integer> f2 = executorService.submit(new Mul(5));
try {
System.out.println(f1.get());
System.out.println(f2.get());
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
executorService.shutdown();
System.out.println("执行完成");
}
}
TimeUnit 枚举类型
TimeUnit 枚举类型定义了时间单位,取值有
- DAYS
- HOURS
- MINUTES
- SECONDS
- MICROSECONDS
- MILLISECONDS
- NANOSECONDS
使用方式之一为
// 修改上一段代码部分
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<Integer> f1 = executorService.submit(new Sum(10));
Future<Integer> f2 = executorService.submit(new Mul(5));
try {
// 等待 10 毫秒之后再执行 get() 方法
System.out.println(f1.get(10, TimeUnit.MILLISECONDS));
System.out.println(f2.get(10, TimeUnit.MILLISECONDS));
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
executorService.shutdown();
System.out.println("执行完成");
}
TimeUnit 提供了时间单位的转换方法
// 将 t 以 tu 为单位得到的结果进行转换,转换结果的时间单位为调用这个方法的时间单位类型
long convert(long t, TimeUnit tu);
// 1000, 1 秒转换成以毫秒为单位
TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS);
// 将调用这个方法的时间单位类型作为单位,t 为值得到的结果转换成以毫秒为单位的值
long toMillis(long t);
// 1000
TimeUnit.SECONDS.toMillis(1);
long toMicros(long t);
long toNanos(long t);
long toSeconds(long t);
long toDays(long t);
long toHours(long t);
long toMinutes(long t);
TimeUnit 提供了三个时间相关方法
// 暂停 t 个调用该方法的时间单位再执行
// 如 TimeUnit.SECONDS.sleep(10) 暂停 10 秒
void sleep(long t);
// 线程 th 暂停 t 个调用该方法的时间单位再执行
void timedJoin(Thread th, long t);
// o 等待 t 个调用该方法的时间单位
void timedWait(Object o, long t);
Locks
locks 对象用于控制共享资源的访问,类似于 synchronized 关键字的作用。在访问共享资源前,先获取和这个资源关联的锁,获取成功则访问共享资源。如果另一个线程在此时访问共享资源,则无法获得锁,维持 suspend 状态,直到已获得锁的线程释放锁。这个过程保证了任何时刻只有一个线程可以访问共享资源。
Lock 接口的 lock()
方法当锁可以获得时,获得锁;否则等待直到可以获得锁。unlock()
方法释放持有的锁。当持有锁时,tryLock()
方法返回 true;否则返回 false。newCondition()
方法返回和锁关联的 Condition 对象,这个对象用于更精确地控制锁。
ReentrantLock 类实现了 Lock 接口,也实现了可重入锁。可重入锁表示一个已经持有锁的线程可以多次获得锁。
public class LockDemo {
static class Shard {
static int count = 0;
}
static class AThread implements Runnable {
String name;
ReentrantLock reentrantLock;
public AThread(String name, ReentrantLock reentrantLock) {
this.name = name;
this.reentrantLock = reentrantLock;
}
@Override
public void run() {
System.out.println("线程 " + name + " 还没有获得锁");
reentrantLock.lock();
System.out.println("线程 " + name + " 已经获得锁");
Shard.count++;
System.out.println("线程 " + name + " 修改共享资源为:" + Shard.count);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println("线程 " + name + " 打算释放锁");
reentrantLock.unlock();
}
}
}
/*
输出
线程 a 还没有获得锁
线程 b 还没有获得锁
线程 a 已经获得锁
线程 a 修改共享资源为:1
线程 a 打算释放锁
线程 b 已经获得锁
线程 b 修改共享资源为:2
线程 b 打算释放锁
*/
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
new Thread(new AThread("a", reentrantLock)).start();
new Thread(new AThread("b", reentrantLock)).start();
}
}
ReadWriteLock 接口表示的锁对于读写有不同的机制。它允许对于不处于写状态的共享资源,可以分配多个锁给不同的线程进行读访问。ReentrantReadWriteLock 类实现了这个接口。
原子操作
原子操作在不使用其他同步机制的情况下,通过特定的类和方法将涉及到单个变量的修改和获取操作集成到一个原子性操作中,要么都执行,要么都不执行。这种方式可以容易的保证单个变量的值在某一时刻只能由一个线程修改。
class Shared {
static AtomicInteger num = new AtomicInteger(0);
}
public class AtomicDemo {
static class ThreadAtomic implements Runnable {
String name;
public ThreadAtomic(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("线程 " + name + " 开始执行");
for (int i = 1; i < 4; i++) {
// 先获取当前值,然后再设置新值
System.out.println("线程 " + name + ":" + Shared.num.getAndSet(i));
}
System.out.println("线程 " + name + " 执行完成");
}
}
/*
线程 a 开始执行
线程 b 开始执行
线程 b:1 (2nd j = 1, num = 1)
线程 b:1 (3rd j = 2, num = 2)
线程 b:2 (4th j = 3, num = 3)
线程 a:0 (1st i = 1, num = 1)
线程 b 执行完成
线程 a:3 (5th i = 2, num = 2)
线程 a:2 (6th i = 3, num = 3)
线程 a 执行完成
*/
public static void main(String[] args) {
new Thread(new ThreadAtomic("a")).start();
new Thread(new ThreadAtomic("b")).start();
}
}
使用 Fork/Join 框架进行并发编程
Fork/Join 框架简化了多线程的创建和使用,自动使用多核处理器。
传统的多线程编程和并发编程区别在于,传统的多线程没有考虑到使用多核处理器的情况,仅考虑最大化单个 CPU 的利用率。
部分任务可以拆分成多个类似的子任务,每个子任务在多核处理器的每个核上单独执行,最后将结果汇总得到了最初任务的结果。这种方式可以极大地提高处理这种类型任务的程序的执行速度。
Fork/Join 框架的核心有 4 个类
- ForkJoinTask<V>
- RecursiveAction
- RecursiveTask<V>
- ForkJoinPool
ForkJoinPool 类管理 ForkJoinTask<V> 抽象类的执行,RecursiveAction 抽象类和 RecursiveTask<V> 抽象类继承自 ForkJoinTask<V> 抽象类。
ForkJoinTask<V> 抽象类定义了被 ForkJoinPool 类管理的任务,其中 V 表示返回值的类型。它和 Thread 类不同,它是轻量级任务的抽象表示,而不是一个执行的线程。 ForkJoinPool 类型线程池管理的线程执行 ForkJoinTask<V>。
ForkJoinTask<V> 类有两个核心方法: fork()
和 join()
。
// 调用该方法的任务异步执行,不影响当前线程执行
final ForkJoinTask<V> fork();
// 调用该方法,当前线程等待任务完成再执行
final V join();
这个类的 invoke()
方法相当于将 fork()
和 join()
方法合成了一个,它开始执行任务,然后等待任务执行完成。invokeAll()
方法可以一次执行多个任务,当前线程等待,直到所有任务执行完毕。其中形式之一为
static void invokeAll(ForkJoinTask<?> ...taskList);
RecursiveAction 抽象类是 ForkJoinTask<V> 类的子类,它通常用于表示可递归的、无返回值的任务。它的 compute()
抽象方法表示任务的计算部分,也就是该任务需要执行的具体行为位于该方法中,形式为
protected abstract void compute();
RecursiveTask<V> 抽象类是 ForkJoinTask<V> 类的子类,它通常用于表示可递归的、有返回值的任务。它的compute()
抽象方法功能与 RecursiveAction 类的相似,除了必须有返回值。形式为
protected abstract V compute();
ForkJoinTask<V> 类表示的任务必须在 ForkJoinPool 类中执行,同时 ForkJoinPool 类也管理着这些任务。ForkJoinPool 类的创建有两种方式,一种是:显式使用构造器创建。另一种是:直接使用称为共同池(common pool)的静态 ForkJoinPool 对象,它可以自动获得。
ForkJoinPool 类的两种常见构造器如下
// 创建的 ForkJoinPool 对象的并行程度
// 和系统中可用的 CPU 核数相同
ForkJoinPool();
// 创建的 ForkJoinPool 对象的并行程度
// 由 pLevel 指定,必须大于 0,小于具体实现定义的限制
ForkJoinPool(int pLevel);
并行程度(level of parallism)决定了同时可以执行的线程数量,也就决定了同时可以执行的任务数量。并行程度不强制保证实际运行的线程数等于指定数量。
ForkJoinPool 类的 invoke()
方法用于开始任务的执行,同时等待任务执行完毕,将任务的执行结果返回。形式为
<T> T invoke(ForkJoinTask<T> task);
execute()
方法也用于开始任务的执行,但是它不等待任务执行完成,而是继续执行当前线程。形式为
void execute(ForkJoinTask<?> task);
调用 commonPool()
方法可以获得共同池的引用。形式为
static ForkJoinPool commonPool();
使用共同池开始执行任务有两种方式。一种方式为,调用 invoke()
或 execute()
方法。另一种方式为,在任务没有被 ForkJoinPool 管理时,调用任务的 fork()
或 invoke()
方法。此时,任务自动被共同池管理并开始执行。
ForkJoinPool 类使用称为 work-stealing 的方法管理线程的执行。每个 worker 线程都有属于自己的任务队列,当某个 worker 线程的任务队列为空时,它会从其他 worker 线程中取任务执行。这种方式提高了总体效率,维护负载均衡。
ForkJoinPool 类使用 daemon 线程,这种线程在所有用户线程终止之后终止,所以 ForkJoinPool 类不需要显式关闭。shutdown()
方法对共同池无效。
对于 Fork/Join 框架中的分治策略,API 文档认为一个任务的执行步骤数量应该在 100 到 10000 之间比较好。写并发程序时,不能假定程序的执行环境。
public class ForkJoinDemo {
// RecursiveAction 表示无返回值的任务
static class SqrtTransform extends RecursiveAction {
double[] data;
int start;
int end;
// 元素个数少于阈值时开始执行操作
final int threshold = 1000;
public SqrtTransform(double[] data, int start, int end) {
this.data = data;
this.start = start;
this.end = end;
}
@Override
protected void compute() {
if (end - start < threshold) {
for (int i = start; i < end; i++) {
data[i] = Math.sqrt(data[i]);
}
} else {
int middle = start + (end - start) / 2;
// ForkJoinTask<V> 的 invokeAll() 方法执行传入的所有
// 任务,等待所有任务执行完成
invokeAll(new SqrtTransform(data, start, middle),
new SqrtTransform(data, middle, end));
}
}
}
/*
输出:
0.0
1.0
1.4142135623730951
1.7320508075688772
2.0
2.23606797749979
2.449489742783178
2.6457513110645907
2.8284271247461903
3.0
*/
public static void main(String[] args) {
double[] d = new double[100_000];
for (int i = 0; i < d.length; i++) {
d[i] = i;
}
// 获取 common pool,作用与构造 ForkJoinPool 对象相同
// ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
ForkJoinPool forkJoinPool = new ForkJoinPool();
// invoke() 方法开始执行任务,并等待任务执行完成
forkJoinPool.invoke(new SqrtTransform(d, 0, d.length));
// 不被线程池管理的任务可以直接调用 invoke() 方法开始执行任务
// 此时默认使用的线程池为 common pool
// new SqrtTransform(d, 0, d.length).invoke();
for (int i = 0; i < 10; i++) {
System.out.println(d[i]);
}
}
}
不同并发程度和阈值情况下的并发程序执行时间开销如下
public class FJExperiment {
static class Transform extends RecursiveAction {
double[] d;
int start;
int end;
int threshold;
public Transform(double[] d, int start, int end, int threshold) {
this.d = d;
this.start = start;
this.end = end;
this.threshold = threshold;
}
@Override
protected void compute() {
if (end - start < threshold) {
for (int i = start; i < end; i++) {
if (d[i] % 2 == 0) {
d[i] = Math.sqrt(d[i]);
} else {
// 三次根
d[i] = Math.cbrt(d[i]);
}
}
} else {
int middle = start + (end - start) / 2;
// 开始执行并等待所有任务执行完成
invokeAll(new Transform(d, start, middle, threshold),
new Transform(d, middle, end, threshold));
}
}
}
/*
pLevel threshold 执行时间(单位:ns)
并发程度 阈值 执行时间
1 100 65185200
1 500 62819900
1 1000 66378700
2 100 39018300
2 500 39767300
2 1000 41178900
4 100 26305400
4 500 30683100
4 1000 30212800
6 100 21954400
6 500 27215300
6 1000 26951100
*/
public static void main(String[] args) {
// 并发程度
int pLevel = 6;
// 阈值
int threshold = 1000;
double[] data = new double[1_000_000];
for (int i = 0; i < data.length; i++) {
data[i] = i;
}
Transform transform = new Transform(data, 0, data.length, threshold);
ForkJoinPool forkJoinPool = new ForkJoinPool(pLevel);
long beginTime = System.nanoTime();
forkJoinPool.invoke(transform);
long endTime = System.nanoTime();
System.out.println("执行时间开销为:" + (endTime - beginTime) + " ns");
}
}
RecursiveTask<V> 的例子如下
public class RecursiveTaskDemo {
static class Sum extends RecursiveTask<Double> {
double[] d;
int start;
int end;
final int threshold = 500;
public Sum(double[] d, int start, int end) {
this.d = d;
this.start = start;
this.end = end;
}
@Override
protected Double compute() {
double sum = 0;
if (end - start < threshold) {
for (int i = start; i < end; i++) {
sum += d[i];
}
} else {
int middle = start + (end - start) / 2;
Sum sum1 = new Sum(d, start, middle);
Sum sum2 = new Sum(d, middle, end);
// 开始执行任务
sum1.fork();
sum2.fork();
// join() 方法等待任务执行完成
sum = sum1.join() + sum2.join();
}
return sum;
}
}
public static void main(String[] args) {
double[] data = new double[5000];
for (int i = 0; i < data.length; i++) {
data[i] = i % 2 == 0 ? i : -i;
}
Sum sum = new Sum(data, 0, data.length);
Double result = sum.invoke();
System.out.println(result);
}
}
ForkJoinPool 类的 execute()
方法开始执行一个 ForkJoinTask 任务,但不等待该任务执行完成,用于异步执行任务。形式为
void execute(ForkJoinTask<V> task);
// 作为传统多线程编程和 Fork/Join 框架之间的桥梁
void execute(Runnable task);
ForkJoinTask 类的 cancel()
方法用于取消正在执行的任务。形式为
// 当前正在执行的任务成功取消,返回 true
// 任务已经完成或没有成功取消,返回 false,此时 interrupt 无法被默认实现使用
boolean cancel(boolean interrupt);
isCancelled()
方法用于判断任务是否被取消,如果成功取消,返回 true;否则返回 false。形式为
final boolean isCancelled();
isCompletedNormally()
方法在任务正常执行完成时返回 true,否则返回 false。isCompletedAbnormally()
方法在任务抛出异常终止或使用 cancel()
方法取消后,返回 true;否则返回 false。形式为
final boolean isCompletedNormally();
final boolean isCompletedAbnormally();
一般情况下,已经执行过的任务不能再次执行。reinitialize()
方法可以初始化已经执行完成的任务状态,初始化后,任务可以再次执行。但是任务执行造成的状态变化不恢复。形式为
void reinitialize();
adapt()
方法将 Runnable 或 Callable 对象转换成 ForkJoinTask 对象。
ForkJoinPool 类的 toString()
方法显示对用户友好的线程池概要信息。isQuiescent()
方法判断线程池中是否所有线程都处于未执行任务状态。getPoolSize()
返回线程池中 worker 线程的个数。shutdown()
方法关闭线程池时,正在执行的任务依然会执行,不会执行新的任务。shutdownNow()
方法立即停止线程池。
ForkJoinTask 的阈值不能设置过低,ForkJoinPool 最好使用默认的并行程度。
参考
[1] Herbert Schildt, Java The Complete Reference 11th, 2019.
[2] https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CountDownLatch.html
[3] https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Exchanger.html
[4] https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Phaser.html
[5] https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html
[6] https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinTask.html