队列-线程池-ForkJoinPool

 

阻塞队列

阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下所示:

队列-线程池-ForkJoinPool_第1张图片

  1. 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
  2. 当阻塞队列是满时,往队列里添加元素的操作将会 被阻塞。
  • 阻塞队列有没有好的一面
  • 不得不阻塞,你如何管理

为什么需要BlockingQueue?

  1. 好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了,在concurrent包发布之前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这回给我们的程序带来不小的复杂度。
  2. 在对线程领域:所谓阻塞,在某些情况下挂起线程(即阻塞),一旦条件满足,被挂起的线程又会被唤醒。

下面是一些阻塞队列(BlockingQueue的子类)

  1. ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  2. LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列。
  3. ProrityBlockingQueue:支持优先级排序但无界阻塞队列。
  4. DelayQueue:使用优先级队列实现但延迟无界阻塞队列。
  5. SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
  6. LinkedTransferQueue:有链表结构组成的无界阻塞队列。
  7. LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

1、ArrayBlockingQueue

ArrayBlockingQueue:是一个基于数组结构的有届阻塞队列,此队列按FIFO(先进先出)原则对元素进行操作

方法类型 异常组 特殊组 阻塞 超时
插入 add(e) offer(e) put(e) offer(e,time,unit)

移除

 

检查

remove()

 

element()

poll()

 

peek()

take()

 

不可用

poll(time,unit)

 

不可用

抛出异常

当阻塞队列满时,再往队列里add插入元素会抛出java.lang.IllegalStateException: Queue full

当阻塞队列空时,再往队列里remove插入元素会抛出java.util.NoSuchElementException

特殊值

插入方法,成功true失败false

移除方法,成功返回出队列元素,队列里面没有就返回null

一直阻塞

当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出

当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用

超时退出 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限定时间后生产者线程会退出
 public static void main(String[] args) {
        BlockingQueue blockingQueue = new  ArrayBlockingQueue(3);

        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        System.out.println(blockingQueue.add("c"));
    }
结果:
true
true
true
Exception in thread "main" java.lang.IllegalStateException: Queue full
	at java.util.AbstractQueue.add(AbstractQueue.java:98)
	at java.util.concurrent.ArrayBlockingQueue.add(ArrayBlockingQueue.java:312)
	at com.yi23.springboot.interview.BlockingQueueTest.main(BlockingQueueTest.java:20)
-------------------------------

    public static void main(String[] args) {
        BlockingQueue blockingQueue = new  ArrayBlockingQueue(3);

        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
//        System.out.println(blockingQueue.add("c"));
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
    }
结果:
true
true
true
a
b
c
Exception in thread "main" java.util.NoSuchElementException
	at java.util.AbstractQueue.remove(AbstractQueue.java:117)
	at com.yi23.springboot.interview.BlockingQueueTest.main(BlockingQueueTest.java:24)

==============================offer(e) poll peek===============
 BlockingQueue blockingQueue = new  ArrayBlockingQueue(3);

        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("d"));

        System.out.println(blockingQueue.peek());

        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
结果:
true
true
true
false
a
a
b
c
null
null

SynchronousQueue没有容量(同步队列)。与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

  public static void main(String[] args) {

        BlockingQueue blockingQueue = new SynchronousQueue<>();

        new Thread(()->{

            System.out.println(Thread.currentThread().getName()+"\t put 1");
            try {
                blockingQueue.put("1");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"\t put 2");
            try {
                blockingQueue.put("2");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"\t put 3");
            try {
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"put线程").start();

        new Thread(()->{

            try {
                Thread.sleep(5);
                System.out.println(Thread.currentThread().getName()+"\t take 1:"+blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                Thread.sleep(5);
                System.out.println(Thread.currentThread().getName()+"\t take 2:"+blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                Thread.sleep(5);
                System.out.println(Thread.currentThread().getName()+"\t take 3:"+blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"take线程").start();
    }
结果:
put线程	 put 1
take线程	 take 1:1
put线程	 put 2
take线程	 take 2:2
put线程	 put 3
take线程	 take 3:3

线程通信之生产者消费者传统版

public class Consumer {

    public static void main(String[] args) {
        ShareData product = new ShareData();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                product.product();
            }

        }, "生产者").start();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                product.consumer();
            }
        }, "消费者").start();
    }
}

class Product {
    //传统模式的生产者消费者
    Integer count = 0;

   public synchronized void product() {

        while (count > 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        count++;
        System.out.println("生产一个:" + count);
        notify();
    }

    public synchronized void consumer() {

        while (count <= 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("消费一个count:" + count);
        count--;
        notify();
    }

}

class ShareData {
    Integer data = 0;

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void product() {

        lock.lock();
        try {
            while (data > 0) {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            data++;
            System.out.println("生产一个:" + data);
            condition.signal();
        } catch (Exception e) {

        } finally {
            lock.unlock();
        }
    }

    public void consumer() {
        lock.lock();
        try {
            while (data <= 0) {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("消费一个count:" + data);
            data--;
            condition.signal();
        } catch (Exception e) {

        } finally {
            lock.unlock();
        }

    }
}
结果:
生产一个:1
消费一个count:1
生产一个:1
消费一个count:1
生产一个:1
消费一个count:1
生产一个:1
消费一个count:1
生产一个:1
消费一个count:1

Synchronized与Lock有什么区别?

1、原始构成

  • synronized是关键字属于JVM层面,
  • monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象只有在同步块或方法中才能调用wait/notify等方法)monitorexit
  • Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁。

2、使用方法

  • synchronized不需要用户去手动解放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用
  • ReentrantLock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象。
  • 需要lock和unlock方法配合try/finally语句块来完成。

3、等待是否可中断

  • synchronized不可中断,除非抛出异常或正常运行完成
  • ReentrantLock可中断,1、设置超时方法tryLock(long timeout,TimeUnit unit)
  •                                     2、lockInterruptibly()放代码块中,调用interrupt()方法可中断

4、加锁是否公平

  • synchronized是非公平锁
  • ReentrantLock两者都可以,默认非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁

5、锁绑定多个条件Condition

  • synchronized没有
  • ReentrantLock用来实现分组唤醒的线程,可以精确唤醒,而不是想synchronized要么随机唤醒一个线程要么唤醒全部线程。

线程池

为什么用线程池,优势?

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量的线程排队等待,等其他线程执行完毕,再从队列中取出任务来执行。

他的主要特点为:

  1. 线程服用:控制最大并发数:管理线程。
  2. 第一、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  3. 第二、提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  4. 第三、提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,海辉降低系统的稳定性,使用线程池可以进行同一分配,调优和监控。

1、创建一个定长的线程池:

ExecutorService executorService1 = Executors.newFixedThreadPool(4);,

主要特点如下:

  1. 创建一个定长线程池最大并发书,超出的线程会在队列中等待。
  2. newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的它使用的LinkedBlockingQueue;

队列-线程池-ForkJoinPool_第2张图片

下面是源码:

//Executors里面的方法 
 public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue());
    }

//ThreadPoolExecutor的方法
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

Executors.newFixedThreadPool(4)使用的阻塞队列是LinkedBlockingQueue,
核心线程数和最大线程数一样(maximumPoolSize=corePoolSize),
存活的时间keepAliveTime

创建一个单一的线程

  1. ExecutorService executorService2 = Executors.newSingleThreadExecutor();

源码(和上面比较说明):

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
    }

创建一个缓存线程

  1. ExecutorService executorService = Executors.newCachedThreadPool();

队列-线程池-ForkJoinPool_第3张图片

源码

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }

ThreadPoolExecutor

源码:

//workQueue:任务队列,被提交但尚未被执行但任务。
private final BlockingQueue workQueue;
//示生成线程池中工作线程的工厂,用于创建线程一般用默认的即可。
private volatile ThreadFactory threadFactory;
//存活时间,多线程可见    
private volatile long keepAliveTime;

    private volatile boolean allowCoreThreadTimeOut;

    /**
      核心线程数
     */
    private volatile int corePoolSize;

    /**
       最大的线程数
     */
    private volatile int maximumPoolSize;

    /**
       拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)
    */
    private static final RejectedExecutionHandler defaultHandler =
        new AbortPolicy();
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);//单位和数字组成了时间
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

ThreadPoolExecutor线程池的7大参数介绍:
corePoolSize:线程池中常驻核心线程数 
maximumPoolSize:线程持能够容纳同时执行的最大线程数,此值必须大约等于1
keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到 
              keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。
TimeUnit:keepAliveTime的单位
workQueue:任务队列,被提交但尚未被执行但任务。
ThreadFactory:示生成线程池中工作线程的工厂,用于创建线程一般用默认的即可。
handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize);

默认情况下:只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize

队列-线程池-ForkJoinPool_第4张图片

队列-线程池-ForkJoinPool_第5张图片

队列-线程池-ForkJoinPool_第6张图片

线程池的底层工作原理:

1、在创建了线程池后,等待提交过来的任务请求。

2、当调用execute()方法添加一个请求任务时,线程池会做如下判断:

  • 2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
  • 2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列。
  • 2.3如果这时候队列满了且正在运行的线程数量还小maximumPoolSize,那么还是要创建非核心线程即刻运行这个任务
  • 2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

3、当一个线程完成任务时,它会从队列中取下一个任务来执行。

4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:

  • 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
  • 所以线程池的所有任务完成后它最终会收缩到corePoolSize。

线程池的拒绝策略:

  • 等待队列也已经满了,再也塞不下新任务了,同时,线程池中的max线程也达到了,无法继续为新任务服务。
  • 这时候我们需要拒绝策略机制合理的处理这个问题。

JDK默认的四种拒绝策略:

  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
  2. CalleRunsPolicy:调用者运行一种调节机制,该策略即不会抛出任务,也不会抛出异常,而是将某些任务退回到调用者,
  3. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
  4. DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是崔浩的一种方案。

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。

队列-线程池-ForkJoinPool_第7张图片

1、RUNNING:

  • (1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。 
  • (2) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为不为0

2、 SHUTDOWN

  • (1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务(包含队列里面的任务)。 
  • (2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

3、STOP

  • (1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务(包括队列里面的任务),并且会中断正在处理的任务。 
  • (2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4、TIDYING

  • (1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。 
  • (2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 
  • 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

5、 TERMINATED

  • (1) 状态说明:线程池彻底终止,就变成TERMINATED状态。 
  • (2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

ThreadPoolExecutor中ctl变量的理解

ThreadPoolExecutor中有个int型变量(其实是AtomicInteger类型)ctl:它的作用是存储线程池的状态和工作线程数量,原理是如何实现:
第一个问题是一个变量如何存储两个数据:
首先,int类型是4个字节,也就是32位,例如一个int值在计算机中的表示:
00000000 01000100 11111111 00000000
因为ThreadPoolExecutor中定义的状态有5种(RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED)

  • 用1位表示,则只有0、1两种情况,只能表示两种状态;
  • 用2位表示,有00、01、10、11四种情况,只能表示四种状态;
  • 用3位表示,有222=8种情况;

所以状态值至少要用3位,那么就可以用int的高3位来表示(最左边3个),剩下29个就可以表示线程数量(所以
线程数量最大值就是29位上全是1)

每当线程池中的线程数量或状态发生变化时,具体操作的便是ctl变量,如以下方法:

private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}

那么又是读取线程状态和数量的值呢:
读取状态利用以下方法:
private static int runStateOf(int c) { return c & ~CAPACITY; }
CAPACITY是个常量00011111 11111111 11111111 11111111,通过 &(按位与)运算,
可以保留高3位,把低29位全部变为0;

读取数量利用以下方法:
private static int workerCountOf(int c) { return c & CAPACITY; }
可以把高3位变为0,低29位保留。

其中传入的参数c,就是ctl。

你在工作中单一的/固定数的/可变的三种创建线程池的方法,你用那个多?

  • 一个都不使用,我们生产上只能自定义的

下面是取自阿里的开发手册:

3. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决 资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或 者“过度切换”的问题。

4. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下: 1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

怎么配置线程数?

1、CPU密集

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。

CPU密集任务只有在真正的多核CPU上才能得到加速(通过多线程)

CPU密集型任务配置尽可能少的线程数量:一般公式:CPU核数+1个线程的线程池。

2、IO密集

IO密集型:

第一种:由于IO密集型任务并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2

第二种:IO密集型,即任务需要大量的IO,即大量的阻塞。

在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力在等待。

所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型时,大部分线程都阻塞,故需要多配置线程数:

参考公式:CPU核数/1-阻塞系数(阻塞系数在0.8-0.9之间)  比如8核CPU:8/(1-0.9)=80个线程数

ForkJoinPool


在Java 7中引入了一种新的线程池:ForkJoinPool。

它同ThreadPoolExecutor一样,也实现了Executor和ExecutorService接口。它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。

ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法。这里的要点在于,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。

那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。

所以当使用ThreadPoolExecutor时,使用分治法会存在问题,因为ThreadPoolExecutor中的线程无法像任务队列中再添加一个任务并且在等待该任务完成之后再继续执行。而使用ForkJoinPool时,就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。

比如,我们需要统计一个double数组中小于0.5的元素的个数,那么可以使用ForkJoinPool进行实现如下:

public class ForkJoinTest {
    private double[] d;
    private class ForkJoinTask extends RecursiveTask {
        private int first;
        private int last;
        public ForkJoinTask(int first, int last) {
            this.first = first;
            this.last = last;
        }
        protected Integer compute() {
            int subCount;
            if (last - first < 10) {
                subCount = 0;
                for (int i = first; i <= last; i++) {
                    if (d[i] < 0.5)
                        subCount++;
                    }
                }
            else {
                int mid = (first + last) >>> 1;
                ForkJoinTask left = new ForkJoinTask(first, mid);
                left.fork();
                ForkJoinTask right = new ForkJoinTask(mid + 1, last);
                right.fork();
                subCount = left.join();
                subCount += right.join();
            }
            return subCount;
        }
    }
    public static void main(String[] args) {
        d = createArrayOfRandomDoubles();
        int n = new ForkJoinPool().invoke(new ForkJoinTask(0, 9999999));
        System.out.println("Found " + n + " values");
    }
}

以上的关键是fork()和join()方法。在ForkJoinPool使用的线程中,会使用一个内部队列来对需要执行的任务以及子任务进行操作来保证它们的执行顺序。

那么使用ThreadPoolExecutor或者ForkJoinPool,会有什么性能的差异呢?

首先,使用ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,显然这是不可行的。

当然,在上面的例子中,也可以不使用分治法,因为任务之间的独立性,可以将整个数组划分为几个区域,然后使用ThreadPoolExecutor来解决,这种办法不会创建数量庞大的子任务。代码如下:

public class ThreadPoolTest {
    private double[] d;
    private class ThreadPoolExecutorTask implements Callable {
        private int first;
        private int last;
        public ThreadPoolExecutorTask(int first, int last) {
            this.first = first;
            this.last = last;
        }
        public Integer call() {
            int subCount = 0;
            for (int i = first; i <= last; i++) {
                if (d[i] < 0.5) {
                    subCount++;
                }
            }
            return subCount;
        }
    }
    public static void main(String[] args) {
        d = createArrayOfRandomDoubles();
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(4, 4, Long.MAX_VALUE, TimeUnit.SECONDS, new LinkedBlockingQueue());
        Future[] f = new Future[4];
        int size = d.length / 4;
        for (int i = 0; i < 3; i++) {
            f[i] = tpe.submit(new ThreadPoolExecutorTask(i * size, (i + 1) * size - 1);
        }
        f[3] = tpe.submit(new ThreadPoolExecutorTask(3 * size, d.length - 1);
        int n = 0;
        for (int i = 0; i < 4; i++) {
            n += f.get();
        }
        System.out.println("Found " + n + " values");
    }
}

在分别使用ForkJoinPool和ThreadPoolExecutor时,它们处理这个问题的时间如下:

线程数    ForkJoinPool    ThreadPoolExecutor
1    3.2s    0.31s
4    1.9s    0.15s
对执行过程中的GC同样也进行了监控,发现在使用ForkJoinPool时,总的GC时间花去了1.2s,而ThreadPoolExecutor并没有触发任何的GC操作。这是因为在ForkJoinPool的运行过程中,会创建大量的子任务。而当他们执行完毕之后,会被垃圾回收。反之,ThreadPoolExecutor则不会创建任何的子任务,因此不会导致任何的GC操作。

ForkJoinPool的另外一个特性是它能够实现工作窃取(Work Stealing),在该线程池的每个线程中会维护一个队列来存放需要被执行的任务。当线程自身队列中的任务都执行完毕后,它会从别的线程中拿到未被执行的任务并帮助它执行。

可以通过以下的代码来测试ForkJoinPool的Work Stealing特性:

for (int i = first; i <= last; i++) {
    if (d[i] < 0.5) {
        subCount++;
    }
    for (int j = 0; j < d.length - i; j++) {
        for (int k = 0; k < 100; k++) {
            dummy = j * k + i; // dummy is volatile, so multiple writes occur
            d[i] = dummy;
        }
    }
}

因为里层的循环次数(j)是依赖于外层的i的值的,所以这段代码的执行时间依赖于i的值。当i = 0时,执行时间最长,而i = last时执行时间最短。也就意味着任务的工作量是不一样的,当i的值较小时,任务的工作量大,随着i逐渐增加,任务的工作量变小。因此这是一个典型的任务负载不均衡的场景。

这时,选择ThreadPoolExecutor就不合适了,因为它其中的线程并不会关注每个任务之间任务量的差异。当执行任务量最小的任务的线程执行完毕后,它就会处于空闲的状态(Idle),等待任务量最大的任务执行完毕。

而ForkJoinPool的情况就不同了,即使任务的工作量有差别,当某个线程在执行工作量大的任务时,其他的空闲线程会帮助它完成剩下的任务。因此,提高了线程的利用率,从而提高了整体性能。

这两种线程池对于任务工作量不均衡时的执行时间:

线程数    ForkJoinPool    ThreadPoolExecutor
1    54.5s    53.3s
4    16.6s    24.2s
注意到当线程数量为1时,两者的执行时间差异并不明显。这是因为总的计算量是相同的,而ForkJoinPool慢的那一秒多是因为它创建了非常多的任务,同时也导致了GC的工作量增加。

当线程数量增加到4时,执行时间的区别就较大了,ForkJoinPool的性能比ThreadPoolExecutor好将近50%,可见Work Stealing在应对任务量不均衡的情况下,能够保证资源的利用率。

所以一个结论就是:当任务的任务量均衡时,选择ThreadPoolExecutor往往更好,反之则选择ForkJoinPool。

另外,对于ForkJoinPool,还有一个因素会影响它的性能,就是停止进行任务分割的那个阈值。比如在之前的快速排序中,当剩下的元素数量小于10的时候,就会停止子任务的创建。下表显示了在不同阈值下,ForkJoinPool的性能:

线程数    ForkJoinPool
20    17.8s
10    16.6s
5    15.6s
1    16.8s
可以发现,当阈值不同时,对于性能也会有一定影响。因此,在使用ForkJoinPool时,对此阈值进行测试,使用一个最合适的值也有助于整体性能。

自动并行化(Automatic Parallelization)

在Java 8中,引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行,前提是使用了ForkJoinPool。

Java 8为ForkJoinPool添加了一个通用线程池,这个线程池用来处理那些没有被显式提交到任何线程池的任务。它是ForkJoinPool类型上的一个静态元素,它拥有的默认线程数量等于运行计算机上的处理器数量。

当调用Arrays类上添加的新方法时,自动并行化就会发生。比如用来排序一个数组的并行快速排序,用来对一个数组中的元素进行并行遍历。自动并行化也被运用在Java 8新添加的Stream API中。

比如下面的代码用来遍历列表中的元素并执行需要的计算:

Stream stream = arrayList.parallelStream();
stream.forEach(a -> {
    String symbol = StockPriceUtils.makeSymbol(a);
    StockPriceHistory sph = new StockPriceHistoryImpl(symbol, startDate, endDate, entityManager);
});


对于列表中的元素的计算都会以并行的方式执行。forEach方法会为每个元素的计算操作创建一个任务,该任务会被前文中提到的ForkJoinPool中的通用线程池处理。以上的并行计算逻辑当然也可以使用ThreadPoolExecutor完成,但是就代码的可读性和代码量而言,使用ForkJoinPool明显更胜一筹。

对于ForkJoinPool通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。如果需要调整线程数量,可以通过设置系统属性:-Djava.util.concurrent.ForkJoinPool.common.parallelism=N

下面的一组数据用来比较使用ThreadPoolExecutor和ForkJoinPool中的通用线程池来完成上面简单计算时的性能:

线程数    ThreadPoolExecutor(秒)    ForkJoinPool Common Pool(秒)
1    255.6    135.4
2    134.8    110.2
4    77.0    96.5
8    81.7    84.0
16    85.6    84.6
注意到当线程数为1,2,4时,性能差异的比较明显。线程数为1的ForkJoinPool通用线程池和线程数为2的ThreadPoolExecutor的性能十分接近。

出现这种现象的原因是,forEach方法用了一些小把戏。它会将执行forEach本身的线程也作为线程池中的一个工作线程。因此,即使将ForkJoinPool的通用线程池的线程数量设置为1,实际上也会有2个工作线程。因此在使用forEach的时候,线程数为1的ForkJoinPool通用线程池和线程数为2的ThreadPoolExecutor是等价的。

所以当ForkJoinPool通用线程池实际需要4个工作线程时,可以将它设置成3,那么在运行时可用的工作线程就是4了。

总结
当需要处理递归分治算法时,考虑使用ForkJoinPool。
仔细设置不再进行任务划分的阈值,这个阈值对性能有影响。
Java 8中的一些特性会使用到ForkJoinPool中的通用线程池。在某些场合下,需要调整该线程池的默认的线程数量。

参考:https://blog.csdn.net/dm_vincent/article/details/39505977 

你可能感兴趣的:(面试,基础,线程)