1. 生产者-消费者问题扩展
1.1. 多个生产者-多个消费者-多个商品
功能:扩展生产者消费者问题。由一个生产者、一个消费者、一个商品扩展为多个生产者、多个消费者、多个商品。
- 最多10个商品,最少0个商品
- 已有10个商品,生产者不再生产,还要通知消费者消费
- 没有商品,消费者不再消费,还要通知生产者生产
1.1.1 定义商品工厂
public class ProductFactory {
private List<String> list = new ArrayList<String>();
private int max = 10;
public ProductFactory() {}
public ProductFactory(int max) {
this.max = max;
}
// 生产商品
public synchronized void produce(String productName) {
// 如果仓库满了,就不再生产了
while (list.size() == max) {
try {
// 是否锁,进入等待队列
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产商品
list.add(productName);
System.out.println(Thread.currentThread().getName() + "生产商品:" + productName + "目前商品数量:" + list.size());
// 通知消费者消费
this.notifyAll();
}
// 消费商品
public synchronized void consume() {
// 如果仓库为空,就等待
while (list.size() == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费商品
String productName = list.remove(0);
System.out.println(Thread.currentThread().getName() + "消费商品:" + productName + ",目前商品数量:" + list.size());
// 通知生产者
this.notifyAll();
}
}
1.1.2. 定义生产者线程
public class ProductRunnable implements Runnable {
private ProductFactory factory;
public ProductRunnable(ProductFactory factory) {
this.factory = factory;
}
@Override
public void run() {
int i = 1;
while (true) {
factory.produce("商品" + i);
i++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.1.3. 定义消费者线程
public class ConsumeRunnable implements Runnable {
private ProductFactory factory;
public ConsumeRunnable(ProductFactory factory) {
this.factory = factory;
}
@Override
public void run() {
while (true) {
// 消费一个商品
factory.consume();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.1.4. 定义测试类
public class Test {
public static void main(String[] args) {
// 创建并启动多个生产者和消费者进程
ProductFactory factory = new ProductFactory();
Runnable target = new ProductRunnable(factory);
for (int i = 0; i < 5; i++) {
new Thread(target, "生产者" + i).start();
}
Runnable target1 = new ConsumeRunnable(factory);
for (int i = 0; i < 10; i++) {
new Thread(target1, "消费者" + i).start();
}
}
}
1.1.5. 总结
Q1: 生产者和消费者使用同一把锁,所以wait()的生产者和消费者都在一个等待队列中。不能保证生产者唤醒的不是生产者
A1: 如果有多个线程,将wait放在while循环中。被唤醒后,会重新进行条件的判断。如果仓库已满,可以在此阻塞
Q2: 仓库中最多的商品动态指定
A2: 可以通过构造方法或者setter方法提供
1.2. 使用匿名内部类
生产者和消费者线程可以使用匿名内部类实现
- 匿名内部类可以访问外部类的成员变量
- 匿名内部可访问外部类当前方法中的局部变量,需要使用final修饰。(JDK1.8后可省略)
public class Test1 {
static int num = 10;
public static void main(String[] args) {
// 创建和启动多个生产者和消费者线程
int max = 30;
ProductFactory factory = new ProductFactory(max);
Runnable runnable = new Runnable() {
@Override
public void run() {
int i = 0;
while (true) {
factory.produce("商品" + i);
i++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
for (int i = 0; i < 5; i++) {
new Thread(runnable, "生产者" + i).start();
}
Runnable runnable1 = new Runnable() {
@Override
public void run() {
while (true) {
factory.consume();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
for (int i = 0; i < 10; i++) {
new Thread(runnable1, "消费者" + i).start();
}
}
}
1.3. 使用Lock锁和Condition
Q:生产者一个等待队列,消费者一个等待队列,唤醒时到相应的等待队列中唤醒
A:使用Lock锁。一个Lock锁可以产生多个Condition条件,一个Condition条件对应一个等待队列
public class ProductFactory1 {
private List<String> list = new ArrayList<String>();
private int max = 10;
private Lock lock = new ReentrantLock();
private Condition produceCond = lock.newCondition();
private Condition consumeCond = lock.newCondition();
public ProductFactory1() {}
public ProductFactory1(int max) {
this.max = max;
}
public void produce(String productName) {
lock.lock();
try {
// 如果仓库满,不再生产
while (list.size() == max) {
try {
produceCond.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产商品
list.add(productName);
System.out.println(Thread.currentThread().getName() + "生产商品:" + productName + "目前商品数量:" + list.size());
// 通知消费者消费
consumeCond.signal(); // 唤醒一个
}finally {
lock.unlock();
}
}
public void consume() {
lock.lock();
try {
// 如果仓库为空,就等待
while (list.size() == 0) {
try {
consumeCond.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费商品
String productName = list.remove(0);
System.out.println(Thread.currentThread().getName() + "消费商品:" + productName + ",目前商品数量:" + list.size());
// 通知生产者
// 唤醒所有的生产者,但是只有一个生产者首先获取Lock,然后进入就绪队列,通过调度获得CP
produceCond.signalAll();
}finally {
lock.unlock();
}
}
}
2. Lock锁
2.1. Lock锁和Synchronized锁的比较
- synchronized的缺陷
- synchronized是java中的关键字,是java语言内置的特性,如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程只能一直等待,等待获取锁的线程释放锁
- 获取锁的线程执行完了该代码块,然后线程释放对锁的占有
- 线程执行发生异常,此时JVM会让线程自动释放锁
如果获取锁的线程由于要等待IO或其他原因被阻塞了,但是又没有释放锁,其他线程只能等待,就会很影响执行效率
因此就需要一种机制:可以不让等待的线程一直无限等待下去,Lock锁就可以办到
- synchronized是java中的关键字,是java语言内置的特性,如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程只能一直等待,等待获取锁的线程释放锁
- 例子:当有多个线程读写文件时,读操作和写操作会发生冲突,写操作和写操作会发生冲突,但是读和读操作不会发生冲突
- 采用synchronized关键字来实现同步,如果多个线程都只是进行读操作,当一个线程进行读操作时,其他只能等待无法进行读操作
- 通过Lock锁可知线程是否成功获取到锁,synchronized不能。
- Lock不是java语言内置的,synchronized是java语言的关键字,是内置特性。Lock是一个类,通过这个类可以实现同步访问
- synchronized不需要手动释放锁,当synchronized方法或代码块执行结束,系统会自动让线程释放对锁的占用;而Lock则必须手动释放锁,如果没有主动释放锁,就可能会导致死锁的现象
- 对于synchronized方法或代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常出现死锁,Lock必须手动释放。
2.2. Lock锁API
- Lock
Lock是一个接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
- lock():获取锁,如果锁已经被其他获取,则进行等待。
如果采用Lock,必须主动释放锁,并且在发生异常时,不会自动释放锁。使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally中,以保证锁被释放,防止死锁。 - tryLock():方法表示尝试获取锁,如果获取成功,返回true,如果获取失败(即锁已被其他线程获取),返回false。如果拿不到锁,也不会一直等待
- tryLock(long time, TimeUnit unit):和tryLock()方法类似,区别在于这个方法拿不到锁时会等待一定的时间,在时间期限内,如果还拿不到锁,就返回false;如果一开始就拿到锁或在等待期间内拿到了锁,就返回true
- lockInterruptibly():当通过这个方法获取锁时,如果线程正在等待获取锁,则这个线程能构相应中断。也就是说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,如A线程获取到了锁,那么B只能在等待,B调用thread.interrupt()方法可以中断线程B的等待过程
- ReentrantLock:可重用锁,ReentrantLock实现了Lock接口的非内部类,并且ReentrantLock提供了更多的方法。
- 同一时间点只能被一个线程锁持有
- 可以被单个线程多次获取
- ReentrantLock分为公平锁和非公平锁。他们的区别体现在获取锁的机制上,ReentrantLock是通过一个FIFO的等待队列来管理获取锁的所有线程的。
- 公平锁:线程依次排队获取锁
- 非公平锁:在锁是可获取状态时,不管在队列哪个位置,都会获取锁
- ReadWriteLock:也是一个接口,里面只定义了两个方法,是将文件的读写操作分开,分成两个锁来分配给线程,从而使得多个线程可以同时进行读操作
- readLock():获取读锁
- writeLock():获取写锁
- ReentrantReadWriteLock:是ReadWriteLock接口的实现类,包括内部类ReadLock和WriteLock,这两个内部类实现了Lock接口;ReentrantReadWriteLock里面提供了很多丰富的方法,最主要的就是readLock()和writeLock()
public class TestLock {
public static void main(String[] args) {
// 默认也是非公平锁,也是可重用锁
ReadWriteLock rwl = new ReentrantReadWriteLock();
// 多次返回的都是同一把读写锁
Lock readLock = rwl.readLock();
Lock readLock1 = rwl.readLock();
Lock writeLock = rwl.writeLock();
readLock.lock();
readLock.unlock();
System.out.println(readLock == readLock1);
}
}
2.3. Lock和Synchronized的选择
- Lock是一个接口,而synchronized是java关键字,synchronized是内置的语言实现
- synchronized发生异常时,会自动释放线程占有锁,不会导致死锁的发生;Lock在发生异常时,如果没有主动通过unLock()释放锁,可能会造成死锁,因此使用Lock时需要在finally中释放锁
- Lock可以让等待锁的线程相应中断,synchronized不行,使用synchronized时,线程会一直等待,不能响应中断
- 通过Lock可以知道有没有成功获取锁,synchronized不行
- Lock可以提高多个线程读操作的效率
在性能上来说,如果竞争资源不激烈,两者性能差不多,当竞争资源非常激烈时(有大量线程同时竞争),此时Lock的性能远优于synchronized。因此,要根据具体情况适当选择
2.4. 认识Condition
Condition是在java1.5中出现的,用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()实现线程间协作更加安全和高效。
他的更强大的地方在于:能构更加精细的控制多线程的休眠与唤醒。对于同一个锁,可以创建多个Condition,在不同情况下使用不同的Condition,一个Condition包含一个等待队列,一个Lock可以产生多个Condition,所以可以有多个等待队列。
在Object中的wait()、notify()、notifyAll()方法是和同步锁关键字synchronized捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。
调用Condition的await()、signal()、signalAll()方法,就是在lock保护之内,必须在lock.lock和lock.unlock之间才可以使用
- Condition的await()对应Object的wait()
- Condition的signal()对应Object的notify()
- Condition的signalAll()对应Object的signalAll()
void await() throws InterruptedException:造成当前线程在接收信号或被中断之前一直处于等待状态。
与此Condition相关的锁以原子方式释放,并且出于线程调度的目的,将禁用当前线程,且在发生以下四种情况之一以前,当前线程将一直处于休眠状态:
- 其他某个线程调用此Condition的signal()方法,并且碰巧当前线程选为被唤醒的线程
- 其他某个线程调用此Condition的signalAll()方法
- 其他某个线程中断当前线程,且支持中断线程的挂起
- 发生"虚假唤醒"
在所有情况下,在此方法可以返回当前线程之前,都必须重新获取与此条件有关的锁。在线程返回时,可以保证他保持此锁
void signal():唤醒一个等待线程。如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从await返回之前,该线程必须重新获取锁。
void signalAll():唤醒所有等待线程。如果所有线程都在等待此条件,则唤醒所有线程。再从await返回之前,每个线程都必须重新获取锁。
ReadWriteLock示例
public class TestReadWriteLock {
public static void main(String[] args) {
final Operator operator = new Operator();
// 创建5个读数据的线程
for (int i = 0; i < 5; i++) {
new Thread("读线程" + i) {
@Override
public void run() {
while (true) {
operator.read();
}
}
}.start();
}
// 创建5个写数据的线程
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
operator.write();
}
}
}, "写线程" + i).start();
}
}
}
public class Operator {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
/*
* 读操作,要添加读锁,希望多个线程同时读取,提高效率
* */
public void read() {
rwl.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "开始读取数据...");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取数据完毕...");
}finally {
rwl.readLock().unlock();
}
}
/*
* 写操作,要使用写锁,保证安全性,只有一个线程独占
* */
public void write() {
rwl.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "开始写数据...");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "写数据完毕...");
}finally {
rwl.writeLock().unlock();
}
}
}
3. BlockingQueue、volatile和线程池
3.1. BlockingQueue
- 什么是BlockingQueue:BlockingQueue即阻塞队列:在某些情况下,对阻塞队列的访问可能会造成阻塞,被阻塞的情况主要有:
- 当队列满时进行入队列操作
- 当队列空时进行出队列操作
因此,当一个线程试图对一个已满队列进行入队操作时,会阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队操作时,会被阻塞,除非有另一个线程进行了入队操作
BlockingQueue接口位于java.util.concurrent包中,阻塞队列是线程安全的。
新增Concurrent包中,BlockingQueue很好的解决了线程中的高效传输数据的问题,通过这些高效且线程安全的队列,为快速搭建高质量的多线程程序带来了极大的便利
- BlockingQueue用法:阻塞队列主要用于生产者/消费者场景
负责生产的线程不断的制造新对象并插入到阻塞队列中,直到达到队列的上限。队列达到上限之后生产线程将会被阻塞,直到消费的线程对这个队列进行消费。同理,负责消费的线程不断的从队列中消费对象,直到这个队列为空,当队列为空时,消费线程将会被阻塞,除非队列中有新对象被插入 - BlockingQueue方法
四种不同行为的解释:- 异常:如果试图的操作无法立即执行,抛一个异常
- 特殊值:如果试图的操作无法立即执行,返回一个特定的值(true/false)
- 阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行
- 超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否能够成功(典型的是true/false)
- BlockingQueue不接受null元素。试图add、put或offer一个null元素时,某些实现会抛出NullPointerException。null被用作指示poll操作失败的警戒值。
- BlockingQueue可以是限定容量的。他在任意给定时间都可以有一个remainingCapacity,超出此容量,便无法无阻塞地put附加元素。没有任何内部容量约束的BlockingQueue总是报告Integer.MAX_VALUE的剩余容量
- BlockingQueue实现是线程安全的。所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到他们的目的
- 实现BlockingQueue
- ArrayBlockingQueue
- 一个有边界(容量有限)的阻塞队列,他的内部实现是一个数组。必须在初始化时指定他的容量大小,一旦指定,就不可改变
- 先进先出的方式来存储数据,最新插入的对象是尾部,最新移除的对象是头部
此类支持对等待的生产者线程和使用者线程进行排序的可选公平策略。默认情况下,不保证是这种排序。然而,通过将公平性设置为true而构造的对列允许按照FIFO顺序访问线程。公平性通常会降低吞吐量,但也减少了可变性和避免了不平衡性。
- LinkedBlockingQueue
- 阻塞队列大小的配置是可选的,如果初始化时指定一个大小,它就是有边界的,如果不指定,就是无边界的(采用默认大小Integer.MAX_VALUE的容量)。他的内部实现是一个链表。
- 和ArrayBlockingQueue一样,LinkedBlockingQueue也是先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部
- ArrayBlockingQueue
3.2. 生产者消费者默认BlockingQueue
public class ProductFactory {
private BlockingQueue blockingQueue;
private int max = 10;
public ProductFactory() {
blockingQueue = new LinkedBlockingQueue(max);
}
public ProductFactory(int max) {
this.max = max;
blockingQueue = new LinkedBlockingQueue(max);
}
/*
* 生产商品
* */
public void produce(String productName) {
try {
blockingQueue.put(productName);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "生产商品:" + productName + ",目前商品数量:" + blockingQueue.size());
}
/*
* 消费商品
* */
public void consume() {
String productName = null;
try {
productName = (String) blockingQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "消费商品:" + productName + ",目前商品数量:" + blockingQueue.size());
}
}
因为BlockingQueue内置了阻塞功能,所以使用其来实现生产者-消费者问题,可以看到代码简单多了
3.3. volatile关键字
public class Test {
private static boolean flag = true;
public static void main(String[] args) {
// 创建一个线程并启动
new Thread(new Runnable() {
@Override
public void run() {
while (flag) {
System.out.println("======");
}
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
}
}
- 基本概念
Java内存模型中的可见性、原子性和有序性- 可见性:是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。 也就是一个线程修改的结果,另一个线程马上就能看到。用volatile修饰的变量,就会有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存,所以对其他线程是可见的。注意:volatile只能让被他修饰内容具有可见性,但不能保证他具有原子性,操作间同样存在线程安全问题。在java中,volatile、synchronized和final实现可见性。
- 原子性:原子是世界上最小的单位,具有不可分割性。比如,a=0,这个操作是不可分割的,是原子操作;a++,实际上是a=a+1,是可分割的,所以不是一个原子操作。非原子操作会存在线程安全问题,需要使用同步技术sychronized来让他变成一个原子操作。java的concurrent包下提供了一些原子类,比如AtomicInteger、AtomicLong、AtomicReference等;在java中synchronized和lock、unlock中操作保证原子性
- 有序性:java中提供volatile和synchronized两个关键字来保证线程间操作的有序性
- volatile原理:稍弱的同步机制,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。 volatile变量不会被缓存在寄存器或对其他处理器不可见的地方,因此在读取volatile类型变量时总会返回最新写入的值。
在访问volatile变量时,不会执行加锁的操作,因此也就不会执行线程阻塞,volatile变量是一种比synchronized关键字更轻量级的同步机制
当对非volatile变量进行读写时,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。
而声明变量是volatile的,JVM保证了每次读变量都从内存中读,跳过CPU cache这一步
当一个变量定义为volatile之后,将具备两种特性:
1. 保证此变量对所有线程的可见性,当一个线程修改了这个变量的值,volatile保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成
2. 禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个内存屏障(指令重排序时,不能把后面的指令重排序(CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路处理单元)到内存屏障之前的位置),只有一个CPU访问内存时,不需要内存屏障
- volatile性能:volatile的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为他需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
3.4. 线程池
- Executor:线程池顶级接口,只有一个方法
- ExecutorService:真正的线程池接口
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
- void shutdown():关闭线程池
- AbstractExecutorService:基本实现了ExecutorService的所有方法
- ThreadPoolExecutor:默认的线程池实现类
- ScheduledThreadPoolExecutor:实现周期性任务调度的线程池
- Executors:工具类、线程池的工厂类,创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,可以安排在给定延迟后运行命令或定期执行
- 线程池ThreadPoolExecutor参数
public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize. long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
}
- corePoolSize:核心池大小
- 默认情况下,创建了线程池后,线程数为0,当有任务来之后,就会创建一个线程去执行任务
- 当线程池中线程数量达到corePoolSize,就会把到达的任务放到队列中等待
- maximumPoolSize:最大线程数
- corePoolSize和maximumPoolSize之间的线程数会自动释放,小于等于corePoolSize的不会释放。大于这个值就会将任务由一个丢弃处理机制来处理
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- 默认只限于corePoolSize和maximumPoolSize之间的线程
- TimeUnit:keepAliveTime的时间单位
- BlockingQueue:存储等待执行的任务的阻塞队列,有多种选择,可以是顺序队列、链式队列
- ThreadFactory:线程工厂,默认是DefaultThreadFactory,Executors的静态内部类
- RejectedExecutionHandler
- 拒绝处理任务时的策略。如果线程池的线程已经饱和,并且任务队列也已满,对新的任务应该采取什么策略。
- 比如抛出异常、直接舍弃、丢弃队列中最旧任务等,默认是直接抛出异常
- CallerRunsPolicy:如果发现线程池还在运行,就直接运行这个线程
- DiscardOldestPolicy:在线程池的等待队列中,将头取出一个抛弃,然后将当前线程放进去
- DiscardPolicy:什么也不做
- AbortPolicy:java默认,抛出一个异常
3.5. ForkJoin框架
- 使用场景:java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干小任务,最终汇总每个小任务结果得到大任务结果的框架
- 工作窃取算法:一个大任务拆分成多个小任务,为了减少线程间的竞争,把这些字任务分别放到不同的队列中,并且每个队列都有单独的线程来执行队列里的任务,线程和队列一一对应。
问题:A线程处理完自己队列的任务,B线程的队列里还有很多任务要处理
结局:A从双端队列尾部拿任务执行,B从双端队列的头部拿任务执行
- 优点:利用了线程并行计算,减少线程间的竞争
- 缺点
- 如果双端队列中只有一个任务,线程间会存在竞争
- 窃取算法消耗了更多的系统资源,如会创建多个线程和多个双端队列
- 多个类
- ForkJoinTask:使用该框架,需要创建一个ForkJoin任务,他提供在任务中执行fork和join操作的机制。一般情况下,并不需要直接继承ForkJoinTask类,只需要继承他的子类
- RescursiveAction:用于没有返回结果的任务
- RescursiveTask:用于有返回结果的任务
- ForkJoinPool:任务ForkJoinTask需要通过ForkJoinPool来执行
- ForkJoinWorkerThread:ForkJoin Pool线程池中的一个执行任务的线程
- ForkJoinTask:使用该框架,需要创建一个ForkJoin任务,他提供在任务中执行fork和join操作的机制。一般情况下,并不需要直接继承ForkJoinTask类,只需要继承他的子类