线程池
进程和线程
进程是一个动态的过程,是一个活动实体。简单来说,一个应用程序的运行就可以被看成一个进程,而线程是运行中的实际的执行者。可以说,一个进程中包含多个可以同时运行的线程。
线程,程序执行流的最小执行单位,是进程中的实际运作单位。
线程的生命周期:
使用 new Thread() 方法创建一个线程,在线程创建完成之后,线程进入就绪状态(Runable),此时创建出来的线程进入抢占CPU资源的状态,当线程抢占到CPU的执行权后,线程就就进入运行状态(Running),当线程的任务执行完成后或者是非常态的调用stop()方法之后,线程就进入死亡状态。
线程还具有一个阻塞状态,
线程进入阻塞状态有三种情况:
1.当线程主动调用sleep()方法时,线程进入阻塞状态
2.当线程主动调用阻塞IO方法时,这个方法有一个返回参数,在参数返回之前,线程会处于阻塞状态。
3.当线程进入等待某个执行通知时,会进入阻塞状态。
为什么线程会进入阻塞状态?
线程在CPU中运行,而CPU的资源非常宝贵,当线程正在进行某种不确定时长的任务时,不能让它一直占用CPU的执行权,而影响其他任务的执行,Java会自动回收CPU的执行权,从而合理的分配应用CPU的资源。
线程是如何跳出阻塞状态的?
当线程结束阻塞状态后,会重新进入就绪状态,重新抢夺CPU的执行权。线程进入阻塞状态都有时间限制的,当sleep()方法的睡眠时长过去后,或者IO方法在返回参数之后,或者获取得到等待的通知时,就自动跳出了阻塞状态,(或当调用 wait() 方法使线程进入阻塞状态,需要手动调用 notify() 方法使其结束阻塞状态)。
单线程与多线程
单线程,就是只有一条线程执行任务,适用于执行程序是有序排列的。
多线程,创建多条线程同时执行任务,这是我们在日常生活中比较常见的。但多线程经常会遇到并行,并发和线程安全问题。
并行和并发,都是同时执行多种任务,
二者是有区别的:
并发,就是同时执行多种事件,实际上多种事件并不是同时进行的,而是交替进行的,由于CPU的运算速度非常快,给我们造成一种错觉,多种事件在同一时间内一起进行。
并行,才是真正意义上的同一时间进行多种事件,这种是在多核CPU的基础上完成的。
为什么会存在多线程安全问题?
如果多个线程共同执行同一个任务,这就意味着他们共享同一种资源,当第一条线程先抢占到CPU资源,他刚刚进行了第一次操作,而此时第二条线程抢占到了CPU的资源,这时共享资源还没来得及更新,第二条线程就在原始数据的基础上执行,就会出现两条线程使用了同一资源的情况,可能会导致程序出现异常,如售票问题。
造成这些问题的主要矛盾在于,线程抢占CPU执行权与资源的共享发生了冲突。解决的方法很简单,就是在第一条线程占据CPU资源时,阻止第二条线程同时抢占CPU的执行权,保证数据可以及时更新。在代码中,我们只需要在方法中使用同步代码块(volatile,synchronized,Lock)即可。
线程池
线程池,一块缓存一定数量线程的区域。在一个应用程序中,我们需要多次使用方法,多次创建线程和销毁线程,而在创建并销毁线程的过程中势必会消耗内存,而在Java中内存资源异常宝贵,而线程池就很好的解决了内存的浪费使用。
优势:
1.降低资源消耗:通过重复利用已经创建的线程降低线程创建和销毁造成的资源消耗。
2.提高响应效率:当任务到达时,任务不需要等待线程的创建就能立即执行。
3.提高线程的可管理性:线程本就是稀缺资源,如果无限制的创建,不仅会消耗资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。
线程池的使用
线程池真正的实现类是ThreadPoolExecutor,其构造方法如下:
重要参数:
corePoolSize(必需):核心线程数,默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout设置为 true时,核心线程也会超时被回收。
maximumPoolSize(必需):线程池所能容纳的最大线程数,当活跃的线程数达到最大值后,后续的新任务就会被阻塞。
keepActiveTime(必需):线程闲置时长,如果线程闲置时间超过该时长,非核心线程就会被回收。
unit(必需):指定keepActiveTime参数的时间单位,常用:TimeUnit.MILLISECONDS,TimeUnit.SECONDS,
TimeUnit.MINUTES。
workQueue(必需):任务列队,通过线程池的 execute() 方法提交的Runable 对象将保存在该参数中,采用 BlockingQueue(阻塞列队)接口实现。
threadFactory(可选):线程工厂,用于指定为线程池创建新线程的方式。
handler(可选):拒绝策略,当达到最大线程数时需要执行的饱和策略。
线程池工作原理
线程池参数
拒绝策略(handler)
当线程池的线程总数达到最大线程数时,如果还有任务需要执行,就需要执行拒绝策略,拒绝策略需要实现 RejuctedExecutionHandler 接口,并实现 rejectedExecution(Runable r,ThreadPoolExecutor executor)方法。不过 Executor框架已经为我们实现了四种拒绝策略:
AbortPolicy(默认);
丢弃任务并抛出异常RejuctedExecutionException。
CallerRunsPolicy:
由调用线程处理该任务。
DiscardPolicy:
弃置策略,丢弃任务但是不会抛出异常。可以配合这种模式进行自定义的处理方式。
DiscardOldestPolicy:
丢弃队列中最早的未处理的任务,然后重新尝试执行任务。
线程工厂(threadFactory)
线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并要实现 newThread(Runnable r) 方法,参数可以不指定,Executors框架已经实现为我们实现了一个默认的工厂。
DefaultThreadFactory
public Thread newThread(Runnable r){
}
任务队列
任务队列是基于阻塞队列实现的,采用生产者消费者模式,在Java中需要实现 BlockingQueue 接口。
7种阻塞队列:
1)ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针形成一个环形列队)
2)**LinkedBlockingQueue:**一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
3)PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
4)DelayQueue:类似于 PriorityBlockingQueue ,是二叉树实现的无界优先级阻塞队列,要求元素都实现 Delay 接口,通过执行时延从队列中提取任务,时间不到任务取不出来。
5)SynchronizedQueue:一个不储存元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者调用 put() 方法时也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
6)LinkedBlockingDeque:使用双向队列实现的有界双端阻塞队列。双端以为着可以想普通队列一样FIFO,也可以想栈一样FILO。
7)LinkedTransferQueue:他是 ConcurrentBlockingQueue 和LinkedBlockingQueue 和 SynchronizedBlockingQueue 的结合体,但是把他用在 ThreadPoolExecutor 中,和LinkedBlockingQueue 一致,但是是无界的阻塞队列。
有界队列和无界队列的区别:
如果使用有界队列,当队列达到饱和时并超过最大线程数时执行拒绝策略;使用无界队列永远都可以添加任务,所以设置maximumPoolSize 是无意义的。