Spring中多线程的使用及问题

由于本周大部分时间都在写原型,没有遇到什么问题,再加上之前会议中也多次提到了线程,而我本人对线程没有什么理解于是便有了以下文章。
为什么使用多线程
在我们开发系统过程中,经常会处理一些费时间的任务(如:向数据库中插入大量数据),这个时候就就需要使用多线程。
Springboot中是否对多线程方法进行了封装
是,Spring中可直接由@Async实现多线程操作
如何控制线程运行中的各项参数
通过配置线程池。
线程池ThreadPoolExecutor执行规则如下
Spring中多线程的使用及问题
文章图片

然后我们来认为构造一个线程池来试一下:

@Configuration @EnableAsync public class ThreadPoolConfig implements AsyncConfigurer { /** * 核心线程池大小 */ private static final int CORE_POOL_SIZE = 3; /** * 最大可创建的线程数 */ private static final int MAX_POOL_SIZE = 10; /** * 队列最大长度 */ private static final int QUEUE_CAPACITY = 10; /** * 线程池维护线程所允许的空闲时间 */ private static final int KEEP_ALIVE_SECONDS = 300; /** * 异步执行方法线程池 * * @return */ @Override @Bean public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setMaxPoolSize(MAX_POOL_SIZE); executor.setCorePoolSize(CORE_POOL_SIZE); executor.setQueueCapacity(QUEUE_CAPACITY); executor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS); executor.setThreadNamePrefix("LiMingTest"); // 线程池对拒绝任务(无线程可用)的处理策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }

ThreadPoolExecutor是JDK中的线程池实现,这个类实现了一个线程池需要的各个方法,它提供了任务提交、线程管理、监控等方法。
corePoolSize:核心线程数
线程池维护的最小线程数量,默认情况下核心线程创建后不会被回收(注意:设置allowCoreThreadTimeout=true后,空闲的核心线程超过存活时间也会被回收)。
大于核心线程数的线程,在空闲时间超过keepAliveTime后会被回收。
maximumPoolSize:最大线程数
线程池允许创建的最大线程数量。
当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。
keepAliveTime:空闲线程存活时间
当一个可被回收的线程的空闲时间大于keepAliveTime,就会被回收。
被回收的线程:
设置allowCoreThreadTimeout=true的核心线程。 大于核心线程数的线程(非核心线程)。

workQueue:工作队列
新任务被提交后,如果核心线程数已满则会先添加到工作队列,任务调度时再从队列中取出任务。工作队列实现了BlockingQueue接口。
handler:拒绝策略
当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现RejectedExecutionHandler接口。
JDK默认的拒绝策略有四种:
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。 DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。 CallerRunsPolicy:由调用线程处理该任务。

我们在非测试文件中直接使用new Thread创建新线程时编译器会发出警告:
不要显式创建线程,请使用线程池。 说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题

public class TestServiceImpl implements TestService { private final static Logger logger = LoggerFactory.getLogger(TestServiceImpl.class); @Override public void task(int i) { logger.info("任务: "+i); } }

@Autowired TestService testService; @Test public void test() { for (int i = 0; i < 50; i++) { testService.task(i); }

我们可以看到一切执行正常;
Spring中多线程的使用及问题
文章图片

之后我有对线程进行了一些测试:
class TestServiceImplTest { @Test public void test() { Thread add = new AddThread(); Thread dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count); }static class Counter { public static int count = 0; }class AddThread extends Thread { public void run() { for (int i=0; i<10000; i++) { Counter.count += 1; } } }class DecThread extends Thread { public void run() { for (int i=0; i<10000; i++) { Counter.count -= 1; } } }

一个自增线程,一个自减线程,对0进行同样次数的操作,理应结果仍然为零,但是执行结果却每次都不同。
经过搜索之后发现对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作
例如,对于语句: n +=1; 看似只有一行语句却包括了3条指令:
读取n, n+1, 存储n;

比如有以下两个进程同时对10进行加1操作
Spring中多线程的使用及问题
文章图片

这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待。
static class Counter { public static final Object lock = new Object(); //每个线程都需获得锁才能执行 public static int count = 0; }class AddThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock) { static class Counter { public static final Object lock = new Object(); public static int count = 0; }class DecThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock) { Counter.count -= 1; } } } }

值得注意的是每个类可以设置多个锁,如果线程获取的不是同一个锁则无法起到上述功能;
【Spring中多线程的使用及问题】springBoot中也定义了很多类型的锁,在此就不一一说明了,我们目前能做到的就是注意项目中的异步操作,观察操作所使用的线程,做到在以后项目中遇到此类问题时能及时发现问题,解决问题。

    推荐阅读