线程

Callable

public class CallableDemo {

public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> future = executorService.submit(() -> {
log.info("do something in callable");
Thread.sleep(5000);
return "Done";
});
log.info("do something in main");
Thread.sleep(1000);
String result = future.get();
log.info("result:{}", result);
}
}

FutureTask

多线程执行任务时,有比较耗时操作,但又需要其返回结果时,可以使用FutureTask

public class FutureTaskDemo {

public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<String>(() -> {
log.info("do something in callable");
Thread.sleep(5000);
return "Done";
});

new Thread(futureTask).start();
log.info("do something in main");
Thread.sleep(1000);
// 获取耗时操作的返回结果,这里是堵塞操作
String result = futureTask.get();
log.info("result:{}", result);
}
}

Fork/Join

用于并行执行任务,将大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。使用工作窃取算法,某个线程从其他队列里窃取任务来执行

  • fork:将大任务切割成若干个子任务并行执行
  • join:合并子任务的执行结果得到大任务的结果

局限性

  1. 不能执行I/O操作(读写数据文件)
  2. 不能抛出检查异常,必须通过必要的代码来处理他们
  3. 任务只能使用fork和join操作来作为同步机制,如果使用其他同步机制,那么执行任务时,工作线程就不能执行其他任务

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

/***
* 继承RecursiveTask,返回值为Integer类型
* 覆写computer方法
*/
@Slf4j
public class ForkJoinTaskDemo extends RecursiveTask<Integer> {

// 阈值
public static final int threshold = 2;
private int start;
private int end;

public ForkJoinTaskDemo(int start, int end) {
this.start = start;
this.end = end;
}

@Override
protected Integer compute() {
int retSum = 0;
boolean canCompute = (end - start) <= threshold;
if (canCompute) {
// 如果任务足够小就计算任务
for (int i = start; i <= end; i++) {
retSum += i;
}
} else {
// 如果任务大于阈值,就不断递归分裂成两个子任务计算
int middle = (start + end) / 2;
ForkJoinTaskDemo leftTask = new ForkJoinTaskDemo(start, middle);
ForkJoinTaskDemo rightTask = new ForkJoinTaskDemo(middle + 1, end);

// 执行子任务
leftTask.fork();
rightTask.fork();

// 等待任务执行结束合并其结果
int leftResult = leftTask.join();
int rightResult = rightTask.join();

// 合并子任务
retSum = leftResult + rightResult;
}
return retSum;
}

public static void main(String[] args) {
ForkJoinPool forkjoinPool = new ForkJoinPool();

// 生成一个计算任务,计算1+2+3+4
ForkJoinTaskDemo task = new ForkJoinTaskDemo(1, 100);

// 执行一个任务
Future<Integer> result = forkjoinPool.submit(task);

try {
log.info("result:{}", result.get());
} catch (Exception e) {
log.error("exception", e);
}
}
}

归并排序

package test.thread.pool.merge;

import java.util.Arrays;
import java.util.Random;

/**
* 归并排序
* @author yinwenjie
*/
public class Merge1 {

private static int MAX = 10000;

private static int inits[] = new int[MAX];

// 这是为了生成一个数量为MAX的随机整数集合,准备计算数据
// 和算法本身并没有什么关系
static {
Random r = new Random();
for(int index = 1 ; index <= MAX ; index++) {
inits[index - 1] = r.nextInt(10000000);
}
}

public static void main(String[] args) {
long beginTime = System.currentTimeMillis();
int results[] = forkits(inits);
long endTime = System.currentTimeMillis();
// 如果参与排序的数据非常庞大,记得把这种打印方式去掉
System.out.println("耗时=" + (endTime - beginTime) + " | " + Arrays.toString(results));
}

// 拆分成较小的元素或者进行足够小的元素集合的排序
private static int[] forkits(int source[]) {
int sourceLen = source.length;
if(sourceLen > 2) {
int midIndex = sourceLen / 2;
int result1[] = forkits(Arrays.copyOf(source, midIndex));
int result2[] = forkits(Arrays.copyOfRange(source, midIndex , sourceLen));
// 将两个有序的数组,合并成一个有序的数组
int mer[] = joinInts(result1 , result2);
return mer;
}
// 否则说明集合中只有一个或者两个元素,可以进行这两个元素的比较排序了
else {
// 如果条件成立,说明数组中只有一个元素,或者是数组中的元素都已经排列好位置了
if(sourceLen == 1
|| source[0] <= source[1]) {
return source;
} else {
int targetp[] = new int[sourceLen];
targetp[0] = source[1];
targetp[1] = source[0];
return targetp;
}
}
}

/**
* 这个方法用于合并两个有序集合
* @param array1
* @param array2
*/
private static int[] joinInts(int array1[] , int array2[]) {
int destInts[] = new int[array1.length + array2.length];
int array1Len = array1.length;
int array2Len = array2.length;
int destLen = destInts.length;

// 只需要以新的集合destInts的长度为标准,遍历一次即可
for(int index = 0 , array1Index = 0 , array2Index = 0 ; index < destLen ; index++) {
int value1 = array1Index >= array1Len?Integer.MAX_VALUE:array1[array1Index];
int value2 = array2Index >= array2Len?Integer.MAX_VALUE:array2[array2Index];
// 如果条件成立,说明应该取数组array1中的值
if(value1 < value2) {
array1Index++;
destInts[index] = value1;
}
// 否则取数组array2中的值
else {
array2Index++;
destInts[index] = value2;
}
}

return destInts;
}
}

归并排序

/**
* 使用Fork/Join框架的归并排序算法
* @author yinwenjie
*/
public class Merge2 {

private static int MAX = 100000000;

private static int inits[] = new int[MAX];

// 同样进行随机队列初始化,这里就不再赘述了
static {
......
}

public static void main(String[] args) throws Exception {
// 正式开始
long beginTime = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
MyTask task = new MyTask(inits);
ForkJoinTask<int[]> taskResult = pool.submit(task);
try {
taskResult.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace(System.out);
}
long endTime = System.currentTimeMillis();
System.out.println("耗时=" + (endTime - beginTime));
}

/**
* 单个排序的子任务
* @author yinwenjie
*/
static class MyTask extends RecursiveTask<int[]> {

private int source[];

public MyTask(int source[]) {
this.source = source;
}

/* (non-Javadoc)
* @see java.util.concurrent.RecursiveTask#compute()
*/
@Override
protected int[] compute() {
int sourceLen = source.length;
// 如果条件成立,说明任务中要进行排序的集合还不够小
if(sourceLen > 2) {
int midIndex = sourceLen / 2;
// 拆分成两个子任务
MyTask task1 = new MyTask(Arrays.copyOf(source, midIndex));
task1.fork();
MyTask task2 = new MyTask(Arrays.copyOfRange(source, midIndex , sourceLen));
task2.fork();
// 将两个有序的数组,合并成一个有序的数组
int result1[] = task1.join();
int result2[] = task2.join();
int mer[] = joinInts(result1 , result2);
return mer;
}
// 否则说明集合中只有一个或者两个元素,可以进行这两个元素的比较排序了
else {
// 如果条件成立,说明数组中只有一个元素,或者是数组中的元素都已经排列好位置了
if(sourceLen == 1
|| source[0] <= source[1]) {
return source;
} else {
int targetp[] = new int[sourceLen];
targetp[0] = source[1];
targetp[1] = source[0];
return targetp;
}
}
}

private int[] joinInts(int array1[] , int array2[]) {
// 和上文中出现的代码一致
}
}
}

BlockingQueue

堵塞队列,有两种情况会堵塞

  1. 队列满时,入队线程会被堵塞
  2. 队列空时,出对线程会被堵塞

操作

Throws Exception

Special Value

Blocks

Times Out

添加

add(o)

offer(o)

put(o)

offer(0,timeout,timeunit)

移除

remove(o)

poll()

take()

poll(timeout,timeunit)

检查

element()

peek()

线程池

ThreadPoolExecutor

Executors

使用场景

想要频繁的创建和销毁线程的时候

线程池的概念

线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的

线程池的优势

  • 降低创建线程和销毁线程的性能开销
  • 提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行
  • 合理的设置线程池大小(限流)可以避免因为线程数超过硬件资源瓶颈带来的问题

Api Executors

newFixedThreadPool

该方法返回一个固定数量的线程池,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中,等待有空闲的线程去执行,用途:FixedThreadPool 用于负载比较大的服务器,为了资源的合理利用,需要限制当前线程数量。

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);

ThreadPoolExecutor(corePoolSize, maximumPoolSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

newSingleThreadExecutor

创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

newCachedThreadPool

根据实际情况调整线程个数,不限制最大线程数,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在 60 秒后自动回收

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

newScheduledThreadPool

创建一个可以指定线程的数量的线程池,但是这个线程池还带有延迟和周期性执行任务的功能,类似定时器

ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory);

线程池参数

ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后 (当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而 步骤2不需要获取全局锁。

线程池中线程总数、运行线程数、空闲线程数、任务队列等之间的关系

  1. 当【运行的线程数 < corePoolSize】,则直接创建新线程来处理任务,即使线程池中的其他线程是空闲的
  2. 当【corePoolSize <= 线程池中线程数 < maximumPoolSize】,则只有当workQueue满时,才创建新线程处理
  3. 当【corePoolSize = maximumPoolSize】,在workQueue没满时,那么请求会放入workQueue,等待空闲线程去除任务处理
  4. 当【运行的线程数 > maximumPoolSize】,如果workQueue已满,那么会根据指定策略来处理提交过来的任务
ThreadPoolExecutor(int corePoolSize,                  //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //超出核心线程数量以外的线程空余存活时间
TimeUnit unit, //存活时间单位
BlockingQueue<Runnable> workQueue, //保存执行任务的队列
ThreadFactory threadFactory, //创建新线程使用的工厂
RejectedExecutionHandler handler) //当任务无法执行的时候的处理方式

饱和策略

RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状 态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法 处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。

  1. AbortPolicy:直接抛出异常。
  2. CallerRunsPolicy:只用调用者所在线程来运行任务。
  3. DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
  4. DiscardPolicy:不处理,丢弃掉。

当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。

任务提交

  1. execute();//任务提交
  2. submit(); //带有返回值的任务提交

最佳线程数

最佳线程数目 = (线程等待时间+任务执行时间)/任务执行时间 * CPU数目

备注:这个公式也是前辈们分享的,当然之前看了淘宝前台系统优化实践的文章,和上面的公式很类似,不过在CPU数目那边,他们更细化了,上面的公式只是参考。不过不管什么公式,最终还是在生产环境中运行后,再优化调整。

例如服务器CPU核数为4核,一个任务线程cpu耗时为20ms,线程等待(网络IO、磁盘IO)耗时80ms,那最佳线程数目:( 80 + 20 )/20 * 4 = 20。也就是设置20个线程数最佳。

合理地配置线程池

CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量 将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过 ​​Runtime.getRuntime().availableProcessors()​​方法获得当前设备的CPU个数。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。

建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点 儿,比如几千。有一次,我们系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任 务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线 程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻 塞,任务积压在线程池里。如果当时我们设置成无界队列,那么线程池的队列就会越来越多, 有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然,我们的系统所 有的任务是用单独的服务器部署的,我们使用不同规模的线程池完成不同类型的任务,但是 出现这样问题时也会影响到其他任务。

任务的性质

  1. CPU密集型任务、
  2. IO密集型任务和混合型任务

多线程最佳实践

  1. 使用本变量地
  2. 使用不可变类
  3. 最小化锁的作用域范围:S=1/(1-a+a/n)
  4. 使用线程池,而不是直接new Thread()
  5. 使用同步也不要使用wait和notify
  6. 使用BlockingQueue实现生产消费模式
  7. 使用并发集合而不是加锁的同步集合
  8. 使用Semaphore创建有界访问
  9. 使用同步块而不是同步方法
  10. 避免使用静态变量,否则用final等不可变类

使用案例

package com.insightfullogic.java8.concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
* @description: 线程测试类
* @author: tiger
* @create: 2022-10-07 11:33
*/
public class MutilThread {

// 建立一个线程池,注意要放在外面,不要每次执行代码就建立一个,具体线程池的使用就不展开了
public static ExecutorService commonThreadPool = new ThreadPoolExecutor(5, 5, 300L,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy());

public static void main(String[] args) {
// 开始多线程调用
List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 12; i++) {
int finalI = i;
Future<String> future = (Future<String>) commonThreadPool.submit(() -> {
System.out.println(finalI);
});
futures.add(future);
}

// 获取结果
List<String> list = new ArrayList<>();
try {
for (int i = 0; i < futures.size(); i++) {
list.add(futures.get(i).get());
}
} catch (Exception e) {
// LOGGER.error("出现错误:", e);
}
}
}

顺序调用

CompletableFuture<A> futureA = CompletableFuture.supplyAsync(() -> doA());
CompletableFuture<B> futureB = CompletableFuture.supplyAsync(() -> doB());
CompletableFuture.allOf(futureA,futureB) // 等a b 两个任务都执行完成

C c = doC(futureA.join(), futureB.join());

CompletableFuture<D> futureD = CompletableFuture.supplyAsync(() -> doD(c));
CompletableFuture<E> futureE = CompletableFuture.supplyAsync(() -> doE(c));
CompletableFuture.allOf(futureD,futureE) // 等d e两个任务都执行完成

return doResult(futureD.join(),futureE.join());

线程池的监控

如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根 据线程池的使用状况快速定位问题。可以通过线程池提供的参数进行监控,在监控线程池的 时候可以使用以下属性。

  1. taskCount:线程池需要执行的任务数量。
  2. completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
  3. largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。
  4. getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销 毁,所以这个大小只增不减。
  5. getActiveCount:获取活动的线程数。

通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的 beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。 这几个方法在线程池里是空方法。

死锁

所谓死锁是指两个或两个以上的进程在执行过程中因争夺资源而相互等待的现象;如果没有外力作用,他们则无法推进下去。

产生死锁的原因

  1. 因为系统资源不足
  2. 进程运行推进的顺序不合适
  3. 资源分配不当等

产生死锁的必要条件

  1. 互斥条件
  2. 请求和保持条件
  3. 不剥夺条件
  4. 环路等待条件
package com.tiger.deadLock;

import java.util.concurrent.TimeUnit;
/**
* 死锁
* @author tiger
* @Date 2017年7月27日
*/
public class DeadLock {

public static void main(String[] args) {

Park task = new Park();
// 章鱼线程
Thread th1 = new Thread(task,"章鱼");
// 光头线程
Thread th2 = new Thread(task,"光头");
th1.start();
th2.start();
}
}
class Park implements Runnable{
//两人共同拥有相同的两把锁
String[] locks = {"0","1"};
@Override
public void run() {
String name = Thread.currentThread().getName();
switch( name ){
//光头用 0 号卡进尖叫地带"。
case "光头":尖叫地带( locks[0] ); break;
//章鱼用 1 号卡进海底世界"。
case "章鱼":海底世界( locks[1] );break;
}
}
/**
* 光头:持0号卡进外围的尖叫地带,玩一阵子后,想持另外一张卡(1号卡)进恐怖森林,但此时0号卡被占用。
* @param card
*/
public void 尖叫地带(String card){
String name = Thread.currentThread().getName();
//card 1 先进尖叫地带
synchronized (card) {
System.out.println(name+" 进到尖叫地带");
//进去玩耍2秒
try {TimeUnit.SECONDS.sleep(3);}
catch (InterruptedException e) {}
/*在外围玩耍完后,想进一步进到恐怖森林时,
手头只有1号卡可以使用,此时1号卡被其他人(线程)持有还没有释放,
因此进不了,只能在外头干等,此时另外一个人也是这种情况,所以造成死锁。*/
System.out.println(name+" 准备进到恐怖盛林");
synchronized (locks[1]) {
System.out.println(name+" 进到恐怖盛林");
}
}
}
/**
* 章鱼:持1号卡进外围的海底世界,玩一阵子后,想持另外一张卡(0号卡)进东海龙宫,但此时0号卡被占用。
* @param card
*/
public void 海底世界(String card){
String name = Thread.currentThread().getName();
// 持1号卡先进海底世界
synchronized (card) {
System.out.println(name+" 进到海底世界");
// 进去玩耍2秒
try {TimeUnit.SECONDS.sleep(3);}
catch (InterruptedException e) {}
/*在外围玩耍完后,想进一步进到东海龙宫时,
手头只有0号卡可以使用,此时0号卡被其他人(线程)持有还没有释放,
因此进不了,只能在外头干等,此时另外一个人也是这种情况,所以造成死锁。*/
System.out.println(name+" 准备进到东海龙宫");
synchronized (locks[0]) {
System.out.println(name+" 进到东海龙宫");
}
}
}
}