实现多线程的集中方式
一、继承Thread类
继承Thread类,重写run方法, start启动线程
二、实现Runnable接口
实现Runnable接口,重写run方法,通过Thread的start方法启动线程
三、使用内部类方式
并不是一种新的实现方式,只是另外一种写法,通过匿名内部类方式来实现,方式依赖有继承Thread和实现Runnable两种写法
四、定时器
就是基于线程的一个工具类
五、带返回值的线程实现方式
1.创建一个类实现Callable接口,实现call方法
2.创建一个FutureTask指定Callable对象,作为线程任务
3.创建线程指定线程任务
4.启动线程
六、基于线程池创建线程
谈论线程池之前先说下什么是This逃逸?
对象在还没没有构造完成时,This引用已经发布出去。这种一般会有两种情况:
一、在构造器中启动线程:启动的线程的任务是内部类,在内部中xxx.this访问了外部类的实例,就会发生访问到还未初始化完成的变量
解决方法:不要在构造器中启动线程,尝试在外部启动
二、在构造器中注册事件,这是因为在构造器中监听事件是有回调函数。(可能访问了操作了的实例变量)
解决方法:将事件监听事件放置于构造器外。
总结:this逃逸一般多发生在多线程中,引起this逃逸的问题是在多线程滥用引用。
那么线程中的Executor框架就是有助于避免this逃逸问题。
Execuor框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等。
线程池提供了一种限制和管理资源。
使用线程池的好处:
一、降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
二、提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立刻执行
三、提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以进行统一得分配,调优和监控。
四种常用线程池:
FixedThreadPool,CachedThreadPool,SingleThreadPool,ScheduledThreadPool
FixedThreadPool:
keepAliveTime:0,不存在空闲线程
BlockingQueue:LinkedBlockingQueue(Integer.MAXValue)无界队列,如果瞬时任务数目非常大,会有OOM风险
有限线程数的线程池,它的核心线程数和最大线程数是一致的,线程数目是固定的。当运行线程数达到最大线程数时,如果此时再有任务提交,任务将放入无界阻塞队列中。等到有线程空闲时再从队列中取出任务继续执行。
1.当线程中数目达到corePoolSize后,新任务将在无界队列中等待,因此线程池中线程数不会超过corePoolSize
2.由于使用了无界队列,所以maximumpoolsize将是一个无效参数,因为不存在队列满的情况
3.由于maximumpoolsize=corePoolsize和无界队列存在keepAliveTime也是一个无效参数
4.在任务比较多的情况会出现OOM内存溢出
CachedThreadPool:
corePoolSize:0
maximumPoolSize:Integer.MaxValue.如果达到上限,没有任何线程能够处理,肯定会跑出OOM异常
BlockingQueue:SynchronousQueue。一种不存在任何元素的阻塞队列
无限线程数的线程池,线程数会根据需要自动增加或减少,如果有任务需要执行,且目前没有空闲的线程可用,就会创建一个新的线程执行任务,当线程空闲时间超过60s时,就会被回收。
如果主线程提交任务速度高于maxnimumpool时,会不断创建新的线程,极端情况下会导致耗尽cpu和内存资源
大量创建线程会导致OOM
SingleThreadPool:
corePoolSize:1,maximumPoolSize:1
keepAliveTime:0
BlockingQueue是LinkedBlockingQueue(Integer.MaxValue)。这是无界队列,如果瞬间任务数非常大,会有OOM的风险。
只要一个线程的线程池,特点是保证所有任务都在同一个线程中顺序执行,避免多线程情况下的竞争和死锁问题,适用于按顺序执行多个任务的场景
ScheduledThreadPool:
定时任务线程池,可以在指定的时间执行任务,也可以定期执行任务
corePoolSize是自定义
maximumPoolSize:Integer.maxvalue。也会存在OOM风险
BlockQueue:DelayedWorkQueue
工作队列:
ArrayBlockingQueue:
ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量,尾部插入,头部获取
初始化时需指定队列的容量 capacity
类比到上面的场景,就是椅子的数量为初始容量capacity
LinkedBlockingQueue:
链表阻塞队列,这是一个无界队列,遵循FIFO,尾部插入,头部获取
初始化时可不指定容量,此时默认的容量为Integer.MAX_VALUE,基本上相当于无界了,此时队列可一直插入(如果处理任务的速度小于插入的速度,时间长了就有可能导致OOM)
类比到上面的场景,就是椅子的数量为Integer.MAX_VALUE
SynchronousQueue:
同步队列,阻塞队列的特殊版,即没有容量的阻塞队列,随进随出,不做停留
类比到上面的场景,就是椅子的数量为0,来一个人就去柜台办理,如果柜台满了,就拒绝
PriorityBlockingQueue
优先级阻塞队列,这是一个无界队列,不遵循FIFO,而是根据任务自身的优先级顺序来执行
初始化可不指定容量,默认11(既然有容量,怎么还是无界的呢?因为它添加元素时会进行扩容)
类比到上面的场景,就是新来的可以插队办理业务,好比各种会员
DelayQueue
DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
线程工厂
每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。在ThreadFactory中只定义了一个方法newThread,每当线程池需要创建一个新线程时都会调用这个方法。Executors提供的线程工厂有两种,一般使用默认的,当然如果有特殊需求,也可以自己定制。
- DefaultThreadFactory:默认线程工厂,创建一个新的、非守护的线程,并且不包含特殊的配置信息。
- PrivilegedThreadFactory:通过这种方式创建出来的线程,将与创建privilegedThreadFactory的线程拥有相同的访问权限、 AccessControlContext、ContextClassLoader。如果不使用privilegedThreadFactory, 线程池创建的线程将从在需要新线程时调用execute或submit的客户程序中继承访问权限。
- 自定义线程工厂:可以自己实现ThreadFactory接口来定制自己的线程工厂方法。
Execuor框架结构
一、任务(Runnable、Callable)
执行任务需要实现Runnable接口或者Callable接口。Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecuor执行。
二、任务的执行(Executor)
Execuor接口,ExecutorService接口是集成了Execuor接口.ThreadPoolExecuor和ScheduledThreadExecutor这两个关键类是实现了ExecuorService接口.
三、异步计算的结果(Future)
Future接口以及Future接口的实现类FutureTask类都可以代表异步计算的结果
1.主线程首先要创建实现Runnable/callable接口的任务对象
2.将任务对象交给ExecuteService执行,ExecuteService有两个方法可以接受任务对象,分别时submite()和execute()。execute()方法执行任务对象没有返回值,submit()方法执行任务对象会返回FutureTask对象
3.如果执行了ExecutorService.submit(),ExecutorService对象将返回一个实现Future接口的对象。由于FutureTask也实现Runnable接口,因此也可以直接创建FututeTask类的任务,然后直接交给ExecutorService执行。
4.最后主线程可以执行FutureTask.get()方法来等待任务执行完成,主线程也可以执行FutureTask.cancel()方法来取消此任务的执行
ThreadPoolExecutor类分析:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
三个最重要线程池参数corPoolSize,MaximunSize,workqueue
coePoolSize:最小可以同时运行的线程数量
maximumPoolSize:当队列中存放的任务达到队列容量时,当前可以同时运行的线程数量变道最大线程数
workQueue:当新任务来的时候先判断当前运行的线程数是否达到核心线程数,如果达到,新任务就会被存放在队列中
keepAliveTime:当线程池中线程数大于corePoolSize时,如果没有新任务提交,核心线程外的线程不会立即销毁,而是会等待,知道等待时间超过keepAliveTime才会被销毁掉
handler:饱和策略,如果当前同时运行的线程数量达到最大线程数量,并且队列也已经被放满了任务时,就会实现饱和策略,饱和策略有抛出异常来拒绝新任务处理(默认),不处理新任务直接丢弃,丢弃最早未处理的任务请求,CallersRunsPolicy(提供伸缩队列)
阿里巴巴java开发手册并发处理明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程,同时强制线程池不允许使用Execuors去创建,而是通过ThreadPoolExecutor构造函数的方式创建,
Executors创建线程池的弊端如下:
在SingleThreadPool和FixedThreadPool中使用的队列都是无界阻塞队列,允许队列的最大长度为Integer.MAXVALUE,可能对接大量请求,造成OOM
在CachedThreadPool和ScheduledThreadPool允许创建的线程数量为Integer.MAX-value可能也会创建大量线程,从而导致OOM。
所以推荐使用ThreadPoolExecutor的构造方法去自定义创建线程池
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final long KEEP_ALIVE_TIME = 1L;
ThreadPoolExecutor executor = new ThreadPoolExector(
CORE_POOL_SIZE , MAX_POOL_SIZE ,KEEP_ALIVE_TIME ,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(Queue_CAPACITY),
new ThreadPoolExecutor.CallersRunsPolicy()
)
1.线程先会执行5个任务,然后5个任务中有任务被执行完的话,就去拿新任务执行
每次只可能存在5个任务同事执行,剩下的5个任务会被放到等待队列中去,如果5个任务中如果有任务被执行完,线程池就会那新的任务执行。
Executor.shutdown();关闭线程池,线程池的状态变为shutdown。线程池不再接受新任务,但是队列中的任务得执行完毕
executor.shutdownnow():关闭线程池,线程状态变为stop,线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的list
isshutdown():调用shutdown方法后返回true
isTermintaed():调用shutdown()方法后,并且所以提交的任务完成后返回为true。
怎么设置线程数
CPU密集型:n(cpu核心数)+1
密集型任务:2N
需要对内存中大量数据进行排序属于cpu密集型,设计到网络读取,文件读取属于IO密集型