关于线程和线程池的学习,我们可以从以下几个方面入手:
第一,什么是线程,线程和进程的区别是什么
第二,线程中的生命周期
第三,单线程和多线程
第四,线程安全
第五,为什么使用线程池
第六,线程池原理
第七,线程池的使用
第一,什么是线程,线程和进程的区别是什么?
线程,程序执行流的最小执行单位,是行程中的实际运作单位,经常容易和进程这个概念混淆。那么,线程和进程究竟有什么区别呢?首先,进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者。可以说,进程中包含了多个可以同时运行的线程。
第二,线程中的生命周期
线程的生命周期,线程的生命周期可以利用以下的图解来更好的理解:
第一步,是用new Thread()的方法新建一个线程,在线程创建完成之后,线程就进入了就绪(Runnable)状态,此时创建出来的线程进入抢占CPU资源的状态,当线程抢到了CPU的执行权之后,线程就进入了运行状态(Running),当该线程的任务执行完成之后或者是非常态的调用的stop()方法之后,线程就进入了死亡状态。而我们在图解中可以看出,线程还具有一个阻塞的过程,当面对以下几种情况的时候,容易造成线程阻塞,第一种,当线程主动调用了sleep()方法时,线程会进入则阻塞状态,除此之外,当线程中主动调用了阻塞时的IO方法时,这个方法有一个返回参数,当参数返回之前,线程也会进入阻塞状态,还有一种情况,当线程进入正在等待某个通知时,会进入阻塞状态。那么,为什么会有阻塞状态出现呢?我们都知道,CPU的资源是十分宝贵的,所以,当线程正在进行某种不确定时长的任务时,Java就会收回CPU的执行权,从而合理应用CPU的资源。(说白了就是避免线程占着茅坑不拉屎,当线程需要等待时将宝贵的CPU资源收回,重新分配给其他线程)我们根据图可以看出,线程在阻塞过程结束之后,会重新进入就绪状态,重新抢夺CPU资源。这时候,我们可能会产生一个疑问,如何跳出阻塞过程呢? 由以上几种可能造成线程阻塞的情况来看,都是存在一个时间限制的,当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态,第二种则是在返回了一个参数之后,在获取到了等待的通知时,就自动跳出了线程的阻塞过程。
第三,单线程和多线程
单线程,顾名思义即是只有一条线程在执行任务。
多线程,创建多条线程同时执行任务,这种方式在我们的日常生活中比较常见。但是,在多线程的使用过程中,还有许多需要我们了解的概念。比如,在理解上并行和并发的区别,以及在实际应用的过程中多线程的安全问题,对此,我们需要进行详细的了解。
并行和并发:都是可以同时执行多种任务。
并发,是从宏观方面来说,是指由于CPU的运算速度非常的快,CPU在极短时间内可以交替运行多个任务,会造成我们的一种错觉,就是在同一时间内进行了多种事情
而并行,则是真正意义上的同时进行多种事情。是在同一时刻,某个时间点上有多个任务在执行。这种只可以在多核CPU的基础下完成。
第四,线程安全
为什么会造成多线程的安全问题呢?我们可以想象一下,如果多个线程同时执行一个任务,这也就意味着他们共享同一种资源,由于线程CPU的资源不一定可以被谁抢占到,这是,第一条线程先抢占到CPU资源,他刚刚进行了第一次操作,而此时第二条线程抢占到了CPU的资源,共享资源还来不及发生变化,就同时有两条数据使用了同一条资源,具体请参考多线程买票问题。造成问题的原因我们可以看出,这个问题主要的矛盾在于,CPU的使用权抢占和资源的共享发生了冲突,解决时,我们只需要让一条线程抢占了CPU的资源时,阻止第二条线程同时抢占CPU的执行权,在代码中,我们只需要在方法中使用同步代码块即可。在这里,同步代码块不多进行赘述,可以自行了解。
第五,为什么使用线程池
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。
如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些 "池化资源" 技术产生的原因。比如数据库连接池,它在很大程度上减少了连接建立和释放带来的性能开销!
具体的来讲,我们来考虑这样一种场景,对于一个 web 项目,每次过来一个请求,都要在服务端创建一个新线程来处理请求,请求处理完成后销毁线程,我们将其简单的量化一下:
创建线程耗时:T1
处理请求耗时:T2
销毁线程耗时:T3
那么对于处理这样的一个请求,总耗时便是 T = T1 + T2 + T3,那我们来想一想如果这个请求非常简单呢?(事实上,现在很多 web 接受的都是这样的短小且大量的请求!),T2 耗时很短,则 T1 + T3 > T2,这样使用多线程技术不但没有提高 CPU 的吞吐量,反而降低了。
可以看出 T1,T3 是多线程本身的带来的开销,我们渴望减少 T1,T3 所用的时间,从而减少总耗时 T 的时间。但一些线程的使用者并没有注意到这一点,所以在程序中频繁的创建或销毁线程,这导致 T1 和 T3 在 T 中占有相当比例。显然这是突出了线程的弱点(T1,T3),而不是优点(并发性)。
线程池技术的提出,正是为了解决上述的问题!它与数据库连接池是同样的道理,使用线程池技术有如下几个优点:
降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
提高响应速度: 当任务到达时,任务可以不需要等到线程创建就能立即执行
提高线程的可管理性: 线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
另外还可以运用线程池能有效的控制线程最大并发数,避免同时执行的线程过多,导致系统资源不足而产生阻塞的情况。
第六,线程池原理
事实上,线程池运用的思想就是“以空间换时间”,牺牲一定的内存,来换取任务效率,就现在服务器发展的速度来看,牺牲这点空间所换来的效率,性价比是非常之大的,线程池合理的利用wait和notify两个方法处理线程状态,从而达到有效减少切换上下文的频率。在讲原理之前我们需要了解几个关键名词:
1.poolSize:线程池中当前存活线程的数量,当该值为0的时候,意味着没有任何线程,线程池会终止;同一时刻,poolSize不会超过maximumPoolSize。
2.allowCoreThreadTimeOut:该属性用来控制是否允许核心线程超时退出。
如果线程池的大小已经达到了corePoolSize,不管有没有任务需要执行,线程池都会保证这些核心线程处于存活状态。但是如果将allowCoreThreadTimeOut属性设置为true,则核心线程在空闲时间达到keepAliveTime时也会退出,该属性只是用来控制核心线程的状态的。
3.keepAliveTime:如果一个线程处在空闲状态的时间超过了该属性值,就会因为超时而退出。
举个例子,如果线程池的核心大小corePoolSize=5,而当前大小poolSize =8,那么超出核心大小的线程(3个),会按照keepAliveTime的值判断是否会超时退出。如果线程池的核心大小corePoolSize=5,而当前大小poolSize =5,那么线程池中所有线程都是核心线程,这个时候线程是否会退出,取决于allowCoreThreadTimeOut。
4.corePoolSize:线程池基础大小(或者说线程池核心线程数量)
5.maximumPoolSize:线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。这里值得一提的是largestPoolSize,该变量记录了线程池在整个生命周期中曾经出现的最大线程个数。为什么说是曾经呢?因为线程池创建之后,可以调用setMaximumPoolSize()改变运行的最大线程的数目。
6.workQueue:(阻塞)缓存队列,用来存放等待被执行的任务。有ArrayBlockingQueue和PriorityBlockingQueue,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
在当前时刻工作线程数达到corePoolSize时,新进入的任务才会被塞入workQueue进行排队等待。当workQueue达到上限,且线程池工作线程达到maximumPoolSize时,新进入线程的任务会被线程池拒绝处理,具体拒绝策略需要参照handler的拒绝策略
7.unit:存活时间的单位(和keepAliveTime配合使用)
8.handler:饱和策略(RejectedExecutionHandler),线程数量大于最大线程数就会采用拒绝处理策略,四种策略为
线程池任务执行描述
1.线程池启动初期:线程池在启动初期,线程并不会立即启动(poolSize=0),而是要等到有任务提交时才会启动,除非调用了prestartCoreThread(预启动一个空闲任务线程待命)或者prestartAllCoreThreads(预启动全部空闲任务线程待命,此时poolSize=corePoolSize)事先启动核心线程。
2.任务数量小于corePoolSize的情况:我们假设未进行线程预启动,那么每提交一个任务给线程池,线程池都会为其创建一个任务线程,直至所创建的线程数量达到corePoolSize(这些线程都是非空闲状态的线程,如果有空闲状态的线程,新提交的任务会直接分配给空闲任务线程去处理)。
3.任务数量大于corePoolSize且小于maximumPoolSize的情况:当线程池中工作线程数量达到corePoolSize时,线程池会把此时进入的任务提交给workQueue进行“排队等待”处理,如果此时恰好线程池内某个(或者某些)核心线程处于空闲状态(已处理完之前的任务),那么线程池会把任务从阻塞队列中取出,交给这个(些)空闲的线程去处理。如果此时workQueue已经满了,且工作核心线程数已经到poolSize=corePoolSize的状态,那么线程池就会继续创建新的线程来处理任务,但是工作线程总数不会超过maximumPoolSize。
4.任务数量超出maximumPoolSize的情况:当线程池接受的任务数量超出maximumPoolSize时,超出的任务会被线程池拒绝处理,我们称线程池已饱和,具体饱和策略需要参照handler。
线程执行过程
(1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
(2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
(3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务
接下来,拿 J.U.C 包中的 ThreadPoolExecutor 执行 execute() 方法来举例,看一下 Java 中的线程池是如何工作的(ThreadPoolExecutor 是 Executor 框架中最核心的类,是线程池的实现类)
(1)如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)
(2)如果运行的线程等于或多于 corePoolSize,则将任务加入 BlockingQueue
(3)如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)
(4)如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution() 方法
ThreadPoolExecutor 采取上述步骤的总体设计思路,是为了在执行 execute() 方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在 ThreadPoolExecutor 完成预热之后(当前运行的线程数大于等于 corePoolSize),几乎所有的 execute() 方法调用都是执行步骤(2) ,而步骤(2)不需要获取全局锁。
第七,线程池的使用
在java.util.concurrent包中我们能找到线程池的定义,其中ThreadPoolExecutor是我们线程池核心类,首先看看线程池类的主要参数有哪些。
/** * Creates a new {@code ThreadPoolExecutor} with the given initial * parameters and default thread factory. */public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue 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.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler;}
corePoolSize:线程池的核心大小,也可以理解为最小的线程池大小。maximumPoolSize:最大线程池大小。keepAliveTime:空余线程存活时间,指的是超过corePoolSize的空余线程达到多长时间才进行销毁。unit:销毁时间单位。workQueue:存储等待执行线程的工作队列。threadFactory:创建线程的工厂,一般用默认即可。handler:拒绝策略,当工作队列、线程池全已满时如何拒绝新任务,默认抛出异常。
其实在 J.U.C 包下已经提供了 Executors 类,它已经封装实现了四种创建线程池的方式,我们可以根据需要,得到我们想要的线程池类型。
1.newCachedThreadPool:
带缓冲线程池,从构造看核心线程数为0,最大线程数为Integer最大值大小,超过0个的空闲线程在60秒后销毁,SynchronousQueue这是一个直接提交的队列,意味着每个新任务都会有线程来执行,如果线程池有可用线程则执行任务,没有的话就创建一个来执行,线程池中的线程数不确定,一般建议执行速度较快较小的线程,不然这个最大线程池边界过大容易造成内存溢出。
/** * Creates a thread pool that creates new threads as needed, but * will reuse previously constructed threads when they are * available. These pools will typically improve the performance * of programs that execute many short-lived asynchronous tasks. */public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue());}
适用场景:执行很多短期异步的小程序或者负载较轻的服务器。
2.newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,核心线程数和最大线程数固定相等。当执行任务时,如果线程都很忙,就会丢到工作队列等有空闲线程时再执行,队列满就执行默认的拒绝策略。
/** * Creates a thread pool that reuses a fixed number of threads * operating off a shared unbounded queue, using the provided * ThreadFactory to create new threads when needed. */public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), threadFactory);}
适用场景:执行长期的任务,性能好很多
3.NewScheduledThreadPool:创建一个调度线程池,支持定时及周期性任务执行,即按一定的周期执行任务,对ThreadPoolExecutor进行了包装而已。
/** * Creates a thread pool that can schedule commands to run after a * given delay, or to execute periodically. */public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize);}
适用场景:在给定延迟后运行命令或者定期地执行任务的场景。
4.newSingleThreadExecutor:单线程线程池,核心线程数和最大线程数均为1,空闲线程存活0毫秒同样无意思,意味着每次只执行一个线程,多余的先存储到工作队列,一个一个执行,保证了线程的顺序执行。
/** * Creates an Executor that uses a single worker thread operating * off an unbounded queue. (Note however that if this single * thread terminates due to a failure during execution prior to * shutdown, a new one will take its place if needed to execute * subsequent tasks.) Tasks are guaranteed to execute * sequentially, and no more than one task will be active at any * given time. Unlike the otherwise equivalent */public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));}
适用场景:按序且逐个执行任务的场景
可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow 首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown 只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个,isShutdown 方法就会返回 true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 isTerminaed方法会返回 true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown 方法来关闭线程池,如果任务不一定要执行完,则可以调用 shutdownNow 方法。
Demo:
newCachedThreadPool:
// 可缓冲的线程池(可以有多个线程) ExecutorService pool = Executors.newCachedThreadPool(); for(int i =0; i< 10; i++){ pool.execute(new Runnable() { @Override public void run() { // 打印当前线程的名字,线程开始 System.out.println(Thread.currentThread().getName()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 线程结束 System.out.println(Thread.currentThread().getName() +" is over"); } }); } System.out.println("All thread is over"); pool.shutdown();
// 可缓冲的线程池(可以有多个线程) ExecutorService pool = Executors.newCachedThreadPool(); for(int i =0; i< 10; i++){ pool.execute(new Runnable() { @Override public void run() { // 打印当前线程的名字,线程开始 System.out.println(Thread.currentThread().getName()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 线程结束 System.out.println(Thread.currentThread().getName() +" is over"); } }); } System.out.println("All thread is over"); pool.shutdown();
newFixedThreadPool:
// 创建固定大小的线程池(7个) ExecutorService pool = Executors.newFixedThreadPool(7); for(int i =0; i< 10; i++){ pool.execute(new Runnable() { @Override public void run() { // 打印当前线程的名字,线程开始 System.out.println(Thread.currentThread().getName()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 线程结束 System.out.println(Thread.currentThread().getName() +" is over"); } }); } System.out.println("All thread is over"); pool.shutdown();
newScheduledThreadPool:
// 创建一个调度线程池(5个) ScheduledExecutorService pool = Executors.newScheduledThreadPool(5); for(int i =0; i< 10; i++){ pool.execute(new Runnable() { @Override public void run() { // 打印当前线程的名字,线程开始 System.out.println(Thread.currentThread().getName()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 线程结束 System.out.println(Thread.currentThread().getName() +" is over"); } }); } System.out.println("All thread is over"); pool.shutdown();
newSingleThreadExecutor:
// 创建一个单线程的线程池 ExecutorService pool = Executors.newSingleThreadExecutor(); for(int i =0; i< 10; i++){ pool.execute(new Runnable() { @Override public void run() { // 打印当前线程的名字,线程开始 System.out.println(Thread.currentThread().getName()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 线程结束 System.out.println(Thread.currentThread().getName() +" is over"); } }); } System.out.println("All thread is over"); pool.shutdown(); }
shutdownNow 方法来关闭线程池:
// 创建固定大小的线程池(7个) ExecutorService pool = Executors.newFixedThreadPool(7); for(int i =0; i< 10; i++){ pool.execute(new Runnable() { @Override public void run() { // 打印当前线程的名字,线程开始 System.out.println(Thread.currentThread().getName()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 线程结束 System.out.println(Thread.currentThread().getName() +" is over"); } }); } System.out.println("All thread is over"); pool.shutdownNow();