1.线程池原理
线程池的工作主要是控制运行的线程数量,处理过程将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过最大数量,超出的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。它的主要特点是:线程复用,控制最大并发,管理线程。
-
线程复用
每个Thread类都有一个start()方法,当调用start()方法启动线程的时候,JVM会调用该类的run()方法,该类的run()方法就是调用Runnable对象的run()方法。我们可以继承Thread类,在其start()方法中添加不断循环调用传过来的的Runnable对象,这就是线程池实现原理。循环方法中不断获取Runnable是用Queue实现的,在获取下一个Runnable之前可以是阻塞的。
-
线程池组成
一般线程池主要分为以下4个部分组成:
- 线程池管理器:用来创建并管理线程池。
- 工作线程:线程池中的线程。
- 任务接口:每个任务必须实现的接口,用于工作线程调度其运行。
- 任务队列:用于存放待处理的线程,提供一种缓存机制。
-
线程池类型
-
Executors.newCachedThreadPool(); 没有核心线程,最大线程数为Integer..MAX_VALUE,线程可以无限创建,当线程池中的线程都处于活动状态,线程池会创建新的线程来处理新任务,否则空闲线程处理,线程池里的空闲线程都是有超时时间的默认60s。
-
newFixedThreadPool(int nThreads);线程池里面全是核心线程,没有超时机制,任务大小没有限制,数量固定,空闲线程不会被回收,除非线程池关闭,线程生命周期跟线程池一致。适用于任务量固定且耗时较长的任务。
-
newScheduledThreadPool(int corePoolSize);核心线程数固定,非核心线程没有限制,没有过期时间。适用于定时任务或执行具体固定周期的重复任务。
-
newSingleThreadExecutor();只且仅有一个核心线程,确保所有任务都在同一线程中顺序执行,不需要处理线程同步问题。适用于多任务顺序执行的场景。
-
ThreadPoolExecutor类,自定义线程类,构造方法有5个参数。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
- corePoolSize:核心运行的线程数,超过这个数就需要把新的异步任务放入等待队列中,小于这个数添加的异步任务直接新建线程开始执行。
- maximumPoolSize:最大线程数,当线程池中的活动线程等于该数,那么新加的异步任务由一个丢弃处理机制来处理。
- keepAliveTime:非核心线程空闲的时间,默认为0,超过该时间非核心线程会被销毁。
- unit:keepAliveTime的单位,TimeUnit 是一个枚举类型。
- workQueue:任务等待队列,当达到corePoolSize的时候向该等待队列放入线程信息。
- threadFactory:线程工厂,用于创建线程,一般默认即可。
- handler:拒绝策略。当线程池满了,阻塞队列也满了时的处理方式。
-
-
拒绝策略
线程池中的线程已经用完,无法继续为新任务服务,同时等待队列已经排满,再也塞不下新任务,这是需要拒绝策略机制处理该问题。
-
AbortPolicy :抛出RejectedExecutionException异常,丢弃任务,阻止系统正常工作(默认拒绝策略)
-
DiscardPolicy:偷偷的丢弃任务,不抛异常,如果允许丢弃任务,这是最好的方式。
-
DiscardOldestPolicy :丢弃最老的任务(队首马上要执行的任务),重新尝试执行任务(重复过程)。
-
CallerRunsPolicy:只要线程池未关闭,直接调用线程执行该任务,可能引起性能急剧下降。
以上内置的拒绝策略都实现RejectedExecutionHandler接口,可以自定义扩展该接口。
-
-
Java线程池工作过程
-
线程池在刚创建的时候里面没有线程,任务队列是作为参数传进来的,就算队列里面有任务,线程池也不会马上执行他们。
-
当调用execute()方法添加一个任务时,线程池会做以下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程执行该任务。
- 如果正在运行的线程数量大于或等于corePoolSize,那么该任务会被放入阻塞队列中。
- 如果队列满了,且正在运行的线程数小于maximumPoolSize,那么还是会创建非核心线程立即运行该任务。
- 如果队列满了,运行线程数大于或等于maximumPoolSize,那么线程池会抛出RejectExecutionException异常(拒绝策略)。
-
当一个线程完成任务时,它会从队列中取出下一个任务执行。
-
当一个线程无事可做,超过keepAliveTime时间,线程池会判断,如果当前运行线程大于corePoolSize,那么这个线程会被停掉。线程池中所有任务完成后,会缩小到corePoolSize的大小。
-
-
ThreadPoolExecutor#submit()和execute()方法的区别:
- execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。
- execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
- execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。
2.Java阻塞队列
阻塞队列,重点是阻塞,线程阻塞有两种情况:
-
当队列中没有数据的时候,消费端的所有线程会被阻塞(挂起),直到有数据放入队列中。
-
当队列填满数据时,生产端的所有线程会被阻塞(挂起),直到队列有空的位置,线程被自动唤醒。
Java中的阻塞队列:
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列,先进先出(初始化时必须设定容量),默认情况下不保证访问者的公平性,公平性指的是按阻塞的先后顺序访问队列。通常公平性会降低系统的吞吐量,可以通过ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(1000, true);保证公平性,底层通过ReentrantLock(true)构造方法实现。
- LinkedBlockingQueue:由链表结构组成的有界阻塞队列,先进先出,默认长度为Integer最大值,对于生产端和消费端分别采用独立的锁来控制数据同步,在高并发情况下,生产者和消费者可以并行操作队列,提交了队列的并发性能。
- PriorityBlockingQueue:支持优先级的无界阻塞队列,按照任务优先级出队。按照对象自然排序或者构造方法的Comparator 决定顺序,可以控制任务的执行顺序。注意:相同优先级无法保证顺序。
- SynchronousQueue:不存储元素的阻塞队列,操作必须是放和取交替进行。每个put操作都会等待一个take操作,否则不能添加新元素。适用于传递场景,一个线程使用的数据传递给另一个线程。
- LinkedTransferQueue:链表结构组成的无界阻塞队列。多了transfer和tryTransfer方法。
- LinkedBlockingDeque:链表结构组成的双向阻塞队列。可以从队列的两端插入和移除数据,在多线程同时入队的情况下,减少了一半的竞争。在初始化时设置容量防止过度膨胀。
- DelayQueue:支持延时获取的无界阻塞队列。队列使用PriorityQueue来实现,队列的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素,只有延时期满时才能获取。可以做缓存失效和定时任务。
3.Java中的线程调度
-
抢占式调度
抢占式调度指的是每个线程的执行时间,线程的切换都是有系统控制。系统控制指的是在系统某种运行机制下,可能每条线程都分同样的时间轮片,也可能某些线程的执行时间片较长,甚至某些线程得不到时间片。在这种机制下,一个线程阻塞不会导致整个进程阻塞。
-
协同式调度
协同式调度指的是当一个线程执行完之后会主动通知系统切换到另一个线程上执行,类似于接力赛。线程的执行时间由线程本身控制,线程切换可以预知,不存在同步问题。但是当一个线程阻塞会导致整个进程崩溃。
4.进程调度算法
-
优先调度算法
-
先来先服务调度算法(FCFS)
当作业调度采用该算法时,每次调度时,都是从后备作业队列中选择一个或多个最先进入该队列的作业,将他们放入内存,为他们分配资源,创建进程放入就绪队列。采用FCFS算法,每次进程调度都是从就绪队列中选择最先进入该队列的进程,为其分配处理机制,该进程一直运行到完成或者发生某事件而阻塞后才放弃处理机。特点:算法比较简单,可以实现基本上的公平。非抢占式的。
-
短作业(进程)优先调度算法
短作业优先(SJF)是从后备队列中选择一个或若干个估计运行时间最短的作业,将他们调入内存中运行。短进程优先(SPF)是从就绪队列中选择一个估计运行时间最短的进程。
-
-
高优先权优先调度算法
为照顾紧急作业,使之进入系统便获得优先处理,引入高优先权优先调度算法(FPF)。作业调度时,从后备队列选择若干个优先权最高的作业装入内存中。进程调度时,从就绪队列中选择优先权最高的进程。
-
基于时间片的轮转调度算法
- 时间片轮转法
- 系统将所有的就绪进程按照先来先服务的原则排成一个队列,每次调度时CPU分配给队首进程,并另其执行一个时间片。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便根据此信号来停止该进程的执行,并将它送往就绪队列末尾,然后再把处理机分配给就绪队列中的新的队首进程,让它执行。
- 多级反馈队列调度算法
- 设置多个就绪队列,各个队列赋予不同的优先级,第一队列优先级最高,第二队列次之,其余队列优先级逐个降低。该算法赋予的时间片也不相同,优先级越高的分配的时间片越少,依次类推。
- 当新进程进入内存后,先存入第一队列末尾,按FCFS原则排队等到调度。当轮到该进程执行时,如果它能在时间片内完成,则准备撤离系统;如果未完成,调度程序则将该进程存入第二就绪队列的队尾,依次类推。
- 仅当第一队列空闲时,调度程序才调度第二队列中的进程执行。
- 时间片轮转法
5.CyclicBarrier 、CountDownLatch 、Semaphore 的用法
-
CountDownLatch:线程计数器
CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似于计数器的功能。比如有一个任务A,它要等待其他4个任务执行完才能执行,可以利用CountDownLatch实现。
final CountDownLatch latch = new CountDownLatch(2); new Thread(){public void run() { System.out.println("子线程"+Thread.currentThread().getName()+"正在执行"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕"); latch.countDown(); };}.start(); new Thread(){ public void run() { System.out.println("子线程"+Thread.currentThread().getName()+"正在执行"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕"); latch.countDown(); };}.start(); System.out.println("等待 2 个子线程执行完毕..."); latch.await(); System.out.println("2 个子线程已经执行完毕"); System.out.println("继续执行主线程");
-
CyclicBarrier:回环栅栏,可以实现让一组线程等到至某个状态后全部同时执行。叫做回环的原因是,所有等待的线程被释放后,CyclicBarrier可以被重用,这个状态叫barrier,当调用await()方法之后,线程就处于barrier状态。
-
Semaphore :信号量,控制同时访问的线程个数。通过acquire()方法获得一个许可,如果没有就等待,release()释放一个许可。
-
CountDownLatch 和 CyclicBarrier都能实现线程间的等待,侧重点不同,CountDownLatch 一般是某个线程等待其他线程执行完再执行;CyclicBarrier一组线程相互等待至某个状态,然后这一组线程同时执行。同时CyclicBarrier是可以复用的,CountDownLatch 不可以。
-
Semaphore 类似于锁,互斥锁就是讲Semaphore (1),一般控制某组资源的访问权限。
6.CAS
CAS(Compare And Swap/Set)比较并替换,它包含三个参数CAS(V,E,N)。V表示要更新的变量(内存值),E表示预期值(旧的),N表示新值,当且仅当V=E时,V=N,返回V;当V跟E不相同时,表示其他线程做了更新,当前线程什么都不做。CAS操作是乐观锁的心态实现的,当多线程CAS操作同一个变量时,只会有一个胜出,其余的失败。失败的线程不会被阻塞,仅会被告知失败,并且允许再次尝试,也允许失败的线程放弃操作。
CAS操作会存在ABA的问题。操作的变量A->B->A然后CAS操作成功,但是并不表示整个过程没有问题。部分乐观锁的实现通过版本号的方式来解决ABA问题,每次执行修改操作时都会带上版本号,一旦版本号不一致表示发生了修改操作。
7.AQS
AQS-AbstractQueuedSynchronizer-抽象队列同步器。定义了一套多线程访问共享资源的同步器框架。它维护了一个volatile int state(共享资源),FIFO(先进先出)线程等待队列(多线程竞争资源被阻塞时存入该队列中)。
- AQS定义了两种资源共享的方式
- Exclusive 独占资源 -ReentrantLock(独占,只有一个线程执行)
- Share 共享资源 -Semaphore/CountDownLatch(多个线程同时执行)
- 同步器的实现(state 资源状态计数)
- 以ReentrantLock为例,state初始化为0,表示未锁定状态, A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程tryAcquire()就会失败,直到A线程unlock(),state=0(即释放锁)为止,其他线程才有机会获得该锁。在释放锁之,A线程可以重复获取该锁(state会累加),这就是可重入的概念,注意:获取多少次就要释放多少次,保证state=0。
- 以CountDownLatch为例,任务分为N个子线程去执行,state初始化为N(与线程数保持一致)。这N个子线程并行执行,每个子线程执行完之后,countDown()一次,state会CAS操作减1。等到所有子线程执行完(state=0),会unpark()主调用线程,然后主线程就会从await()函数返回,继续后续动作。