Java多线程篇

线程

一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于进程之间是相互独立的,而线程之间则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

  • Java进程结构拓扑
    image

计数器

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

Native方法

又成本地方式,可以直接访问操作系统资源,连接Java程序与本地资源的桥梁;如本地文件访问、网络请求、图形化界面访问,都需要使用Native方式进行连接。

虚拟机栈

每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

Thread

Thread继承了Runnable接口,Runnable接口是Java线程的最基本定义,提供了run方法用于执行自定义业务模块;Thread是Java线程了基本载体,定义了对线程的相关使用规范。

创建线程的方式

  • 继承Thread类
    继承Thread类,并实现实现run方法;使用时创建自定义的Thread子类实例,并调用start方法将线程放入线程组,开启线程的生命周期
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("This is a new thread!");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
  • 实现Runnable接口
    实现Runnable接口,并重写run方法;使用时创建Thread实例,并将自定义Runnable对象作为参数传入,调用start开启线程生命周期
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("This is a new thread!");
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}
  • 实现Callable接口-返回线程执行结果,产生阻塞
    实现Callable接口,并重写call方式实现具体业务
    使用过程:
    • 使用时先创建FutureTask实例并传入自定义Callable实例
    • FutureTask实现了Runnable接口,默认重写了run方法,run中会调用Callable.call执行业务代码
    • 创建Thread实例并传入FutureTask
    • 调用Thread实例的start开启线程周期
    • 调用FutureTask实例的get方法,阻塞获取线程运行结果
public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "This is a new thread!";
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        MyCallable callable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        String result = futureTask.get();
        System.out.println(result);
    }
}
  • 通过匿名内部类创建
    直接创建Thread实例,并实现Runnable内部类;使用时调用Thread实例的start开启线程生命周期
public class Main {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("This is a new thread!");
            }
        }).start();
    }
}
  • 线程池创建
    使用Executors工具类创建线程池,并在线程池中添加线程任务;这种方式线程池会自动管理线程生命周期,使用完毕后需要关闭线程池,否则核心工作线程不会被回收,导致内存泄漏。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("This is a new thread!");
            }
        });
       executorService.shutdown();
    }
}

线程生命周期

Java中重新自定义了线程生命周期

public static enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
        private State() {
        }
    }
  • NEW: 初始状态,线程被创建出来但没有被调用 start() 。
  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
  • BLOCKED:阻塞状态,需要等待锁释放。-发生锁竞争-不会释放CPU
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。-显性调用wait-释放CPU
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。-显性调用wait-释放CPU
  • TERMINATED:终止状态,表示该线程已经运行完毕。

生命周期拓扑图
image

sleep() 方法和 wait() 方法对比

两个方法都能让线程暂停运行

  • sleep:
    定义在Thread中,目的是使线程进入等待状态,不会释放锁资源
    通常使用时只是单纯的想让当前线程等待指定的时间,比如当前线程调用的外部接口,而外部接口无法实时返回内容,可以使用sleep循环获取外部接口的执行结果。
  • wait:
    定义在Object对象中,目的是使线程进入等待状态,释放锁资源,等待被其它线程通过Object.notify唤醒
    通常用于多个线程之间的通信,相互协作使用共享资源,比如生产者与消费者对阻塞队列中任务的操作

java内存模型

volatile关键字

volatile关键字声明的遍历可以保证多线程同时操作的可见性,但是不保证原子性(非线程安全)。
volatile修饰的变量会存储在共享内存中,每个线程访问volatile变量时会获取一个内存副本,当修改变量值时会实时同步到共享内存中,而其它线程线程可以从共享内存中获取到最新的修改值。

  • 可见性:每个线程访问volatile变量时都能从内存中获取最新的值
  • 原子性(不能保证):volatile变量无法保证线程在读取变量后,当线程进行写操作时,写入内存的值与自己实际想要写入的值一致(幻读问题)
  • 指令重排:可以避免指令重排,如下
    Singleton =uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:为uniqueInstance分配内存空间,初始化uniqueInstance,将uniqueInstance指向分配的内存地址;但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

ThreadLocal

针对共享变量对多个线程可见时容易出现线程安全问题,Java提供了ThreadLocal,为每个线程申请自己专属的内存空间,线程之间隔离。

  • 使用
public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

        Thread thread1 = new Thread(() -> {
            threadLocal.set(1);
            for (int i = 0; i < 100; i++) {
                threadLocal.set(threadLocal.get() + 1);
            }
            System.out.println("Thread1: " + threadLocal.get());
        });

        Thread thread2 = new Thread(() -> {
            threadLocal.set(1);
            for (int i = 0; i < 100; i++) {
                threadLocal.set(threadLocal.get() + 1);
            }
            System.out.println("Thread2: " + threadLocal.get());
        });

        thread1.start();
        thread2.start();
    }

两个线程的累加操作互不影响

  • 原理
    每个线程维护了自己的ThreadLocalMap内存空间,set值时操作的是自己内存空间的值
public void set(T var1) {
        Thread var2 = Thread.currentThread();
        ThreadLocalMap var3 = this.getMap(var2);
        if (var3 != null) {
            var3.set(this, var1);
        } else {
            this.createMap(var2, var1);
        }
    }
  • ThreadLocal引发内存泄漏
    存储ThreadLocal数据的ThreadLocalMap是一个静态单例对象,map中存储元素的结构如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

可以看出k是弱引用,用于存储线程id,当发生GC时会被回收,而v是强引用,由于引用树的根节点ThreadLocalMap是静态单例的,因此v永远都不会被回收;

  • 解决方案
    当前线程使用完ThreadLocal后需要即使调用remove方法删除内容;

  • 死锁问题
    两个线程A、B同时竞争锁资源1、2,当A获取到1请求锁2,B获取到2等待锁1时,就产生了死锁问题。
  • 解决方案
    死锁产生的条件是
  1. 互斥条件:每个线程可以独占锁资源
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

破坏请求与保持条件:获取锁资源时使用tryLock,获取失败则使用Object.wait()进行阻塞,释放已占有的锁资源
银行家算法:对锁资源进行评估,判断每个线程对锁资源的可达性,然后按照可达顺序获取锁;如上述示例中A、B线程都采用先获取1再获取2的顺序,则不会死锁。

悲观锁

在获取临界资源时直接上锁,排斥其它线程同时获取到临界资源,如synchronized和ReentrantLock都是悲观锁。因为锁的力度比较大,所以容易产生死锁问题和造成大面积阻塞。
悲观锁一般用于写多读少的场景;因为读操作并不会导致线程安全,使用悲观锁会导致没必要的阻塞,同时写的并发量高,使用CAS会频繁导致上锁失败而重试,浪费CPU资源。

乐观锁

在获取临界资源时并不上锁,而是通过版本控制,在更新数据时比较当前数据与被更新数据的版本,如果不一致,则上锁失败。一般采用CAS机制实现。
乐观锁用于读多写少的场景;高并发读没有线程安全,没必要进行阻塞,少量的写也减少CAS失败的频率,上锁的成功率会提高。

乐观锁产生的问题

  • 不恰当的使用临界资源会产生ABA问题
    如需要的基金产品的份额做CAS,当线程A查询到份额为10,在做更新比较前,线程B完成了10->9的更新,之后线程C释放份额完成了9->10的更新,这种情况下线程A再次更新做数据比较时会成功更新份额;出现了似乎没有其它线程变更过的假象。
    解决方案:使用恰当的临界资源做版本控制,如使用version(每次更新+1)或时间戳(每次更新记录当前时间)
  • 大量线程使用CAS时容易造成频率的CAS失败,引发重试,占用了较高的CPU
    解决方案:给重试增加间隔时间,如使用Thread.sleep会释放CPU

synchronized

synchronized是Java提供的同步关键字,属于可重入的排他锁,可以锁方法,也可以锁代码块

  • 锁方法,当在静态方法上加关键字时,锁的是整个class,对非静态方法加锁时锁的是当前实例对象
synchronized void method() {
    //业务代码
}

synchronized static void method() {
    //业务代码
}

在方法上加锁谨慎使用,因为锁的粒度比较大,容易造成非必要的阻塞

  • 代码块加锁,锁住指定的对象
    • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁。
    • synchronized(this) 表示进入同步代码前要获得 当前实例对象的锁
    • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
  • 锁升级
    synchronized具备锁升级的特性,分为无锁、偏向锁、轻量级锁、重量级锁
    • 无锁:无线程访问
    • 偏向锁:当只有一个线程访问该锁时,锁会升级到偏向锁-支持单线程可重入特性。
    • 轻量级锁:当第二个线程尝试获取该锁时,锁会升级到轻量级锁-采用乐观锁(CAS)自选等待机制获取锁资源。
    • 重量级锁:当多个线程竞争该锁时,锁会升级到重量级锁-采用悲观锁,获取临界资源时直接阻塞,等待其它线程释放资源后唤醒。

ReentrantLock

实现了Lock规范的可重入独占锁,默认实例化为非公平锁,可通过构造方法的入参控制是否为非公平锁
构造方法如下:

public ReentrantLock() {
    this.sync = new NonfairSync();
}

public ReentrantLock(boolean var1) {
    this.sync = (Sync)(var1 ? new FairSync() : new NonfairSync());
}
  • 公平锁:ReentrantLock针对与产生阻塞的线程维护了一个链表进行存储,在公平锁的情况下,会按照阻塞顺序依次释放锁资源
  • 非公平锁:当锁资源空闲时,会随机从阻塞链表中获取一个线程执行

方法详解

image
lock: 获取锁资源,获取失败时一直阻塞,即使拥有锁资源的线程发生异常,但没释放锁资源,阻塞的线程也没法获取
tryLock: 尝试获取锁,失败时返回false
lockInterruptibly: 获取中断锁,获取锁的线程可以被中断,不会一直阻塞;如用有锁的线程异常退出没释放锁资源,通过lockInterruptibly获取锁的线程在长期获取不到锁时会被中断,返回异常
注意:Object.wait()和Object.notify()对ReentrantLock是无效的,ReentrantLock可以搭配Condition.await()和Condition.signal()使用

ReentrantReadWriteLock

可重入读写锁,其中区分读锁和写锁,读锁为共享锁,写锁为排他锁
默认为非公平锁

  • 使用示例
public class ReentrantReadWriteLockExample {
    private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private static final Object data = new Object();
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            // 读操作
            lock.readLock().lock();
            try {
                System.out.println("Thread1 reads data: " + data);
            } finally {
                lock.readLock().unlock();
            }
        });

        Thread thread2 = new Thread(() -> {
            // 写操作
            lock.writeLock().lock();
            try {
                System.out.println("Thread2 writes data");
                data = new Object();
            } finally {
                lock.writeLock().unlock();
            }
        });

        thread1.start();
        thread2.start();
    }
}

lock.readLock().lock():获取读锁资源,产生共享锁,其它线程也可通过读锁获取资源,但是排斥写锁
lock.writeLock().lock():获取写锁资源,产生排他锁,其它线程的读锁与写锁都会造成阻塞

  • 原理解析
    ReentrantReadWriteLock中包含了ReadLock和WriteLock两个实例,而ReadLock和WriteLock均实现了Lock结构,各具有锁的特性。

StampedLock

在读多写少的情况下使用ReentrantReadWriteLock容易导致写锁长时间无法获取到锁而导致饥饿,StampedLock是对读写锁的扩展,提供了读锁、写锁和乐观读锁。StampedLock是基于CLH 锁实现。

  • 写锁:独占锁,当有线程持有时其它线程阻塞
  • 读锁:共享锁,当有线程持有时,其它线程访问读锁可用
  • 乐观读锁:共享锁,当有线程持有时,其它线程可以访问读锁与写锁。

线程池

针对需要使用多线程分批处理业务的场景,线程池提供了一套线程管理机制,有效的管理线程创建、运行、消费过程;线程池提供了核心工作线程和非核心线程,核心线程创建后不会被回收,一直等待新的任务进入,然后直接消费,节省了线程创建的开销,非核心线程是为了应对大量任务进入,核心线程不够用的场景;同时提供了阻塞队列统一管理存储待消费任务。

Executor框架

Executor是线程池模块的顶层接口,定义了线程池包含的线程工厂、队列以及拒绝策略等功能。
Executor 框架结构主要由三大部分组成:任务、任务执行、任务结果获取。

  • 任务
    所有任务执行的代码都基于Runnable或者Callable实现,其中Callable可以获取执行结果;实际上Callable方式最终也是依赖于Runnable创建线程,Callable只是单纯的提供业务代码执行,最终需要使用FutureTask进行包装,而FutureTask继承了Runnable,FutureTask作为线程的实际执行者以及结果反馈者。
  • 任务执行
    Executor提供了两类执行方式submit、execute;
    • submit:会返回FutureTask进行阻塞获取返回结果(submit也支持传入参数,该参数为指定的返回结果)
    • execute:线程池不会通过该方法返回结果,不过业务代码中可以使用FutureTask作为执行任务传入execute,并通过FutureTask获取返回结果
  • 结果获取
    通过FutureTask.get()可以获取线程执行结果,获取过程会阻塞主线程。

ThreadPoolExecutor

线程池实现类 ThreadPoolExecutor 是 Executor 框架最核心的类,是使用最频繁的线程池实体。

核心参数

    /**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> 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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

参数说明
* corePoolSize:核心线程数,创建后不会被销毁,持续消费队列中的任务
* maximumPoolSize:最大线程数,当核心线程数量已满,队列中进入了新的任务时会创建非核心线程,直到达到最大线程数时终止创建,当队列中任务逐渐减少,会一次销毁非核心线程。
* keepAliveTime:指定多余的非核心线程存活的最长时间
* unit:指定时间单位
* workQueue:任务队列
* threadFactory:线程工厂,用于创建线程
* handler:拒绝策略,当阻塞队列已满,或线程池状态为shutdown时,新进入的任务会被拒绝
* allowCoreThreadTimeOut:是否消费核心线程-此参数非默认可设置,默认不可销毁
参数设置参考方式
* corePoolSize:根据任务类型设置,CPU密集型设置为2N,IO密集型设置为N+1,N为CPU内核数;线程数量与内核数量挂钩,是因为太多的线程数会导致CPU频繁切换上下文,降低CPU处理效率,过少的线程无法完整的利用CPU。
* 阻塞队列长度:根据实际允许的等待时间与单个任务处理的时间做除法;如单个任务单线程执行时间为100ms,假设已确定使用4个线程,则可以粗略估算单个任务处理时间为25ms=100ms/4,若期望等待执行时间为10s,则队列长度为400=10s/25ms。
以上只是理论算法,可以上述的理论计算作为压测基础值,根据压测情况做动态调整;如CPU过高则降低线程数,如CPU较低且内存使用率也不高,则可以增大线程数。

  • 拒绝策略定义
    • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
    • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
    • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
    • ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。
      当添加任务被拒绝,但是任务又必须正常执行的处理方案:
  1. 根据现有的多线程使用情况做参数调整,增加线程数或增加队列长度,保证所有任务都能被执行
  2. 自定义拒绝策略,在任务被拒绝后尝试重新将任务加入到队列中,或开启新的线程池执行任务

线程池创建方式

  • 方式一(推荐):通过ThreadPoolExecutor构造函数来创建
  • 方式二:通过 Executor 框架的工具类 Executors 来创建。
    Executors提供了FixedThreadPool、SingleThreadExecutor、CachedThreadPool、ScheduledThreadPool这几个常用线程池
    • FixedThreadPool:固定线程数量的线程池,指定核心线程数,不使用非核心线程
    • SingleThreadExecutor:单线程的线程池,核心线程只有一个,不使用非核心线程
    • CachedThreadPool:不固定线程数量,所有线程都为非核心线程,当队列中存在任务但是没有空闲线程时就会创建新线程,线程超时空闲就会被销毁;实例化时制定核心线程数为0,最大线程数为Integer.MAX_VALUE
    • ScheduledThreadPool:延时执行线程池,新增的任务需要基于RunnableScheduledFuture实现,提供一个延时时间,任务会在指定的延时时间之后执行。延时线程池的延时特性来源于延时线程池使用了延时队列DelayedWorkQueue,而延时任务RunnableScheduledFuture也是继承了Delayed。

线程池使用的阻塞队列

不同的线程池会选用不同的阻塞队列,我们可以结合内置线程池来分析。

  • 容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue(无界队列):FixedThreadPool 和 SingleThreadExector 。FixedThreadPool最多只能创建核心线程数的线程(核心线程数和最大线程数相等),SingleThreadExector只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。
  • SynchronousQueue(同步队列):CachedThreadPool 。SynchronousQueue 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,CachedThreadPool 的最大线程数是 Integer.MAX_VALUE ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。
  • DelayedWorkQueue(延迟阻塞队列):ScheduledThreadPool 和 SingleThreadScheduledExecutor 。DelayedWorkQueue 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 Integer.MAX_VALUE,所以最多只能创建核心线程数的线程。
    由上可知Executors提供的几种线程池均没有约束阻塞队列数量或线程数量,因此如果有大量的任务需要操作时容易导致OOM

线程池原理分析

  • 任务提交源码
   // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
   private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

    private static int workerCountOf(int c) {
        return c & CAPACITY;
    }
    //任务队列
    private final BlockingQueue<Runnable> workQueue;

    public void execute(Runnable command) {
        // 如果任务为null,则抛出异常。
        if (command == null)
            throw new NullPointerException();
        // ctl 中保存的线程池当前的一些状态信息
        int c = ctl.get();

        //  下面会涉及到 3 步 操作
        // 1.首先判断当前线程池中执行的任务数量是否小于 corePoolSize
        // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 2.如果当前执行的任务数量大于等于 corePoolSize 的时候就会走到这里,表明创建新的线程失败。
        // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态并且队列可以加入任务,该任务才会被加入进去
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
            if (!isRunning(recheck) && remove(command))
                reject(command);
                // 如果当前工作线程数量为0,新创建一个线程并执行。
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
        // 传入 false 代表增加线程时判断当前线程数是否少于 maxPoolSize
        //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
        else if (!addWorker(command, false))
            reject(command);
    }
  1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
  2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
  3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
  4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用RejectedExecutionHandler.rejectedExecution()方法。
    image

AQS详解

AQS 的全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器。
源码解读

作用

多线程并发场景下,当临界资源处于阻塞状态时(仅限Lock锁相关的实现),会出现阻塞线程,AQS的作用就是用于存储管理这些阻塞线程,控制线程之间的阻塞与唤醒。AQS是基于CHL队列,提供一个双向链表用于存储所有的阻塞线程。
队列结构图
image

  • 锁状态控制
    AQS 使用 int 成员变量 state 表示同步状态,通过内置的 FIFO 线程等待/等待队列 来完成获取资源线程的排队工作。当state状态为0时,表示当前锁资源空闲,state可以累加-可重入锁的实现原理。
  • AQS 资源共享方式
    AQS 定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

基于AQS的同步工具

  • CountDownLatch(计数器)
    CountDownLatchch初始化时给AQS的state制定一个数量,当每个线程执行完时对CountDownLatch进行countDown;最终可以通过getCount获取当前state数量
  • Semaphore(信号量)
    Semaphore初始化时给AQS的state制定一个数量,提供了acquire和release方法分别对state做加一减一操作。
posted @ 2024-03-18 22:30  周仙僧  阅读(13)  评论(0编辑  收藏  举报