一、相关概念
操作系统中是以多进程的形式执行,以多线程的方式执行,允许将单个任务由多个部分执行,并且在多线程之间能够提供协调机制,允许进程之间、线程之间有共享的部分,又能够保证进程之间、线程之间不会相互影响。
进程
- 进程是系统中正在运行的一个程序,可以看成是程序的一个实例,进程也是系统进行资源分配和调度基本单位。。
- 进程有独立性、动态性、并发性这3个特征。
- 进程间交互,需要使用进程间通信,如管道、系统IPC(包括消息队列,信号量,共享存储)、套接字等。
- 操作系统支持多进程并发执行,是CPU(单核)快速在进程之间轮换执行造成的错觉。
线程
- 线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是一个单一顺序的控制流。
- 多线程拓展了多进程的概念,使得同一个进程可以同时并发多个线程,每条线程并行执行不同的任务。
- 多线程的优势如下:与父进程共享同一地址空间(该进程的所有资源),线程之间共享内存非常容易。创建或切换线程的代价比进程小,更轻量级。
- 每个线程也有自己的运行栈、程序计数器(PC)和线程的本地存储。
- 线程间通信有两种方式,共享内存和消息传递。共享内存需关注:可见性和有序性(JMM保证),原子性(锁保证)。
进程调度算法
- 优先调度算法,(1)先来先服务调度算法(2)短作业优先调度算法
- 高优先权优先调度算法,(1)优先权调度算法的类型(2)高响应比优先调度算法
- 基于时间片的轮转调度算法,(1)时间片轮转法(2)多级反馈队列调度算法
线程调度
- 抢占式调度,是每条线程执行的时间、线程的切换都由系统控制,一个线程的堵塞不会导致整个进程堵塞。java使用的线程调使用抢占式调度,Java中线程会按优先级分配CPU时间片运行。
- 协同式调度,指某一线程执行完后主动通知系统切换到另一线程上执行,如果一个线程运行到一半就堵塞,那么可能导致整个系统崩溃。
上下文切换
- 利用时间片轮转的方式, CPU给每个任务都服务一定的时间,使多个任务在同一颗 CPU 上执行变成了可能。
- 寄存器,是CPU内部的数量较少但是速度很快的内存。
- 程序计数器,是一个专用的寄存器,用于表明指令序列中CPU正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置。
- 上下文,是指某一时间点CPU寄存器和程序计数器的内容。
- 上下文切换,cpu会把当前任务的状态保存在PCB(process control block,进程控制块,也叫切换帧),在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载, 这段过程就叫做上下文切换。
二、线程
线程实现方式
创建线程的方式有三种。
1、Thread()继承java.lang.Thread类,并且重写run()方法,通过Thread实例的start方法启动线程。
2、实现java.lang.Runable接口,一般通过匿名内部类形式对Thread实例进行初始化。
- 建立Runable对象
- 使用Runable参数构造Thread对象,Thread(Runable target)。
- 调用start()方法启动线程
3、Callable接口是Runable接口的加强,Callable接口里定义的方法有返回值,可以声明抛出异常。
一般推荐使用接口方式创建多线程,虽然编程稍稍复杂,但线程类还可以继承其他类,多个线程可以共享同一个target对象。
1 | import java.util.concurrent.Callable; |
线程生命周期
有5种状态:
- 新建状态(New)–>刚创建Thread对象,由JVM分配内存,初始化成员变量。
- 就绪状态(Ready)–>调用start方法后,不一定会立即得到执行。JVM会为其创建方法调用栈和程序计数器,等待调度运行。
- 运行状态(Running)–>一旦就绪状态的线程得到CPU时间片则开始执行。就绪状态和运行状态都算是可运行状态(Runnable)。
- 死亡状态(Dead)
- 阻塞状态(Blocked),是指线程因某种原因放弃了cpu的使用权,暂时停止运行。阻塞后可以重新解除进入就绪状态。
阻塞的情况分为3种:
- 等待阻塞,运行的线程执行wait()方法后,会释放锁,JVM会把该线程放到等待队列中。
- 同步阻塞,运行的线程在获取对象的同步锁时,被占用,JVM会把该线程放到锁池(lock pool)中。
- 其他阻塞,运行的线程执行sleep()或join()方法,或者有IO请求时,JVM会把该线程置为阻塞状态,会让出CPI时间片,但不会释放锁。当sleep()休眠状态超时,join()等待线程终止或者超时,或者IO处理完毕时,线程重新转入可运行状态。
线程操作
1、线程的加入:join()方法,使当前线程暂停,转为阻塞状态,让出CPU时间片,但不会释放锁。直到调用join()方法的线程执行完毕后,当前线程会由阻塞状态变为就绪状态,继续执行。
1 | public class ThreadJoin { |
2、线程的中断:目前已废除stop()方法,容易导致死锁,因此不推荐。可在run方法中使用布尔标记控制无限循环的停止。如线程是因为sleep或wait方法进入就绪状态,则可使用interrupted()方法,会在程序中抛出InterruptedException异常,并在异常处理时结束while循环。
1 | public class ThreadInterrupt { |
3、线程睡眠:sleep,当前线程睡眠,让出CPU时间片进入阻塞状态,与wait不同的是sleep不会释放锁。可使用TimeUnit枚举类工具来控制时间颗粒度。
4、线程的礼让:yield,一般不使用。让对应线程暂停,不是阻塞,而是从运行状态转回到就绪状态,让CPU重新调度,并不是一定给其他线程先执行,有可能CPU还是调度给自己。
5、线程优先级:通过setPriority()方法设置。最小是1,最大是10,默认为5。并不是一定先执行,由CPU调度,可能优先。
1 | public class ThreadYieldAndPriority { |
6、线程安全:多线程导致线程安全问题,来源于两个线程同时存取单一对象的数据导致。
7、线程同步机制:相当于给共享资源上一道锁,java提供同步机制,有效防止资源冲突。
8、同步代码块:将共享资源的操作放到synchronized关键字定义的区域内,
synchronized(object){ }
9、同步方法:通过synchronized关键字修饰方法。
10、同步锁:由Lock接口充当或者synchronized关键字来实现,是控制多个线程对共享资源进行访问的工具。他们都是可重入锁,可以进行阻塞式的同步。但Lock接口提供了比synchronized更广泛和灵活的锁操作,比较常用的实现是ReentrantLock。
11、线程通信(等待和唤醒):
- 使用synchronized关键字保证同步,可使用隐式的同步监听器,通过wait()、notify()、notifyAll()方法进行线程通信。
- 使用Lock对象保证同步时,使用Condition对象的await()、signal()、signalAll()方法来控制线程通信。
- 使用阻塞队列(BlockingQueue)来控制线程通信,当队列已经满放入元素,或者队列已空取出元素时,线程会被阻塞。
12、可定义线程的线程组,这样可以来操作整个线程组里的所有线程。可使用setUncaughtExceptionHandler为指定的线程实例设置异常处理类。可使用线程池来有效控制系统并发线程的数量,节约启动新线程的成本,提高性能。
三、锁
乐观锁(Optimistic Locking)和悲观锁(Pressimistic Locking)
- Java实现中,Synchronized关键字和Lock的实现类都是悲观锁。
- Java是通过使用无锁编程来实现乐观锁,最常采用的是CAS算法,如原子类AtomicInteger等。
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
公平锁和非公平锁
- 非公平锁,是指JVM按随机、就近原则分配锁的机制。由于非公平锁实际执行的效率高5-10倍,最常用的分配机制,Synchronized关键字采用的就是非公平锁,ReetrantLock的lock默认方法也是非公平锁,。
- 公平锁,锁的分配机制是公平的,多核下需要维护一个队列,通常先对锁提出获取请求的线程会先被分配到锁,ReentrantLock在构造函数中提供了是否公平锁的初始化设置。
可重入锁(递归锁)
指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,不会因为之前已经获取过还没释放而阻塞。ReentrantLock和Synchronized都是可重入锁。
独占锁和共享锁
- JUC中提供的加锁模式分为独占锁和共享锁。
- 独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock和Synchronized就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读冲突,如果某个只读线程获取锁,则其他读线程都只能等待。
- 共享锁则允许多个线程同时获取锁,并发访问共享资源,如ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
互斥锁(同步锁)和死锁
- 互斥锁,我们要保证线程的互斥(同步),就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。可以使用synchronized关键字来取得一个对象的同步锁,或者使用ReentrantLock创建一个同步锁。
- 死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
CAS(Compare And Swap)
CAS是比较与交换,是一种无锁算法,可以认为是一种乐观锁。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
CAS算法的过程:它包含3个参数CAS(V,E,N)。V表示要更新的变量(内存值),E表示预期值(旧的),N表示新值。当且仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
java实现时CAS,是调用unsafe.compareAndSwapInt方法,查看native代码可知,最后执行的是汇编命令cmpxchg。
CAS虽然高效,但是会存在几个问题:
- ABA问题,解决思路是变量添加版本号的方式。这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
- 当CAS自旋时间长,会给CPU带来大量的开销。
- 只能保证一个共享变量的原子操作。
自旋锁和适应性自旋锁
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
当某线程尝试获取同步资源的锁失败时,不放弃CPU时间片,不阻塞,进行自旋,如果自旋完成后锁已释放,那么可以不必阻塞而直接获取同步资源,避免切换线程的开销。
自旋锁的原理同样是CAS。JDK6中默认开启自旋锁,并引入了适应性自旋锁(Adapative Self Spinning),当自旋超过了限定的次数(默认是10,可使用-XX:PreBlockSpin来更改)没有成功获得锁,就挂起线程。
在自旋锁中,另有三种常见的锁形式:TicketLock、CLHlock和MCSlock。
对象头
- synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头。
- 在Hotspot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
- 其中对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。数组对象的对象头还额外包括Array Length。
类型 | Mark Word | Klass Pointer | Array Length(数组对象独有) |
---|---|---|---|
32位JVM | 32 bits | 32 bits | 32 bits |
64位JVM | 64 bits | 64 bits,开启指针压缩(默认开启)后是32 bits | 64 bits,开启指针压缩后是32 bits |
64位JVM的对象头描述如下,可使用jol工具来打印对象头信息。
- 分代年龄:占4位,即最大值为15,所以新生代中的对象在eden区和survior区进行15次转移,当达到阈值时,会转移到老年代。通过VM参数-XX:MaxTenuringThreshold设置的最大值为15。
- Epoch:偏向时间戳。
- hashcode:31位的对象标识Hash码,采用延迟加载技术,只有调用后才会设置。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
1 | <dependency> |
1 | import org.openjdk.jol.info.ClassLayout; |
可以看到在64位JVM中,Mark Word占8个字节,开启指针压缩后Klass Pointer占4个字节。
JDK8中,指针压缩默认开启,如需要关闭可使用vm参数-XX:-UseCompressedOops。
另外,由于内存地址按照8字节对齐,长度必须是8的倍数,因此会添加Padding从12字节补全到16字节。
可以看到在64位JVM中,Mark Word占8个字节,未开启指针压缩的Klass Pointer占8个字节。
synchronized互斥锁(同步锁)
- synchronized可以把任意一个非NULL对象当作锁。属于独占式的悲观锁,还是可重入锁。通过synchronized可实现同步代码块和同步方法。
- 每个对象都有个monitor对象,加锁就是在竞争monitor对象,代码块加锁是在前后分别加上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标记位来判断的。
- synchronized是一个重量级操作,需要调用操作系统相关接口,性能是低效。Java6以后进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在对象头中有标记位,不需要经过操作系统加锁。
synchronized的作用范围:
- 作用于实例方法,锁住的是对象的实例(this)
- 作用于静态方法,锁住的是类的class对象的实例,class对象实例存储在永久代全局共享的,相当于类的一个全局锁。
- 作用于静态方法,锁住的是所有以该对象为锁的代码块。
synchronized的核心组件:
- Wait Set:调用wait方法被阻塞的线程被放置在这里。如果在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList
- Contention List:先进先出的竞争队列,所有请求锁的线程首先被放在这个竞争队列中
- Entry List:Contention List中那些有资格成为候选资源的线程被移动到 Entry List中
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck
- Owner:当前已经获取到所资源的线程被称为Owner
- !Owner:当前释放锁的线程
1 | import java.util.concurrent.TimeUnit; |
无锁、偏向锁、轻量级锁、重量级锁
- 这四种锁是指锁的状态,专门针对Synchronized的,存放在对象头里。
- Synchronized开始只有重量级锁,由于效率问题,JDK从1.6开始优化,引入偏向锁、轻量级锁,来减少重量级锁的使用。
- 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀。
锁状态:
- 无锁:当一个对象刚开始new出来时,该对象是无锁状态。
- 偏向锁:如果有线程上锁,指的就是把markword的线程ID改为自己线程ID的过程。偏向锁只依赖一次CAS原子指令,在只有一个线程执行同步块时进一步提高性能。
- 轻量级锁:如果有线程竞争,撤销偏向锁,升级轻量级锁,线程在自己的线程栈生成LockRecord,用CAS操作将markword设置为指向自己这个线程的LockRecord的指针,设置成功者得到锁。轻量级锁的获取及释放依赖多次CAS原子指令,轻量级锁是为了在线程交替执行同步块时提高性能。
- 重量级锁:如果竞争加剧(超过10次自旋或者自旋线程数超过CPU核数的一半)升级重量级锁,向操作系统申请资源,而操作系统实现线程之间的切换需要从用户态转换到核心态,导致效率很低。Sychronized通过对象内部的monitor(监视器锁)来实现,monitor又依赖于底层操作系统的Mutex Lock互斥锁来实现。
偏向锁验证:
1 | import org.openjdk.jol.info.ClassLayout; |
轻量级锁:
1 | import org.openjdk.jol.info.ClassLayout; |
重量级锁:
1 | import org.openjdk.jol.info.ClassLayout; |
ReentrantLock互斥锁(同步锁)
ReentrantLock和Synchronized锁的共同点:
- 都是用来协调多线程对共享对象、变量的访问
- 都是可重入锁,同一线程可以多次获得同一个锁
- 都保证了可见性和互斥性
ReentrantLock和Synchronized锁的区别如下:
比较方面 | Synchronized | ReentrantLock |
---|---|---|
1.原始构成 | 是java语言的关键字,是原生语法层面的互斥,需要jvm实现 | 是JDK 1.5之后提供的API层面的互斥锁类 |
2.代码编写 | 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用,更安全 | 要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块来完成 |
3.灵活性 | 锁的范围是整个方法或synchronized块部分 | Lock因为是方法调用,可以跨方法,灵活性更大类 |
4.等待可中断 | 不可中断,除非执行完成或者抛出异常分 | Lock因为是方法调用,可以跨方法,灵活性更大类 |
5.是否公平锁 | 非公平锁 | 两者都可以,默认公平锁,false为非公平锁 |
6.条件Condition | 通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能 | |
7.提供的高级功能 | 提供很多方法用来监听当前锁的信息 |
1 | import java.util.concurrent.TimeUnit; |
线程的等待和唤醒
- object.wait、object.notify、object.notifyAll方法必须在synchronized修饰的代码块中才能正常使用,否则会报IllegalMonitorStateException异常。
- condition.await、condition.signal、condition.signalAll方法必须在ReentrantLock的lock和unlock期间使用,否则会报IllegalMonitorStateException异常。
- Condition类的awiat方法和Object类的wait方法等效。Condition类的signal方法和Object类的notify方法等效。Condition类的signalAll方法和Object类的notifyAll方法等效。
- ReentrantLock类可以唤醒指定条件的线程,而Object的唤醒是随机的。因此Condition唤醒一般使用signal,Object唤醒一般使用notifyAll。
Semaphore
- Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。
- Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池。
- 我们可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。
- Semaphore基本能完成ReentrantLock的所有工作,使用方法也与之类似,通过acquire与release方法来获得和释放临界资源。
- Semaphore提供了公平与非公平锁的机制,也可在构造函数中进行设定。
案例:3个线程轮流打印ABC
1、使用synchronized
1 | public class ThreadSignalSynchronized { |
2、使用Condition
1 | import java.util.concurrent.locks.Condition; |
3、使用Semaphore
1 | public class ThreadSignalSemaphore { |
读写锁
- 为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制。
- 读锁,如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁
- 写锁,如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁
- 读写锁有个接口java.util.concurrent.locks.ReadWriteLock,也有具体的实现ReentrantReadWriteLock。
1 | import java.util.ArrayList; |
分段锁
- 分段锁也并非一种实际的锁,而是一种思想。
- ConcurrentHashMap是学习分段锁的最好实践。
锁优化
- 减少锁持有时间,只用在有线程安全要求的程序上加锁。
- 减小锁粒度,将大对象,拆成小对象,大大增加并行度,降低锁竞争。如分段锁的典型应用ConcurrentHashMap,默认被进一步细分为16个Segment(段),就是锁的并发度。
- 锁分离,最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。
- 锁粗化,在遇到一连串地对同一锁不断进行请求和释放的操作时,把所有的锁操作整合成锁的一次请求,从而减少对锁的请求同步次数。
- 锁消除,在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。
常用工具
- CountDownLatch,线程计数器,可初始化一个线程数量,每当线程完成任务后可以调用countdown方法使得计数器-1,直到计数器变为0,调用await方法等待的线程才可以继续执行。
- CyclicBarrier,回环栅栏,等待至barrier状态再全部同时执行。
- ThreadLocal,线程本地存储,ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。数据存储在ThreadLocalMap中,线程结束时必须要remove,不然会造成内存泄漏。可用来存储用户Session,或者解决线程安全问题(如java7中的DateTimeFormatter)。
- ThreadLocalRandom,在concurrent包内,和ThreadLocal没啥关系。使用Random在大并发下会有线程阻塞问题。
volatile关键字
Java 语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。
- volatile变量具备两种特性,volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
- 变量可见性,是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。
- 禁止重排序,volatile禁止了指令重排。
- volatile比sychronized更轻量级的同步锁.在访问volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞。volatile适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。
- volatile不保证自增操作原子性。
AQS(AbstractQueuedSynchronizer,抽象的队列同步器)
- AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock、Semaphore、CountDownLatch等。
- AQS定义了2种资源共享方式,Exclusive独占资源(如ReentrantLock),以及Share共享资源(如Semaphore、CountDownLatch)。比较特殊的是ReentrantReadWriteLock同时实现了独占和共享。
- AQS 只是一个抽象类,它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列),具体资源的获取/释放方式交由自定义同步器去实现。
自定义同步器需要实现以下方法:
- isHeldExclusively(),该线程是否正在独占资源。
- tryAcquire(int),独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int),独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int),共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余
可用资源;正数表示成功,且有剩余资源。 - tryReleaseShared(int),共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回
true,否则返回false。
四、线程池
- 作用:是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
- 特点:线程复用;控制最大并发数;管理线程。
1 | import java.util.concurrent.*; |
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。
线程池主要分为以下4个组成部分:
- 线程池管理器:用于创建并管理线程池
- 工作线程:线程池中的线程
- 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
- 任务队列:用于存放待处理的任务,提供一种缓冲机制
线程池工作流程:
1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用 execute() 方法添加一个任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务。
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
- 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务。
- 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
线程池管理器
ExecutorService(ThreadPoolExecutor的顶层接口)使用线程池中的线程执行每个提交的任务,通常我们使用Executors的工厂方法来创建ExecutorService。
ThreadPoolExecutor是用来处理异步任务的一个接口,可以将其理解成为一个线程池和一个任务队列,提交到 ExecutorService 对象的任务会被放入任务队或者直接被线程池中的线程执行。ThreadPoolExecutor 支持通过调整构造参数来配置不同的处理策略,本文主要介绍常用的策略配置方法以及应用场景。
1 | public ThreadPoolExecutor(int corePoolSize, |
- corePoolSize:线程池中的核心线程数量。可使用setCorePoolSize动态更改。
- maximumPoolSize:线程池中的最大线程数量。可使用setMaximumPoolSize动态更改。
- keepAliveTime:当前线程池数量超过corePoolSize时,多余的空闲线程的存活时间,即多次时间内会被销毁。
- unit:keepAliveTime 的单位。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:线程工厂,用于创建线程,一般用默认的即可。
- handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。JDK的4种内置拒绝策略均实现了RejectedExecutionHandler接口。如无法满足,可自定义。
- AbortPolicy,直接抛出异常,阻止系统正常运行。
- CallerRunsPolicy,只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。
- DiscardOldestPolicy,丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务
- DiscardPolicy,该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
预定义线程池
Executors是jdk里面提供的创建线程池的工厂类,它默认提供了4种常用的线程池应用。
1、FixedThreadPool,核心线程和最大线程一样,无界阻塞队列,适用于web服务瞬时削谷。
1 | public static ExecutorService newFixedThreadPool(int nThreads) { |
2、CachedThreadPool,线程数量无限制,60s后自动结束,可快速处理大量耗时较短任务。
1 | public static ExecutorService newCachedThreadPool() { |
3、SingleThreadExecutor,单线程。
1 | public static ExecutorService newSingleThreadExecutor() { |
4、ScheduledThreadPool,使用的是DelayedWorkQueue。
1 | public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { |
阻塞队列
- 阻塞队列接口BlockingQueue继承自Queue接口。
- 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
- 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
- 阻塞队列的常规方法参考Queue。
类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element(e) | peek(e) | – | – |
主要有三种队列策略:
- Direct handoffs,直接握手队列。默认选择是SynchronousQueue,它将任务交给线程而不需要保留。
- Unbounded queues,无界队列。没有预定义容量的LinkedBlockingQueue。
- Bounded queues,有界队列。ArrayBlockingQueue,需要和有限的maximumPoolSizes配置有助于防止资源耗尽。
常见阻塞队列:
- ArrayBlockingQueue :由数组结构组成的有界阻塞队列。遵循FIFO原则,可通过构造器构造公平的阻塞队列。
- LinkedBlockingQueue :由链表结构组成的有界阻塞队列。对生产端和消费端采用独立的锁来控制同步数据。默认无限容量,可预定义容量构造有界阻塞队列。
- PriorityBlockingQueue :支持优先级排序的无界阻塞队列。默认采用自然顺序升序排序,可通过compareTo自定义。相同优先级的无法保证。
- DelayQueue:使用优先级队列实现的无界阻塞队列。支持延时获取元素,队列的元素必须实现Delayed接口,在创建元素时可以以指定多久才能从队列中获取当前元素。可用于缓存设计和定时设计。
- SynchronousQueue:不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。适合传递性场景。
- LinkedTransferQueue:由链表结构组成的无界阻塞队列。多了tryTransfer和transfer方法
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列。多了addFirst、addLast、offerFirst、offerLast、peekFirst、peekLast等方法。