java多线程并发那些事儿

多线程的基本概念

进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。凡是用于完成操作系统的各种功能的进程就是系统进程,而所有由你启动的进程都是用户进程。
在这如图所示每一个正在运行的 .exe 程序都是一个进程。里插入图片描述如图所示每一个正在运行的 .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:表示当拒绝处理任务时的策略,有以下四种取值:

1AbortPolicy:直接抛出异常。
2CallerRunsPolicy:只用调用者所在线程来运行任务。
3DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
4DiscardPolicy:不处理,丢弃掉。
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 已经实现的线程池

  1. SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。
  2. SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
  3. ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。
  4. SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类。
  5. ThreadPoolTaskExecutor :最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装。

异步的方法有:

  1. 最简单的异步调用,返回值为void
  2. 带参数的异步调用,异步方法可以传入参数
  3. 存在返回值,常调用返回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() 可以在多个线程中调用 计算调用次数是所有线程调用次数的总和

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

别假

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值