多线程
概述
串行 并行 并发
串行sequential 是排队进行任务,比如接力
并行parallel 所有任务同时进行,一边打游戏一边听歌
并发concurrent 是在一个任务等待的时候,开始进行另外一个任务,
线程的生命周期

• 新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
• 就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
• 运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
• 阻塞状态:
如果一个线程执行了sleep(睡眠)、yield 等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
• 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
• 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
• 等待状态:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。可以通过join()转换成就绪
• 死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
线程安全
- 有序性: 代码的执行顺序, 和编写的顺序可能没有保障
- 可见性: 一个线程对数据进行更新 , 另外一个线程可能读新数据也可能读旧数据
- 原子性: 操作可能无法做到不可分割
有硬件原因: 每个线程拥有自己的堆内存和方法区
多线程编程
线程的创建
在java中,创建一个线程就是创建一个Thread类的对象。
继承Thread
重写run()方法中的代码就是子线程要执行的代码
public class Machine extends Thread{
private int a =1 ;
@Override
public void run(){
//继承Thread类,覆盖run()方法
a++;
System.out.println(a);
sleep(); //进入睡眠状态
}
使用线程的start()方法来启动该线程,告诉jvm相应的线程准备好了,具体什么时候运行,则由线程调度器决定。并且如果有多个线程启动,线程的执行顺序并不一定。
public static void main(String args[]){
Machine machine = new Machine();
machine.start(); //启动线程 会自动执行run方法
machine.interrupt(); //中断machine线程的睡眠
yield(); //线程让步
}
实现Runnable接口
有些时候类已经有父类了,就可以实现runnable接口
public class Machine implements Runnable{
private int a =0;
@Override
public void run(){
// 重载Runnable接口
a++;
System.out.println(a);
}
}
public static void main(String args[]){
Machine machine = new Machine();
// 调用Thread 定义的构造方法,传入runnable接口的实现类,
Thread t1 = new Thread(machine);
// 启动线程
t1.start();
}
使用匿名内部类,在Tread的构造函数中定义匿名类。
public static void main(String args[]){
Thread t2 = new Thread(new Runnable(){
@Override
public void run(){
System.out.println('匿名内部类');
}
});
t2.start();
}
常用方法
静态方法 | |
---|---|
public static Thread currentThread() | 返回对当前正在执行该方法的线程对象的引用。 |
public static void sleep(long millisec) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。 |
public static void yield() | 暂停当前正在执行的线程对象,优先给其他线程执行。 |
静态方法通过Thead.方法名直接使用。
Thead.currentThread() 返回的当前线程,其实就是调用该方法的线程,在主线程中可以调用子线程的run方法,执行该方法的是主线程而非子线程
public static void main(String args[]){
// 主线程调用Machine构造方法
Machine machine = new Machine();
// 主线程调用Thread 定义的构造方法,传入runnable接口的实现类
Thread t1 = new Thread(machine);
// 启动线程 t1线程去执行它的run方法
t1.start();
// 调用t1类的run方法,此方法由当前线程(main)执行,不开启新线程
t1.run();
}
实例方法 | |
---|---|
public final void setDaemon(boolean on) | 将该线程标记为守护线程(true)或用户线程(false)。 |
public final void setName(String name) | 改变线程名称,使之与参数 name 相同。 |
public final boolean isAlive() | 测试线程是否处于活动状态(已启动未终止)。 |
public final void setPriority(int priority) | 更改线程的优先级(1-10)。一般不设置 |
public void interrupt() | 中断线程。 |
守护线程
为其他线程提供服务,当虚拟机中只有守护线程时,java虚拟机会停止运行
public void interrupt() 并不是真正的中断线程,只是给线程一个标记,可以在线程里拿到这个标记,停止线程
public static void main(String args[]){
Machine machine = new Machine();
Thread t1 = new Thread(thread1);
t1.start();
t1.interrupt();// 给子线程中断标记
}
public class thread1 implements Runnable{
private int a =0;
@Override
public void run(){
while(true){
// 如果子线程的中断标志为true,则中断
if(this.isInterrupted()){
System.out.println("byebye");
break;
}
System.out.println("工作中");
}
}
}
线程同步
通过锁对共享数据的并发访问转换成串行访问,一个线程持有锁才能访问共享数据,一次只能有一个线程持有锁,保证了操作的原子性和有序性,锁的获得和释放动作,隐含着冲刷处理器缓存的动作,保证了可见性。
对数据的访问和修改都需要添加锁,进行同步
锁
可重入:一个线程持有一个锁时,还能申请该锁,称为可重入
争用与调用:内部锁为公平锁,显示lock锁支持公平也支持不公平锁。
粒度:衡量可保护的数据的大小(代码的长度?),大=粗,小=细,过粗会导致申请锁进行多余的等待,过细会增加锁调度的开销。
乐观和悲观:
乐观锁认为别人不会操作数据,只会对数据提交时进行验证是否修改,(一般通过加版本号的方式)如果修改则回退该操作。
悲观锁认为别人会修改数据,一次只给一个人锁,释放锁之前其他人不允许操作。
一般想要同步代码。将锁对象设置为static,因为只有使用同一个锁对象才能同步。
公平锁和非公平锁:
-
公平锁 : 按照时间顺序, 先到先得. 会导致线程饥饿. 性能高 synchronized
-
非公平锁 : 随机从阻塞队列中选择一个线程. 性能低 ReentrantLock可以在创建锁对象时传true 设置为公平锁
内部锁 synchronized
synchronized关键字修饰,是排他锁,一次只能有一个线程持有 , 获得锁对象的线程, 才能执行锁对象对应的代码
多个线程必须要获取同一个锁对象时 , 才会串行的等待。 多个线程等待同一把锁,叫做同步。不管任何方法只要用的一个锁,同一时间所有只能由一个线程访问。
相当于多个房间(方法或者代码块)都用了一种锁(锁对象),只有拿到唯一一把钥匙的人(线程)才能进入。
修饰代码块
synchronized(锁对象){
代码块
}
修饰方法
方法example拥有锁 默认对象锁为this
public synchronized void example(){
代码块
}
修饰代码块的粒度细,效率高
修饰方法的粒度粗,效率低
补充:
- 某个线程出现异常,会将锁对象释放
举例
public class Test {
public static void main(String[] args) {
Test test = new Test();
new Thread(new Runnable() {
@Override
public void run() {
test.mm();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test.mm();
}
}).start();
}
public void mm(){
// this指的就是test实例对象,新建的两个线程都是调用test对象的方法,其中一个线程获得test对象的 锁,另外的线程就会进入等待区等待代码执行完,对象锁释放。
synchronized (this){
for(int i =0;i<100;i++){
System.out.println(Thread.currentThread().getName()+i);
}
}
}
以上this指的就是Test实例对象,新建的两个线程都是调用test这个对象的方法,其中一个线程获得test对象的锁,另外的线程就会进入等待区等待代码执行完,对象锁释放。这时会依次执行for循环。
使用this,当两个线程调用的不是同一个对象时,因为使用的不是一把锁,则会并发交替进行。如下
public class Test {
public static void main(String[] args) {
Test test = new Test();
Test test2 = new Test();
new Thread(new Runnable() {
@Override
public void run() {
// 该线程调用test对象,拿到test对象的锁
test.mm();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// 该对象调用test2对象,拿到test2对象的锁
test2.mm();
}
}).start();
}
public void mm(){
// this指的就是调用该方法的实例对象
synchronized (this){
for(int i =0;i<100;i++){
System.out.println(Thread.currentThread().getName()+i);
}
}
}
使用this作为锁对象, 只有调用同一个实例对象的线程会排队等待
所以有时会使用常量对象作为锁对象 , 所有调用该方法的线程都会排队等待
public final static Object OBJ = new Object();
public void mm(){
synchronized (OBJ){
for(int i =0;i<100;i++){
System.out.println(Thread.currentThread().getName()+i);
}
}
}
多个方法用同一把锁 , 也得等待这个锁的释放
public class Test {
public static void main(String[] args) {
Test test = new Test();
Test test2 = new Test();
new Thread(new Runnable() {
@Override
public void run() {
// obj锁的方法
test.mm();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// obj锁的方法
test2.mm2();
}
}).start();
}
public final static Object OBJ = new Object();
public void mm(){
// OBJ锁
synchronized (OBJ){
for(int i =0;i<100;i++){
System.out.println(Thread.currentThread().getName()+i);
}
}
}
public void mm2(){
// OBJ锁
synchronized (OBJ){
for(int i =0;i<100;i++){
System.out.println(Thread.currentThread().getName()+i);
}
}
}
}
死锁
当多线程顺序中,获得多个锁的顺序不一致,就会导致死锁。
当需要获得多个锁,所有线程获得锁的顺序一致,就能避免死锁。
volatile
volatile作用使变量在多个线程之间可见。强制线程从公共内存中读取变量的值,而不是从工作内存中读取。
没有volatile时,有时候一个线程修改了一个值,另一个线程无法读到,使用volatile修饰变量,则其他线程能够读到。
volatile 可以保证数据的可见性,不能保证原子性,只解决可见性。
synchronized 可以保证数据的原子性和数据的可见性,解决的是多个线程之间的同步性。
public final volatile Object OBJ = new Object();
Lock 显示锁
Lock为可重入锁,即一个线程获得对象锁后,还能再次获得同一个对象锁,这就是可重入性,
lock显示锁有几个子类,ReentrantLock(),
ReentrantLock
当一个线程获得锁时,对于其他方法中同一个lock对象,其他线程无法获得锁,就无法执行其他方法,例如当t1线程进入test方法,获得lock锁,则其他线程无法调用test2,因为lock是可重入锁,所以t1可以在释放锁之前调用test2。
一般情况下,在try代码块中获得锁,在finally代码块中释放锁。
使用**.lock()**方法,当获得锁后,即使线程被调用.interrupt()方法也不会被中断
使用**.lockInterruptibly()**方法,可以被.interrupt()方法中断
// 定义锁(非公平锁)
static Lock lock = new ReentrantLock();
// 定义锁(公平锁)
static Lock lock = new ReentrantLock(true);
public void test(){
lock.lock(); // 获得锁,其他线程就会停留尝试获得锁的地方,不会被.interrupt()方法中断
lock.lockInterruptibly() // 获得锁,可以被.interrupt()方法中断
代码
test2();
lock.unlock(); // 释放锁,随机一个线程获得锁
}
static public void test2(){
lock.lock(); //
代码
lock.unlock(); //
}
对于synchronized内部锁,如果一个线程开始等待锁,只有两种结果,要么获得锁,要么继续等待,所以会产生死锁。
对于ReentrantLock可重入锁来说,还可以在等待过程中,使用interrupt中端线程, 来解决死锁问题
// 定义锁
static Lock lock = new ReentrantLock();
static public void test2(){
lock.lockInterruptibly(); //
while(1){
// 假设发生死锁
}
lock.unlock(); //
}
// 定义线程类
static class subThread extends Thread{
@Override
public void run() {
test2();
}
}
public static void main(String[] args) {
Thread t1 = new subThread();
Thread t2 = new subThread();
t1.start();
t2.start();
Thread.sleep(3000);
if(t1.isAlive()){t1.interrupt()};
}
tryLock()
tryLock(Long time, TimeUnit unit) 的作用是, 在给定时间内锁没有被其他线程持有, 当前也没有被中断, 则获得该锁, 获得锁则返回true, 指定时间内没有获得锁, 则不再等待, 返回false.
lock.tryLock(3,TimeUnit.SECONDS) // 最多等待3s
lock.tryLock() // 锁被持有,直接放弃
以下代码, 一个线程拿到锁之后, 另一个线程最多等待3秒, 超时之后不再等待, 打印我不等了
// 定义锁
static Lock lock = new ReentrantLock();
static public void test2(){
// 最多等待3秒
if (lock.tryLock(3,TimeUnit.SECONDS)){
执行4秒的任务
Thread.sleep(4000);
}else{
System.out.println("我不等了");
}
lock.unlock(); //
}
// 定义线程类
static class subThread extends Thread{
@Override
public void run() {
test2();
}
}
public static void main(String[] args) {
Thread t1 = new subThread();
Thread t2 = new subThread();
t1.start();
t2.start();
}
lock.isHeldByCurrentThread() 当前线程是否持有锁
lock.getHoldCount() 当前锁被持有的次数
lock.getQueueLength() 等待锁的线程的预估数
lock.getWaitQueueLength(Condition) 该condition进入等待的线程的预估数(调用wait()方法的数量)
lock.hasQueuedThead(Thread) 该线程是否在等待该锁
lock.hasQueuedTheads() 是否有线程在等待该锁
lock.hasWaiters(Condition) 该条件该条件是否有线程在等待
lock.isFair() 是否为公平锁
lock.isLocked() 当前锁是否被线程持有
ReentrantReadWriteLock 读写锁
是改进的排它锁, 共享/排它锁. 允许多个线程同时读取, 但只允许一个线程对数据进行更新.
读锁可以由多个线程持有 , 写锁只允许一个线程持有. 但读锁和写锁不能同时被任何线程持有.
在读锁被持有时, 其他线程可以获得读锁, 但无法获得写锁. 在写锁被线程持有时, 其他线程无法获得任何锁
readLock()与write()返回的是同一个锁的不同角色
//获得读写锁
ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 获得读锁
Lock readLock = rwLock.readLock()
// 获得写锁
Lock writeLock = rwLock.writeLock();
// 读数据
readLock.lock(); // 拿读锁
try{
读数据
}finally{
readlock.unlock();
}
// 写数据
writeLock.lock(); // 拿读锁
try{
读数据
}finally{
readlock.unlock();
}
常用原子类
**AtomicInteger类 **
使用 .getAndIncrement()方法可以线程安全自增
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.getAndIncrement();
CAS
可以把 read - modify - write 操作, 变得线程安全
例如 i++
线程从主内存拿到变量10放到线程的工作内存 , 对线程进行加1操作, 再写入到主内存
在这个操作过程中, 可能另一个线程也拿到变量, 就会出现两个线程都将值11写入,与预期不符
CAS会在写入到主内存之前, 验证主内存的值和读取到本线程内存的值是否相等, 相等则写入, 不相等则撤销这次操作.
CASCounter casCounter = new CASCounter();
casCounter.incrementAndGet();
但会碰到ABA问题, 数据A变成B再变回A, 会被误认为没有改变.
基于CAS, 改进ABA问题, 有了一些原子变量类

线程通信
等待/通知机制 wait和notify
在一个A线程中, 需要满足条件才能继续执行, 稍后其他的线程B更新条件使得A线程的条件得到满足, 可以将A线程暂停, 直到A线程条件得到满足再将A线程唤醒.
synchronized实现
Object()类的wait()方法可以使执行当前代码的线程等待, 转入阻塞状态 进入等待池 直到接到通知或被中断为止.
- wait()方法只能在同步代码块当中, 由锁对象调用, 所以必须获得锁才能用wait()方法
- wait()方法被调用, 会释放锁
- wait(long) 在long时间内没有被唤醒, 则自动唤醒
Object()类的notify()可以唤醒线程 , 也必须在同步代码块中由锁对象调用, notify()调用后, 会等待当前同步代码块执行完毕才会释放锁对象 如果有多个等待的线程 , 则只能随机唤醒其中的一个. notifyAll() 可以唤醒所有线程.
通知过早
当线程还没有进入等待状态, 使用notify会打乱运行逻辑
当线程处于等待状态时 , 会被本线程的interruput()方法中断, 也会释放锁对象
public class Test {
public static void main(String[] args) {
String lock = "test";
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(lock){
System.out.println("t1开始执行");
System.out.println("t1开始等待");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1等待结束");
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(lock){
System.out.println("t2开始唤醒");
lock.notify(); //释放lock的锁,
System.out.println("等待t2同步代码块执行");
}
}
});
t1.start();
try {
Thread.sleep(3_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hhehAvzQ-1647140240771)(多线程与高并发/img/1646623949915.png)]
Lock显示锁实现
使用newContition()方法返回Condition对象, Condition对象可以选择唤醒的线程.
await(): 使当前线程等待, 同时释放锁, 当其他线程调用signal()时, 线程会重新获得锁并继续执行.
signal(): 唤醒一个当前condition对象等待队列的线程
调用以上两个方法时, 需要持有相关的lock锁.
不同线程可以使用同一个lock对象,通过使用不同的condition对象,来指定唤醒的的对象的等待队列的线程,
// 定义锁
static Lock lock = new ReentrantLock();
// 获得Condition对象
static Condition conditionA = lock.newCondition();
static Condition conditionB = lock.newCondition();
public void waitA(){
lock.lock();
conditionA.wait()
lock.unlock();
}
public void waitB(){
lock.lock();
conditionB.wait()
lock.unlock();
}
// 假设A B线程两个线程的run()方法为执行waitA(),waitB();
// 唤醒执行waitA的线程
lock.lock();
conditionA.signal()
lock.unlock();
// 唤醒执行waitB的线程
lock.lock();
conditionB.signal()
lock.unlock();
以下例子,子线程在获得锁之后等待,释放锁,主线程睡眠3s后获得锁,唤醒子线程
public class Test {
// 定义锁
static Lock lock = new ReentrantLock();
// 获得Condition对象
static Condition condition = lock.newCondition();
// 定义线程类
static class SubThread extends Thread{
@Override
public void run() {
lock.lock(); //获得锁
try {
condition.await(); // 进入等待状态
System.out.println("解锁");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //释放锁
}
}
}
public static void main(String[] args) throws InterruptedException {
SubThread subThread = new SubThread();
subThread.start();
// 线程睡眠
Thread.sleep(3_000);
// 先要获取锁之后,才能唤醒
lock.lock();
try {
condition.signal();
}finally {
lock.unlock();
}
}
}
生产者消费者模式
负责产生数据的为生产者, 负责使用数据的模块是消费者. 先有数据才能使用, 没有数据时,消费者需要等待.
在多生产者多消费者情况下, 使用if作为条件, 会因为等待状态时其他线程改变了条件, 导致消费者取空, 所以要使用while判断条件, 在被唤醒之后再判断一次条件. 此外, 使用notify会导致假死, 所有线程都处于等待状态. 所以要使用notifyAll().
管道实现线程通信
在java.io包中的PipeStream用于线程之间传送数据, 使用PipedInputStream 和 PipedOutputStream管道字节流在线程之间传递数据.
Out写入数据传出去, in读取数据
public class Test {
public static void main(String[] args) {
PipedInputStream inputStream = new PipedInputStream();
PipedOutputStream outputStream = new PipedOutputStream();
try {
// 将管道连接
inputStream.connect(outputStream);
} catch (IOException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
writeData(outputStream);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
readData(inputStream);
}
}).start();
}
// 写入数据
public static void writeData(PipedOutputStream out){
try {
for (int i=0; i<100; i++) {
// 把i转成字符串
String data = "" + i;
// 将data写入到管道中
out.write(data.getBytes(StandardCharsets.UTF_8));
}
// 关闭管道
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 读取数据
public static void readData(PipedInputStream in){
// 创建byte数组 保存byte
byte[] bytes = new byte[128];
try {
int len = in.read(bytes); //将字节读入数组,没有读到则返回-1
while (len!=-1){
// 把前len个字节转换成字符串打印
System.out.println("读 :"+new String(bytes,0,len));
len = in.read(bytes); //继续读
}
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
ThreadLocal
ThreadLocal可以给每个线程一个自己的值. 这个例子并不完美, 当多个线程共用一个对象的时候可以使用ThreadLocal.
原理大概是, 每个线程都有一个ThreadLocalmap, key为ThreadLocal对象, value为set的值
ThreadLocal未设置初始值时 , 所get的为null,可以重写 initialValue()方法, 定义默认值
public class Test {
// 定义ThreadLocal对象
static ThreadLocal threadLocal = new ThreadLocal();
// 定义线程类
static class subThread extends Thread{
@Override
public void run() {
for(int i = 0; i< 20; i++){
// 将值与线程关联
threadLocal.set(Thread.currentThread().getName()+": "+i);
// 获取线程相关联的值
System.out.println(threadLocal.get());
}
}
}
public static void main(String[] args) {
Thread t1 = new subThread();
Thread t2 = new subThread();
t1.start();
t2.start();
}
}
修改ThreadLocal的默认值
static class SubThreadLocal extends ThreadLocal{
@Override
protected Object initialValue() {
return 1;
}
}
// 定义ThreadLocal对象
static ThreadLocal threadLocal = new SubThreadLocal();
join()
线程管理
线程组
使用线程组定义一组相关的线程, 或者子线程组, 类似于文件夹
Thread类创建线程时可以指定线程组 , 不指定则默认为父类所在的线程组
现在已经淘汰阿
守护线程
调用线程组的 setDaemon(true) 可以把线程组设置为守护线程组. 守护线程组没用活动线程, 则销毁该守护线程组, 守护线程组中可以有守护线程或者活动线程
public class Test {
public static void main(String[] args) {
// 定义线程组
ThreadGroup threadGroup = new ThreadGroup("group");
// 设置为守护线程
threadGroup.setDaemon(true);
// 添加三个非线程
for (int i=0;i<3;i++){
new Thread(threadGroup, new Runnable() {
@Override
public void run() {
while (true){
System.out.println("这是线程"+Thread.currentThread().getName());
}
}
}).start();
}
}
}
捕获线程的异常
线程产生异常会优先调用 线程设置的回调接口>线程组设置的回调接口>System.err
线程设置回调接口
// 设置线程的全局回调接口
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("发生了异常");
}
});
注入Hook钩子线程
在JVM退出时, 会执行Hook线程, 为了防止进程重复启动, 会在程序启动时创建.lock文件, 校验程序是否启动, 在退出时删除该文件, 注意要避免在Hook线程中进行复杂的操作.
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
System.out.println("JVM退出,会启动当前线程");
}
});
线程池
核心
线程池是有效使用线程的一种方式 , 线程池内部预先创建一定数量的工作线程 , 客户端任务作为对象提交给线程 , 线程池将任务缓存在工作队列中 , 线程池的工作线程不断地从队列中取出任务并执行
https://www.jianshu.com/p/210eab345423
ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,(必
int maximumPoolSize,(必
long keepAliveTime,(必
TimeUnit unit,(必
BlockingQueue<Runnable> workQueue,(必
ThreadFactory threadFactory,(选
RejectedExecutionHandler handler(选)
// 一共最多有七个参数
-
int corePoolSize 该线程池中核心线程数最大值
线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过则新建的是非核心线程,核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)超过一定时间(long keepAliveTime),就会被销毁掉
-
int maximumPoolSize 最大线程数
-
long keepAliveTime 非核心线程存活时长
一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉
如果allowCoreThreadTimeOut这个属性为true , 这个时长也会作用于核心线程
-
TimeUnit unit 存活时长的单位
-
BlockingQueue workQueue
当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务 , 以下是常见的使用对象
-
SynchronousQueue:( 直接摇人) 这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大
-
LinkedBlockingQueue:(只用核心线程 队列无界) 这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
-
ArrayBlockingQueue:(优先核心, 队满摇人 队列有界) 传入参数限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误
-
DelayQueue:(入队等候) 队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务
-
PriorityBlockingQueue: 以上都是先进先出执行任务,这个根据优先级来执行任务
-
ThreadFactory threadFactory 线程工厂 创建线程的方式, 可以自定义
-
RejectedExecutionHandler handler 异常处理, 拒绝策略,当工作队列发生错误时,启用
TimeUnit的单位
NANOSECONDS : 1微毫秒 = 1微秒 / 1000
MICROSECONDS : 1微秒 = 1毫秒 / 1000
MILLISECONDS : 1毫秒 = 1秒 /1000
SECONDS : 秒
MINUTES : 分
HOURS : 小时
DAYS : 天
添加任务
threadPoolExecutor.execute(Runnable command)
public static void main(String[] args) {
// 创建5个线程的线程池
ExecutorService threadPool= new ThreadPoolExecutor(5,
10,
60L,
TimeUnit.MINUTES,
new ArrayBlockingQueue<Runnable>(20));
for (int i=0; i<30; i++){
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread()+"启动");
Thread.sleep(1_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
常见四种线程池
-
CachedThreadPool() 可缓存线程池
可缓存线程池:适合耗时短,数量多的任务
- 线程数无限制
- 有空闲线程则复用空闲线程,若无空闲线程则新建线程
- 一定程序减少频繁创建/销毁线程,减少系统开销
创建方法
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
实现:由SynchronousQueue队列实现
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
-
FixedThreadPool()
定长线程池:
- 可控制线程最大并发数(同时执行的线程数)
- 超出的线程会在队列中等待
创建方法
//nThreads => 最大线程数即maximumPoolSize
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads);
//threadFactory => 创建线程的方法,
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory);
源码: 由LinedBlockingQueue队列实现, 有界队列
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
-
ScheduledThreadPool()
定长线程池:
- 支持定时及周期性任务执行。
创建方法:
ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);
源码:使用
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } //ScheduledThreadPoolExecutor(): public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); }
-
SingleThreadExecutor()
单线程化的线程池:
- 有且仅有一个工作线程执行任务
- 所有任务按照指定顺序执行,即遵循队列的入队出队规则
创建方法:
ExecutorService singleThreadPool = Executors.newSingleThreadPool();
源码:使用的LinkedBlockingQueue队列
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
-
ForkJoinPool()
拒绝策略
RejectedExecutionHandler handler 拒绝策略,当工作队列发生错误时,例如新来一个任务使得线程超出线程池允许最大线程数量,的处理方法
拒绝策略:
- AbortPolicy: 抛出异常(默认)
- CallerRunsPolicy:在调用者线程中允许被放弃的任务
- DiscardOldestPolicy:丢弃最早的任务(马上要被执行的),尝试执行添加的任务
- DiscardPolicy:丢弃这个无法处理的任务
自定义拒绝策略
public static void main(String[] args) {
// 创建5个线程的线程池
ExecutorService fixedThreadPool= new ThreadPoolExecutor(1,
1,
60L,
TimeUnit.MINUTES,
new ArrayBlockingQueue<Runnable>(20),
new Handler());
for (int i=0; i<30; i++){
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread()+"启动");
Thread.sleep(1_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
// 实现该接口 即可
public static class Handler implements RejectedExecutionHandler{
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// r为出错请求的任务,executor为当前线程池
System.out.println("处理出错的任务");
}
}
ThreadFactory
线程池中的线程,由ThreadFactory创建,它只有一个用来创建线程的方法。
猜测:任务自动放入到了r的run方法中,可以在newThread中实现一些在线程创建时自定义的功能,例如设置为守护线程,使主线程结束时,所有线程结束
// 创建线程池,自定义线程的创建
ExecutorService fixedThreadPool= new ThreadPoolExecutor(1,
1,
60L,
TimeUnit.MINUTES,
new ArrayBlockingQueue<Runnable>(20),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
System.out.println("创建了一个线程");
return t;
}
},
new Handler());
监控线程池
ThreadPoolExecutor有以下实例方法可以监控线程池
方法 | 作用 |
---|---|
getActiveCount() | 线程池中正在执行任务的线程数量 |
getCompletedTaskCount() | 线程池已完成的任务数量,该值小于等于taskCount |
getCorePoolSize() | 线程池的核心线程数量 |
getLargestPoolSize() | 线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了maximumPoolSize |
getMaximumPoolSize() | 线程池的最大线程数量 |
getPoolSize() | 线程池当前的线程数量 |
getTaskCount() | 线程池已经执行的和未执行的任务总数 |
可以重写ThreadPoolExecutor类的以下方法对增加额外功能
方法 | 含义 |
---|---|
shutdown() | 线程池延迟关闭时(等待线程池里的任务都执行完毕),统计已执行任务、正在执行任务、未执行任务数量 |
shutdownNow() | 线程池立即关闭时,统计已执行任务、正在执行任务、未执行任务数量 |
beforeExecute(Thread t, Runnable r) | 任务执行之前,记录任务开始时间,startTimes这个HashMap以任务的hashCode为key,开始时间为值 |
afterExecute(Runnable r, Throwable t) | 任务执行之后,计算任务结束时间。统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止信息 |
线程池死锁
当线程池中执行A的同时,又提交了任务B,任务B进入等待队列中,如果任务A的结束需要等待任务B的完成,就有可能造成死锁。
同一个线程池提交相互独立的任务,相互联系的任务提交到不同的线程池。
锁的优化
- 减少锁持有的时间
- 减少锁的粒度
- 使用读写分离锁来代替独占锁
- 锁分离,可以同时执行的操作,使用不同的锁
- 粗锁化:将多次请求锁整合成一次
JVM对锁的优化
锁偏向:
如果一个线程获得了锁,那么这个线程再次请求锁时,无需再做同步操作(忽略synchronized关键字),提高了程序的性能,JVM偏向给获得过锁的线程再次给锁。
对象的对象头中,Mark Word 中保存了锁信息 ,是否为偏向锁为1,锁标值为01时,表示该锁为锁偏向状态。 若为可偏向状态,则测试Mark Word中的线程ID是否与当前线程相同,若相同,则直接执行同步代码。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁状态的线程才会释放锁 。出现竞争时,会升级成为轻量级锁。

轻量级锁:
在没有多线程竞争的情况下,使用轻量级锁能够减少性能消耗 。
理解:会把对象头中的mark word复制到锁记录中,然后把mark word变成指向锁记录的指针,锁记录中的owner指针指向锁对象的mark word。
如果失败了,就会验证mark word是否指向当前线程的锁记录,如果是就不需要再获取锁了,直接执行同步代码。如果不是就膨胀为重量级锁。
重量级锁:互斥锁