高并发编程
多线程的基本概念
进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。凡是用于完成操作系统的各种功能的进程就是系统进程,而所有由你启动的进程都是用户进程。
如图所示每一个正在运行的 .exe 程序都是一个进程。
线程是进程中的实际运行单位,是独立运行于进程之中的子任务。是操作系统进行运算调度的最小单位。可理解为线程是进程中的一个最小运行单元。
进程和线程之间的关系
一个进程下包含 N 个线程。
举例说明:玩英雄联盟的时候,打开客户端便启动了许多个线程:排队队列线程、好友聊天线程、正在支付线程。在英雄联盟这一个进程之下便启动了 N 个线程。
我们初学 java 边写代码的时候,通常使用 main 方法进行运行,此时 main 方法执行的便是一个主线程,而所谓的多线程,即是在主线程执行的过程中,同时执行其他的线程。但是同时执行多个线程容易出现报错现象,例如同时同分同秒,两个线程同时修改一个 txt、数据库表文件,或第一个线程没有修改完 txt、数据库表文件,第二个线程同时也去修改。这便是线程之间的混乱、资源竞争、脏读,便是程序员需要去解决的疑难杂症。
多线程的三种创建方式(直接上代码)
1.创建多线程 —— 继承 Thread
public class test0 {
public static void main(String[] args) {
Thread MyThread = new MyThread();
MyThread.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello myThread" + Thread.currentThread().getName());
}
}
2.创建多线程 —— 实现 Runnable
public class test0 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("hello myRunnable" + Thread.currentThread().getName());
}
}
3.实现多线程返回值 —— 实现 Callable
public class study2 {
public static void main(String[] args) {
MyCallable MyCallable = new MyCallable("张方兴");
String call = null;
try {
call = MyCallable.call();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(call);
}
}
class MyCallable implements Callable<String>{
private String name;
public MyCallable(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
return "call:" + name;
}
}
4.实现多线程传参 —— 有参构造
class ThreadA extends Thread{
private String age;
public ThreadA(String age){
this.age = age;
}
@Override
public void run() {
System.out.println("age=" + age);
}
}
public class study1 {
public static void main(String[] args) {
String age = new String("12");
ThreadA a = new ThreadA(age);
a.start();
}
}
无论 extendsThread 还是 implementsRunnable ,传参都需要使用线程初始化的有参构造形式,达到多线程传参的目的。也可以做到重载有参构造,传入各式对象。
线程池的几种创建方式
一. 通过Executors工厂方法创建
package com.javaBase.LineDistancePond;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
- @author kzx
- @since 2021-07-14
*/
public class TestThreadPoolExecutor {
public static void main(String[] args) {
//创建使用单个线程的线程池
ExecutorService es1 = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
es1.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行任务");
}
});
}
//创建使用固定线程数的线程池
ExecutorService es2 = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
es2.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行任务");
}
});
}
//创建一个会根据需要创建新线程的线程池
ExecutorService es3 = Executors.newCachedThreadPool();
for (int i = 0; i < 20; i++) {
es3.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行任务");
}
});
}
//创建拥有固定线程数量的定时线程任务的线程池
ScheduledExecutorService es4 = Executors.newScheduledThreadPool(2);
System.out.println("时间:" + System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
es4.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间:"+System.currentTimeMillis()+"--"+Thread.currentThread().getName() + "正在执行任务");
}
},3, TimeUnit.SECONDS);
}
//创建只有一个线程的定时线程任务的线程池
ScheduledExecutorService es5 = Executors.newSingleThreadScheduledExecutor();
System.out.println("时间:" + System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
es5.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间:"+System.currentTimeMillis()+"--"+Thread.currentThread().getName() + "正在执行任务");
}
},3, TimeUnit.SECONDS);
}
}
}
二. 通过new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
workQueue)自定义创建
参数含义:
corePoolSize:核心池的大小
maximumPoolSize:线程池最大线程数
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
PriorityBlockingQueue
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和SynchronousQueue。线程池的排队策略与BlockingQueue有关。
threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程做些更有意义的事情,比如设置daemon和优先级等等
handler:表示当拒绝处理任务时的策略,有以下四种取值:
1、AbortPolicy:直接抛出异常。
2、CallerRunsPolicy:只用调用者所在线程来运行任务。
3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
4、DiscardPolicy:不处理,丢弃掉。
5、也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
线程池的处理机制:
如果当前线程池的线程数还没有达到基本大小(poolSize < corePoolSize),无论是否有空闲的线程新增一个线程处理新提交的任务;
如果当前线程池的线程数大于或等于基本大小(poolSize >= corePoolSize) 且任务队列未满时,就将新提交的任务提交到阻塞队列排队,等候处理workQueue.offer(command);
如果当前线程池的线程数大于或等于基本大小(poolSize >= corePoolSize) 且任务队列满时;当前poolSize<maximumPoolSize,那么就新增线程来处理任务;
当前poolSize=maximumPoolSize,那么意味着线程池的处理能力已经达到了极限,此时需要拒绝新增加的任务。至于如何拒绝处理新增的任务,取决于线程池的饱和策略RejectedExecutionHandler。
Spring boot中配置线程池
1.自定义线程池
/**
* 线程池配置
* @author kzx
*/
@Configuration
@EnableAsync
public class TaskPoolConfig {
@Bean
public ThreadPoolTaskExecutor taskExecutor(){
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
/**
* 核心线程数(默认线程数)
*/
taskExecutor.setCorePoolSize(10);
/**
* 最大线程数
*/
taskExecutor.setMaxPoolSize(50);
/**
* 允许线程空闲时间(单位:默认为秒)
*/
taskExecutor.setKeepAliveSeconds(10);
/**
* 缓冲队列大小
*/
taskExecutor.setQueueCapacity(200);
/**
* 线程池名前缀
*/
taskExecutor.setThreadNamePrefix("taskExecutor--");
/**
* 线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用
*/
taskExecutor.setAllowCoreThreadTimeOut(true);
/**
* 等待所有任务结束后再关闭线程池
*/
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
/**
* 关闭线程池等待运行的时长
*/
taskExecutor.setAwaitTerminationSeconds(30);
/**
* 拒绝处理策略
* CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
* AbortPolicy():直接抛出异常。
* DiscardPolicy():直接丢弃。
* DiscardOldestPolicy():丢弃队列中最老的任务。
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
/**
* 线程装饰,可以进行一些日志打印,设置请求头等操作
*/
taskExecutor.setTaskDecorator(ContextCopyingDecorator());
taskExecutor.initialize();
return taskExecutor;
}
@Slf4j
static class ContextCopyingDecorator implements TaskDecorator {
@Nonnull
@Override
public Runnable decorate(@Nonnull Runnable runnable) {
log.info("装饰前");
//子线程逻辑
return () -> {
try {
log.info("打印日志-开始");
runnable.run();
} finally {
log.info("打印日志-结束");
}
};
}
}
}
2.默认线程池配置
/**
* 重写默认线程池配置
* @author kzx
*/
@Slf4j
@Configuration
@EnableAsync
public class OverrideDefaultThreadPoolConfig implements AsyncConfigurer {
@Autowired
private TaskThreadPoolConfig config;
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程池大小
executor.setCorePoolSize(config.getCorePoolSize());
//最大线程数
executor.setMaxPoolSize(config.getMaxPoolSize());
//队列容量
executor.setQueueCapacity(config.getQueueCapacity());
//活跃时间
executor.setKeepAliveSeconds(config.getKeepAliveSeconds());
//线程名字前缀
executor.setThreadNamePrefix("default-thread-");
/*
当poolSize已达到maxPoolSize,如何处理新任务(是拒绝还是交由其它线程处理)
CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
*/
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
/**
* 异步任务中异常捕获
*
* @return
*/
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("==========================" + ex.getMessage() + "=======================", ex);
log.error("exception method:" + method.getName());
};
}
}
@Async调用线程池为什么推荐使用自定义线程池的模式?
Spring 已经实现的线程池
- SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。
- SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
- ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。
- SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类。
- ThreadPoolTaskExecutor :最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装。
异步的方法有:
- 最简单的异步调用,返回值为void
- 带参数的异步调用,异步方法可以传入参数
- 存在返回值,常调用返回Future
@Async应用默认线程池
Spring应用默认的线程池,指在@Async注解在使用时,不指定线程池的名称。查看源码,@Async的默认线程池为SimpleAsyncTaskExecutor。
a. 无返回值调用
基于@Async无返回值调用,直接在使用类,使用方法(建议在使用方法)上,加上注解。若需要抛出异常,需手动new一个异常抛出。
1 /**
2 * 带参数的异步调用 异步方法可以传入参数
3 * 对于返回值是void,异常会被AsyncUncaughtExceptionHandler处理掉
4 * @param s
5 */
6 @Async
7 public void asyncInvokeWithException(String s) {
8 log.info("asyncInvokeWithParameter, parementer={}", s);
9 throw new IllegalArgumentException(s);
10 }
b. 有返回值Future调用
1 /**
2 * 异常调用返回Future
3 * 对于返回值是Future,不会被AsyncUncaughtExceptionHandler处理,需要我们在方法中捕获异常并处理
4 * 或者在调用方在调用Futrue.get时捕获异常进行处理
5 *
6 * @param i
7 * @return
8 */
9 @Async
10 public Future<String> asyncInvokeReturnFuture(int i) {
11 log.info("asyncInvokeReturnFuture, parementer={}", i);
12 Future<String> future;
13 try {
14 Thread.sleep(1000 * 1);
15 future = new AsyncResult<String>("success:" + i);
16 throw new IllegalArgumentException("a");
17 } catch (InterruptedException e) {
18 future = new AsyncResult<String>("error");
19 } catch(IllegalArgumentException e){
20 future = new AsyncResult<String>("error-IllegalArgumentException");
21 }
22 return future;
23 }
@scheduled遇到的坑,如何配置多线程定时
原因分析:Spring中@scheduled默认是单线程的串行执行,当定时任务1一直在执行,定时任务2一直在等待定时任务1执行完成。这就导致了生产上定时任务全部卡死的现象。这个ssh的工具包很久没更更新过了,也没有设置例如httpclient的超时时间之类的。这就很难办了!果断放弃!!
解决方案:
一.添加配置
方法一:
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(50));
}
}
方法二:
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(20);
return taskScheduler;
}
这个方法,在程序启动后,会逐步启动50个线程,放在线程池中。每个定时任务会占用1个线程。但是相同的定时任务,执行的时候,还是在同一个线程中。也会造成单此任务的卡死现象
二.配置多线程定时处理任务
@EnableAsync
@Configuration
public class TaskPoolConfig {
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(10);
executor.setThreadNamePrefix("task-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
return executor;
}
}
在使用的时候注入定时线程池:
/**
* @author kzx
*/
@Component
public class GlobalTask {
public static ConcurrentHashMap<String, ScheduledFuture> taskMap = new ConcurrentHashMap<>();
private ScheduledFuture future;
@Autowired
private TestTask testTask ;
@Autowired
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
public void stopCron(DemoRunnable demoRunnable) {
if (taskMap.get(demoRunnable.getTaskName()) != null) {
taskMap.get(demoRunnable.getTaskName()).cancel(true);
taskMap.remove(demoRunnable.getTaskName());
}
}
public boolean triggerAgain(DemoRunnable demoRunnable, String cron) {
this.stopCron(demoRunnable);
future = threadPoolTaskScheduler.schedule(demoRunnable, new CronTrigger(cron));
taskMap.put(demoRunnable.getTaskName(), future);
return true;
}
public void startTestTask() {
triggerAgain(testTask, "0 0 9 * * ?");
}
}
定义一个任务接口继承Runnable (构造定义线程名称的方法)
public interface DemoRunnable extends Runnable {
String getTaskName();
}
@RequiredArgsConstructor
@Component
@Slf4j
public class TestTask implements DemoRunnable {
private static final String TASK_NAME = "TestTask";
private final IUserBillService userBillService;
@Override
public String getTaskName() {
return TASK_NAME ;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void run() {
//写业务
}
项目初始化的时候去调用定时任务:
@Component
@Slf4j
public class TaskRunner implements ApplicationRunner {
@Autowired
private GlobalTask globalTask;
@Override
public void run(ApplicationArguments args) {
globalTask.startTestTask();
log.info("Start scheduled task success.");
}
}
这种方法,每次定时任务启动的时候,都会创建一个单独的线程来处理。也就是说同一个定时任务也会启动多个线程处理。
例如:任务1和任务2一起处理,但是线程1卡死了,任务2是可以正常执行的。且下个周期,任务1还是会正常执行,不会因为上一次卡死了,影响任务1。
但是任务1中的卡死线程越来越多,会导致50个线程池占满,还是会影响到定时任务。
这时候,可能会几个月发生一次~到时候再重启就行了!
CountDownLatch 的 .await() 的线程阻塞 和countDown() 计时唤醒
public class Test {
public static void main(String[] args) {
CountDownLatch begin = new CountDownLatch(1);
CountDownLatch end = new CountDownLatch(2);
for(int i=0; i<2; i++){
Thread thread = new Thread(new Player(begin,end));
thread.start();
}
try{
System.out.println("the race begin");
begin.countDown();
end.await();
System.out.println("the race end");
}catch(Exception e){
e.printStackTrace();
}
}
}
/**
* 选手
*/
class Player implements Runnable{
private CountDownLatch begin;
private CountDownLatch end;
Player(CountDownLatch begin,CountDownLatch end){
this.begin = begin;
this.end = end;
}
public void run() {
try {
begin.await();
System.out.println(Thread.currentThread().getName() + " arrived !");;
end.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
总结:
1、CountDownLatch end = new CountDownLatch(N); //构造对象时候 需要传入参数N
2、end.await() 能够阻塞线程 直到调用N次end.countDown() 方法才释放线程
3、end.countDown() 可以在多个线程中调用 计算调用次数是所有线程调用次数的总和