个人对多线程的应用以及总结
一、创建线程方式
1、通过继承 Thread 类,重写run()方法并调用start()启动线程
2、实现 Runnable 接口
3、函数表达式
二、线程池
在Java中,线程池是用来管理多个线程的集合,避免频繁创建和销毁线程所带来的性能开销。Java提供了 Executor 框架来方便地使用线程池。
2.1、创建方式
1、使用Executors 工具,创建 固定大小线程池 创建一个固定数量的线程,可以确保不会创建超过指定数量的线程,适用于处理负载稳定的任务。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolA {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int taskNumber = i;
executor.submit(() -> {
System.out.println("任务:。。。 " + taskNumber + " 正在线程中运行 " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
2、缓存线程池:可缓存线程池在需要时创建新线程,并在之前构建的线程可用时重用它们。此池适合处理负载变化较大的任务。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolB {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int taskNumber = i;
executor.submit(() -> {
System.out.println("任务:。。。 " + taskNumber + " 正在线程中运行 " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
3、单线程池:确保只有一个线程在执行任务。它常用于需要保证任务顺序的场景。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutorC {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int taskNumber = i;
executor.submit(() -> {
System.out.println("任务:。。。 " + taskNumber + " 正在线程中运行 " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
4、定时线程池:可以在指定的延迟后运行任务,或周期性地运行任务。
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolD {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
executor.schedule(() -> System.out.println("2庙后运行"), 2, TimeUnit.SECONDS);
executor.scheduleAtFixedRate(() -> System.out.println("每1秒执行一次"), 0, 1, TimeUnit.SECONDS); // 定期执行
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
}
}
5、ThreadPoolExecutor 类(重要)
1、是一个更灵活的创建线程池的方式。通过直接实例化该类,您可以自定义线程池的参数,包括核心线程数、最大线程数、空闲线程存活时间、任务队列类型和线程工厂等。
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolExample {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60, // 空闲线程的存活时间
TimeUnit.SECONDS, // 空闲线程存活时间的单位
new ArrayBlockingQueue<>(10) // 阻塞队列
);
for (int i = 0; i < 20; i++) {
final int taskNumber = i;
executor.submit(() -> {
System.out.println("任务... " + taskNumber + " 正在线程中运行 " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
2.2、ThreadPoolExecutor
1、核心参数:
- corePoolSize:线程池中的常驻核心线程数
- maximumPoolSize:线程池中允许的最大线程数
- keepAliveTime:当线程池中的线程数量超过 corePoolSize 时,多余的空闲线程等待新任务的最大时间。
- unit:keepAliveTime 的时间单位。
- workQueue:任务队列,用于在任务通过 execute 方法提交时保存等待执行的任务。
- threadFactory:线程工厂,用于创建线程。
- handler:拒绝策略,用于处理任务队列已满的情况。
2、工作队列:
- SynchronousQueue:这种队列没有容量,任务只有在有空闲线程的情况下才能提交成功。这种队列通常被用于需要立即执行的任务,优先执行当前任务,如果当前没有空闲线程,那么会新建线程执行任务。
- LinkedBlockingQueue:这种队列是基于链表实现的,容量可选,可以无限大。如果容量为正数,那么就是一个固定大小的队列。如果容量为负数,那么队列大小就是无限大的。这种队列通常被用于需要大量等待时间的任务,如 IO 操作等,因为它可以缓存大量的任务。
- ArrayBlockingQueue:这种队列是基于数组实现的,容量固定,不能动态增加。这种队列通常被用于需要有界限的任务,如控制并发数量。
- PriorityBlockingQueue:这种队列是基于优先级排序的,可以实现优先级任务先执行。这种队列通常被用于需要将任务按照一定优先级进行排序执行的任务,比如在线程池中处理任务的优先级。
3、拒绝策略:
- CallerRunsPolicy:使用调用者线程执行该任务,如果提交任务的线程池已满,将会使用调用线程来执行当前任务。这种策略通常被用于对提交任务的队列长度进行控制,而不是对线程数进行控制。
- AbortPolicy:直接抛出拒绝执行异常,从而抛弃任务。(默认)
- DiscardPolicy:直接抛弃添加的任务,不抛出异常。
- DiscardOldestPolicy:抛弃队列头的元素,以便为新的任务提供空间。
通常情况下,如果要保证所有任务一定被执行完毕,可以选择 CallerRunsPolicy。如果要保证线程池中的任务最多只有固定数目,可以选择 AbortPolicy或者 DiscardPolicy。如果要保证任务队列不会满,可以选择 DiscardOldestPolicy。
4、execute() 方法用于向线程池提交任务分为三个步骤
- 如果线程池中的线程数量小于 corePoolSize,尝试创建一个新的线程来执行任务。
- 如果任务可以成功放入任务队列中,需要再次检查线程池的状态,如果线程池已经关闭或者线程池中的线程数量为 0,则需要重新创建一个新的线程。
- 如果任务不能放入任务队列中,尝试创建一个新的线程来执行任务,如果创建失败,则拒绝该任务。ThreadPoolExecutor column:1342
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
5、worker 线程是线程池的核心
Worker 类实现了 Runnable 接口,并且继承了 AQS类,用于实现线程的同步和状态管理。runWorker() 方法用于执行任务分为以下:
- 获取 Worker 线程的第一个任务,如果第一个任务为空,则从任务队列中获取任务。
- 执行任务,如果任务执行过程中出现异常,则将异常抛出。
- 任务执行完成后,更新 Worker 线程的状态和统计信息。
- 如果 Worker 线程的状态为 TERMINATED,则移除该线程。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
6、线程池中核心线程参数设置,零与非零区别
1. 线程池中的核心参数指线程池中一直存在的线程数量,当有新任务提交到线程池时,线程池会优先使用核心线程来执行任务。如果核心线程数为零,则线程池中将没有一直存在的线程,所有的线程都是临时创建的,这样的话,线程池的创建和销毁就会比较大,会影响性能。
2. 如果核心线程为非零,线程池中会一直存在一定数量的线程,可以频繁的创建和销毁线程,提高了性能。一般来说,核心线程数的设置应该根据应用程序的实际需要来决定,如果应用程序需要处理大量并发任务,可以适当增加核心线程数,提高并发能力。但是,如果核心线程数设置过大,会占用过多的系统资源,影响系统资源、稳定性和性能。因此,在设置核心线程数时需要根据实际情况进行权衡和调整。
7、什么需求下设置线程池的核心线程数为零
1. 将线程池的核心线程数设置为零,意味着线程池不会创建任何线程,所有任务都会被放入任务队列中等待执行。这种情况下,只有当有任务提交时,线程池才会创建新的线程来处理任务,直到线程池中的线程数达到最大线程数。
将线程池的核心线程数设置为零,一般用于以下几种情况:
① 任务执行时间较短:如果任务执行时间很短,那么创建线程的开销可能会超过任务执行的时间,此时将线程池的核心线程数设置为 0,可以避免创建大量的线程,从而提高系统的性能和资源利用率。
② 资源受限:如果系统的资源受限,如 CPU、内存等资源较少,那么创建大量的线程可能会导致系统的负载过高,从而影响系统的稳定性和性能。此时将线程池的核心线程数设置为 0,可以避免创建大量的线程,从而减少对系统资源的占用。
注意:将线程池的核心线程数设置为 0,可能会导致任务在队列中等待的时间较长,从而影响任务的响应速度和处理效率。因此,在设置线程池的核心线程数时,需要根据实际情况进行权衡和调整,以达到最优的性能和资源利用率。
8、线程池什么业务条件下使用SynchronousQueue 做队列
SynchronousQueue是一个无界队列,它不会存储任何元素,每个插入操作都必须等待相应的删除操作,否则插入操作将一直阻塞。这意味着在线程池中使用SynchronousQueue作为队列,任务只有在有空闲的线程时才会被执行,否则任务将一直等待,直到有线程可用。
① 任务处理时间短:由于SynchronousQueue没有容量,任务提交后需要立即被处理,如果任务处理时间过长,会导致任务在队列中等待,从而影响系统的性能。
② 任务提交速度与任务处理速度相近:由于 SynchronousQueue 没有容量,如果任务提交速度大于任务处理速度,就会导致任务被直接传递给另一个线程执行,而不是放到队列中等待执行,这样会导致系统的负载增加,可能会出现任务积压的情况
③ 系统负载较低:由于 SynchronousQueue 没有容量,如果系统负载较高,就会导致任务被直接传递给另一个线程执行,而不是放到队列中等待执行,这样会进一步增加系统的负载,可能会出现任务积压的情况。
9、线程池运行机制
① 创建线程池:初始化线程池,并设置线程池的核心线程数、最大线程数、线程空闲时间、工作队列等参数。
② 提交任务:当需要执行任务时,将任务提交到线程池中。
③ 线程调度:线程池中的线程会从工作队列中获取任务,并执行任务。如果工作队列为空,线程会根据设置的空闲时间进入等待状态。
④ 任务执行:线程会执行任务,并根据需要向工作队列中添加新任务。
⑤ 线程销毁:如果线程池中的线程闲置时间超过了设置的空闲时间,线程会被销毁
10、线程池运行流程三个阶段:提交任务、执行任务和关闭线程池
① 提交任务:将任务提交到线程池中,如果线程池中的线程数量小于核心线程数,那么会新建一个线程来执行任务,否则会将任务加入到等待队列中;如果等待队列已满,那么会新建一个线程来执行任务,否则根据设置的策略执行相应操作。
② 执行任务:线程从等待队列中获取任务并执行,如果线程执行完任务后处于空闲状态,那么根据设置的策略决定是否要销毁该线程。
③ 关闭线程池:调用 shutdown() 方法来关闭线程池,该方法不会立即关闭线程池,而是等待正在执行的任务和等待队列中的任务执行完毕后才会关闭线程池。
11、创建线程池方式优缺点
创建线程池的方式有两种:使用Executors工厂类创建和自定义线程池。
11.1. 使用Executors工厂类创建线程池:
优点:
方便快捷:Executors提供了一些静态方法,可以快速创建不同类型的线程池,如newFixedThreadPool()、newCachedThreadPool()等。
简化配置:通过使用预定义的线程池配置,可以简化线程池的创建和配置过程。
缺点:
容易导致资源耗尽:Executors的一些静态方法创建的线程池默认使用无界队列,当任务过多时,可能导致内存溢出。
不可控制:Executors的一些静态方法创建的线程池的配置是固定的,无法对线程池进行灵活的调整和优化。
11.2. 自定义线程池:
优点:
灵活可控:可以根据实际需求自定义线程池的配置,如核心线程数、最大线程数、任务队列类型等,以满足不同的业务需求。
资源控制:可以通过自定义线程池来控制资源的使用,避免资源耗尽的问题。
缺点:
需要手动配置:相比于使用Executors工厂类,自定义线程池需要手动配置线程池的各项参数,需要更多的工作量和技术要求。
容易出错:自定义线程池需要考虑线程安全、任务拒绝策略、线程池的状态管理等问题,容易出现错误。
总结:使用Executors工厂类创建线程池是方便快捷的方式,但可能导致资源耗尽和无法灵活控制线程池。自定义线程池可以灵活控制和配置线程池,但需要手动配置和容易出错。选择创建线程池的方式应根据实际需求和具体情况来决定。
三、Fork/Join 框架
1、Java 7 引入了 Fork/Join 框架,主要用于任务的分解和并行计算,尤其适合处理大规模计算任务。适合用于处理大规模计算或需要通过分治法解决的问题
2、设计目标
①通过将一个大任务分解为多个小任务,来实现任务的并行执行。
②在多个 CPU 核心上高效地利用计算资源,从而提高程序性能。
3、核心组件
①用于运行 Fork/Join 任务的线程池。它可以自动管理可用线程,并工作以实现最大并行度。
②表示将要在 ForkJoinPool 中执行的任务。ForkJoinTask 及其子类允许任务被分叉(fork)并最终合并(join)
4、任务分割
Fork(分叉): 将任务分成更小的子任务并并行执行。
Join(合并): 在所有子任务完成后,合并结果。
5、工作原理
①任务分解: 当一个任务被提交到 ForkJoinPool 时,该任务会被评估是否应当被进一步分解。如果任务足够小,则会被直接执行;如果任务较大,则会被分解为若干个子任务。
②任务执行: 被分解的子任务会被分叉到不同的线程中并行执行。每个小任务将在相应的工作线程中独立运行。
③结果合并: 当所有子任务完成后,主任务会等待所有结果并合并之。结果集通常会通过 join() 方法来获得。
6、优点
高性能: 利用多核处理器,提高应用性能。
易于使用: 对于需要并行处理的算法,将任务分解为子任务非常直观。
任务管理: Fork/Join 框架自动管理任务的分发、调度和执行。
7、缺点
适用场景有限: Fork/Join 框架更适合用于可分解的计算任务,对于不易分解的任务(如I/O处理),则不太适用。
内存开销: 大量的小任务会消耗更多内存,并可能导致频繁的上下文切换。
四、Java8 CompletableFuture(了解)
Java 8引入了 CompletableFuture 类,允许我们进行异步编程,以便在任务完成时获取结果,并支持非阻塞操作。
五、Java中的并发工具类
并发工具类:
1、CountDownLatch: 允许一个或多个线程等待其他线程执行完成。
2、CyclicBarrier: 使一组线程互相同步,直到所有线程都到达某一点。
3、Semaphore: 控制同时访问某特定资源的线程数量。
4、ReentrantLock: 一个比使用 synchronized 关键字更灵活的锁。