线程池开发实例:
用于oss文件上传时开启线程:
实例一、
Appication启动类:
@Bean(name = "Oss2CdnTaskExecutor")
public TaskExecutor Oss2CdnTaskExecutor() {
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
int max = Runtime.getRuntime().availableProcessors() + 1;
executor.setCorePoolSize(max);
// 设置最大线程数
executor.setMaxPoolSize(max+1);
// 设置队列容量
executor.setQueueCapacity(200);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(60);
// 设置默认线程名称
executor.setThreadNamePrefix("Oss2CdnTaskExecutor-pool-");
// 设置拒绝策略,该任务被线程池拒绝,由调用 execute方法的线程执行该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
实体层:
@Override
@Async("Oss2CdnTaskExecutor")//异步调用注解
public String generateCdnUrlAsync(String ossUrl) {
return generateCdnUrl(ossUrl);
}
实例二、
public class ExecutorUtils {
/** 核心线程数,线程闲置的时候也不会收回,当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理*/
private static final int CORE_POOL_SIZE = 50;
/**最大线数,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务*/
private static final int MAX_POOL_SIZE = 100;
/**当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize*/
private static final Long KEEP_ALIVE_TIME = 0L;
/** 当核心线程数达到最大时,新任务会放在队列中排队等待执行*/
private static final int QUEUE_LENGTH = 30000;
public static ThreadPoolExecutor SCHEDULED_THREAD_POOL = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(QUEUE_LENGTH),
new ThreadFactory(){
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "cloud_thread_getToken_" + r.hashCode());
}},
new ThreadPoolExecutor.AbortPolicy());
public static ExecutorService getThreadPool() {
return SCHEDULED_THREAD_POOL;//返回值是上面new的线程变量对象
}
}
在SpringBoot中使用异步调用是很简单的,只需要使用@Async注解即可实现方法的异步调用。
注意:需要在启动类或配置类加入@EnableAsync使异步调用@Async注解生效。
1、什么是线程池
java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池
一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列((askQueue):用于存放没有处理的任务。提供一种缓冲机制。
2.线程池的优势:
- 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
- Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
new Thread 缺点- 每次new Thread新建对象性能差。
- 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
- 缺乏更多功能,如定时执行、定期执行、线程中断。
3、Executors提供四种线程池
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
一般都不用Executors提供的线程创建方式,使用ThreadPoolExecutor创建线程池
4.不建议使用excutros
规避资源耗尽的风险
说明:定义好的线程池创建方式
Executors返回的线程池对象的弊端如下:
1:FixedThreadPool 和 SingleThreadPool:
允许的请求队列(底层实现是LinkedBlockingQueue)长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2:CachedThreadPool 和 ScheduledThreadPool
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
创建线程池的正确姿势
避免使用Executors创建线程池,主要是避免使用其中的默认实现
那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。或者是使用开源类库:
private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));
6、Java线程池七个参数详解
1.corePoolSize 线程池核心线程大小:
(1)在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,理解为今日当值线程。
(2)当线程池中的线程数目达到corePoolSize后,就会把到达的任务放入到缓存队列当中。
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize<br />**2.maximumPoolSize 线程池最大线程数量**<br /> 一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,会执行拒绝策略,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。<br />**3.keepAliveTime 空闲线程存活时间**<br /> 一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,直到线程中的线程数不大于corepoolSIze,这里的指定时间由keepAliveTime来设定
4.unit 空闲线程存活时间单位
keepAliveTime的计量单位
5、workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列,当线程池中线程数量达到核心线程池数量后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
六、threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
七、handler 拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
①CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
为线程池默认的拒绝策略,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做=直接抛弃不处理。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
7.线程池流程分析
线程池中线程数**小于corePoolSize**时,**新任务将创建**一个新线程执行任务,不论此时线程池中存在空闲线程;<br /> 线程池中线程数**达到corePoolSize**时,**新任务将被放入workQueue中**,等待线程池中任务调度执行;<br />当workQueue已满,且maximumPoolSize>corePoolSize时,新任务会创建新线程执行任务;<br />当workQueue已满,且提交任务数超过maximumPoolSize,任务由RejectedExecutionHandler处理;<br /> 当线程池中线程数**超过corePoolSize**,且超过这部分的空闲时间达到keepAliveTime时,**回收该线程**;<br /> 如果设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收;
8、线程池的五种状态
running,showdown,stop,Tiding,Terminated
runing:
线程池处于运行状态,可以接受任务,执行任务,创建线程默认就是这个状态。
showDown
调用showdown()函数,不会接受新任务,但是会慢慢处理完堆积的任务。
Stop
调用showdownnow()函数,不会接受新任务,不处理已有任务,会中断现有任务。
Tiding
当线程池状态为showdown或者stop,任务数量为0,就会变为tidying。这个时候会调用钩子函数terminated()。
Terminated:
termiinated()执行完成
9.线程和进程的差别
**进程**是程序的一次执行,
线程可以理解为进程中执行的一段程序片段
**进程**是独立的,进程是无法突破进程边界存取其他进程内的存储空间,
线程由于处于进程空间内,所以同一进程所产生的线程共享同一内存空间,线程是属于进程的,当进程退出时,该进程所产生的线程都会被强制退出并清除
10、创建线程的三种方式
第一种方法: 继承Thread类,重写run()方法,run()方法代表线程要执行的任务。
第二种方法: 实现Runnable接口,重写run()方法,run()方法代表线程要执行的任务。
第三种方法: 实现callable接口,重写call()方法,call()作为线程的执行体,具有返回值,并且可以对异常进行声明和抛出
返回值是创建的新线程,new前面的线程对象
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
public static ThreadPoolExecutor SCHEDULED_THREAD_POOL = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(QUEUE_LENGTH)
优缺点:
使用继承的方式编写代码简单,适用于单继承的情况。
使用实现接口的方式避免单继承的局限性,实现资源共享。
推荐使用实现Runnable的方式。
在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法,等待Callable结束并获取它的执行结果。
11. 线程的状态
创建 new线程对象
就绪 调用start方法
运行 run
阻塞 sleep join
死亡 运行完毕
11.run和star的区别
系统调用start()方法启动一个线程,该线程处于就绪状态,而非运行状态,也就意味着这个线程可以被JVM来调度执行,在调度过程中,JVM通过线程类的run()方法来完成实际的操作,当run()方法结束后就会终止;
如果直接调用run()方法,这就相当于一个普通函数的调用,是同步的,无法达到多线程的目的,start()方法能后异步的调用run()方法,真正达到多线程的目的
11、简述sleep方法和wait方法的功能和区别
12.什么是线程安全和线程不安全?
通俗的说:加锁的就是是线程安全的,不加锁的就是是线程不安全的
线程安全: 就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
13、什么是自旋锁?
自旋锁是SMP架构中的一种low-level的同步机制。
当线程A想要获取一把自旋锁而该锁又被其它线程锁持有时,线程A会在一个循环中自旋以检测锁是不是已经可用了。
自旋锁需要注意:
- 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间。
- 持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁。
15、阻塞队列
JDK7提供了7个阻塞队列。(也属于并发容器)
- ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
- 阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。
16.threadlocal
**ThreadLoca**l叫做**_线程变量_**,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
**ThreadLocal 与普通变量的区别在于**,每个使用该变量的线程都会初始化一个完全独立的实例副本。 ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
17、ThreadLocal与Synchronized
1、Synchronized用于线程间的数据共享
而ThreadLocal则用于线程间的数据隔离。
2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问
3.ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
一句话理解ThreadLocal,向ThreadLocal里面存东西就是向它里面的Map存东西的,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了。
三、ThreadLocal的简单使用
public class ThreadLocaDemo {
private static ThreadLocal<String> localVar = new ThreadLocal<String>();
static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " :" + localVar.get());
//清除本地内存中的本地变量
localVar.remove();
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
ThreadLocaDemo.localVar.set("local_A");
print("A");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
},"A").start();
Thread.sleep(1000);
new Thread(new Runnable() {
public void run() {
ThreadLocaDemo.localVar.set("local_B");
print("B");
System.out.println("after remove : " + localVar.get());
}
},"B").start();
}
}
A :local_A
after remove : null
B :local_B
after remove : null
18、ThreadLocal 常见使用场景
- 1、每个线程需要有自己单独的实例
- 2、实例需要在多个方法中共享,但不希望被多线程共享
场景
1)存储用户Session
一个简单的用ThreadLocal来存储Session的例子:
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
场景二、数据库连接,处理数据库事务
场景三、数据跨层传递(controller,service, dao)
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。
场景四、Spring使用ThreadLocal解决线程安全问题
19、volatile
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
[
](https://blog.csdn.net/u012723673/article/details/80682208)