java线程池--初步了解线程池

当时觉得自己菜,写的没深度,放弃了。这次希望自己坚持下去,加油

博客主页:知识积累

个人网站:个人网站

今天我们简单来学习一下java线程池的基本知识,了解一个简单的运行流程。

线程池的流程图

流程图

大概就是图中这几步。

jdk版本为1.8 

注意看类的包路径,我一开始就看错了类,看到了tomcat里面的线程池去了,而且类名什么的还都一样,实现是不一样的。

java的线程池的类都在 package java.util.concurrent 包下,注意别看错了!

1 创建线程池

1.1 了解参数
下面是一个创建线程池的代码
ExecutorService exec = new ThreadPoolExecutor(20, 100,1, TimeUnit.MINUTES,new ArrayBlockingQueue(100));

简单要了解一下线程池到底有哪些参数
下面是线程池所有入参的构造函数,我们来一一说明每个参数的含义

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {....}

corePoolSize:核心线程数量。核心线程即使空闲也不会被回收。
maximumPoolSize:最大线程数。即线程池中最多,只能存放这么多数量的线程,所以核心线程数量+其他线程数量 < 最大线程数量
keepAliveTime:线程空闲时存活的时间。一旦非核心线程出现空闲,再指定时间内还是没有被使用,就会被回收。
unit:上述线程存活时间的单位。
workQueue:阻塞队列。提交任务时,所有的核心线程被其他任务占用,没有空闲的核心形成,那么会将这个任务存放到阻塞队列当中。直到有空闲的核心线程来执行阻塞队列中的任务。
threadFactory:创建线程池中线程对象的工厂。
handler:拒绝策略。 当提交任务时,此时核心线程都在被其他任务占用,阻塞队列的也被提交是任务占满,而此时创建的其他线程也达到了总线程数的上线,那么就会将该任务交给拒绝策略来处理。

简单了解几个常用的阻塞队列
ArrayBlockingQueue
:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue,静态工厂方法 Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

简单了解几个常用的拒绝策略
AbortPolicy
:不处理,直接抛出异常。
CallerRunsPolicy:若线程池还没关闭,调用当前所在线程来运行任务,r.run()执行。
DiscardOldestPolicy:LRU策略,丢弃队列里最近最久不使用的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉,不抛出异常。

线程池也是有状态的,线程池很多操作都是根据对线程池状态和数量执行CAS操作,来保证线性安全。
线程池状态是一个AtomicInteger类型的整数,用高3位表示线程池的运行状态,低29位表示线程池的线程数量。
简单介绍一下状态
RUNNING:高3位为111,运行中,该状态的线程池会接收新任务,并处理阻塞队列中的任务;
SHUTDOWN:高3位为000,关闭,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
STOP :高3位为001,停止,该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
TIDYING :高3位为010,整理, 所有的任务都已经终止;
TERMINATED:高3位为011,终止, terminated()方法已经执行完成

1.2 具体创建流程

1.2.1 简单介绍几个类中的重要参数
ctl:控制器

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

这个整形变量的高3位表示线程池的状态,低29位表示线程池中线程的数量。通过cas对该值的修改,来实现并发时的线性安全。

ftask :可以获得返回值的任务
RunnableFuture<Void> ftask = newTaskFor(task, null);

我们往线程池提交任务的时,会先将任务转成RunnableFuture,然后才调用添加任务的接口

workers :线程池中工作线程的容器
private final HashSet<Worker> workers = new HashSet<Worker>();

Worker:线程池中创建的线程最终都会封装成worker,我们通过worker去执行我们提交的任务。
该类继承了同步器AbstractQueuedSynchronizer和继承了线程类Runnable

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{
    //序列化id
    private static final long serialVersionUID = 6138294804551838833L;

    //持有的线程
    final Thread thread;
    //线程的任务
    Runnable firstTask;
    //线程的计数器,统计该worker总共完成了多少个任务
    volatile long completedTasks;

    //构造
    Worker(Runnable firstTask) {
        //同步状态设置为-1
        setState(-1); 
        this.firstTask = firstTask;
        //用创建线程池传入的线程工厂创建线程
        this.thread = getThreadFactory().newThread(this);
    }

    //运行worker
    public void run() {
        //运行worker
        runWorker(this);
    }
    
    /*------------------------------下面都是重写同步器的方法-----------------------------------------*/
    //判断同步状态是否不是0
    protected boolean isHeldExclusively() {
        return getState() != 0;
    }
    //试图获取锁
    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            //上面cas执行成功,说明该线程已经获得了这个同步状态,那么将同步器的exclusiveOwnerThread设置为当前线程。
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
    //试图释放锁
    protected boolean tryRelease(int unused) {
        //释放锁,说明没有线程获得同步状态,那么就将同步器的exclusiveOwnerThread设置为null
        setExclusiveOwnerThread(null);
        //同步状态设置为0
        setState(0);
        return true;
    }

    public void lock()        { acquire(1); }
    public boolean tryLock()  { return tryAcquire(1); }
    public void unlock()      { release(1); }
    public boolean isLocked() { return isHeldExclusively(); }
    //如果线程池启动时被中断,执行下面代码
    void interruptIfStarted() {
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
            try {
                t.interrupt();
            } catch (SecurityException ignore) {
            }
        }
    }
}


1.2.2 查看具体代码流程
创建线程池

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    //调用最终构造函数
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

由上可知,如果不传拒绝策略,默认使用了AbortPolicy,即什么都不做

private static final RejectedExecutionHandler defaultHandler =
    new AbortPolicy();


接下来简单看看 最终的构造函数干了什么

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();
    //启用Java安全管理器,获取系统安全接口,防止恶意代码的运行。
    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;
}

由上可知,就是给各种全局变量赋值。

2 往线程池中添加任务
线程池有两个方法可以提交任务,execute()和submit():
区别在于,submit是可以接受返回值,而execute不可以。

//创建线程池
ExecutorService executorService= new ThreadPoolExecutor(20, 100,
        1, TimeUnit.MINUTES,
        new ArrayBlockingQueue(100));
//往线程池中添加任务
executorService.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("1线程名:" + Thread.currentThread().getName());
    }
});
executorService.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("2线程名:" + Thread.currentThread().getName());
    }
});


2.1 看submit和execute的区别
submit的底层调用的还是execute,在调用execute之前,将传入的thread转成了RunnableFuture

AbstractExecutorService.submit():

public Future<?> submit(Runnable task) {
    //添加的任务不能为空
    if (task == null) throw new NullPointerException();
    //创建一个RunnableFuture,RunnableFuture这个类继承了Runnable和Future<V>,可以通过这个对象获取任何运行成功的返回值
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    //往线程池中添加封装后的ftask
    execute(ftask);
    return ftask;
}

在提交任务的时候,将我们提交的任务封装成一个RunnableFuture
我们先来看看RunnableFuture到底是个什么东西
RunnableFuture

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

RunnableFuture基础了Runnable和Future,那么他本身可以作为线程,Future允许方法获得该异步线程执行后的结果。
为什么要使用RunnableFuture,因为线程池也可以通过该类中,使用的是RunnableFuture的子类FutureTask

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
}

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    //我们提交任务
    this.callable = callable;
    this.state = NEW;    
}


2.2 接下来看看executre方法。
ThreadPoolExecutor

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    //ctl就是那个高三位表示线程池状态,低29位表示线程池数量的那个ctl控制值
    int c = ctl.get();
    //1 如果当前线程数量小于核心线程数量,那么去创建核心线程来执行该任务
    if (workerCountOf(c) < corePoolSize) {
        //讲该任务添加到线程池中,并创建一个核心线程去执行他。
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //2 核心线程已经满了,那么添加到阻塞队列中
    //判断状态是否是运行中,且添加元素到阻塞队列是否成功
    if (isRunning(c) && workQueue.offer(command)) {
        //再次获取ctl的值
        int recheck = ctl.get();
        //如果此时状态不是RUNNING,
        if (! isRunning(recheck) && remove(command))
            //回退操作
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //3 如果阻塞队列也满了,那么就创建非核心线程来执行该任务
    else if (!addWorker(command, false))
        //回退操作
        reject(command);
}

由上可知,线程池添加任务的逻辑大概就是三步

 

 

1 如果当前线程数量小于核心线程数量,那么去创建核心线程来执行该任务
2 核心线程已经满了,那么添加到阻塞队列中
3 如果阻塞队列也满了,那么就创建非核心线程来执行该任务
下面一个个来分析2.2.1  如果当前线程数量小于核心线程数量,那么去创建核心线程来执行该任务,addWorker()
ThreadPoolExecutor

private boolean addWorker(Runnable firstTask, boolean core) {
    //1 通过死循环,用cas给线程池的线程数量+1,从而实现乐观锁,从而实现并发安全。
    //标志位,用于循环中的灵活跳转
    retry:
    for (;;) {
        //获取中央控制值
        int c = ctl.get();
        //获取此时线程池的状态
        int rs = runStateOf(c);
        //如果线程池的状态不是运行中,
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            //获取此时线程池拥有线程的数量
            int wc = workerCountOf(c);
            //如果线程数量大于最大容量,直接返回失败
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //通过cas给线程数量加1,成功就退出标志位retry所关联的循环
            if (compareAndIncrementWorkerCount(c))
                break retry;
            //cas失败,那么就获取此时线程池的ctl的值
            c = ctl.get();  // Re-read ctl
            //获取此时ctl的线程池状态,并且和方法初获取的线程池状态做比较。如果一样,继续在子循环中执行cas,如果不相等,回到retry关联的最外层循环,重新开始执行。
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
    //2 根据线程工厂,往线程池中添加一个核心线程对象
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        //新建一个worker
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            //获得锁,是个全局变量且final修饰的ReentrantLock。
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                //获得此时线程池的状态
                int rs = runStateOf(ctl.get());
                //如果线程池的状态是小于SHUTDOWN即RUNNING,或者线程池的状态是关闭且任务是null
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    //线程是否是存活状态,是的话说明该worker已经被其他任务占用,抛出异常
                    if (t.isAlive()) 
                        throw new IllegalThreadStateException();
                    //将新建的worker添加到workers中
                    workers.add(w);
                    //获取worder的数量
                    int s = workers.size();
                    //将largestPoolSize 设置为当前worker的数量
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    //设置标识,worker添加成功    
                    workerAdded = true;
                }
            } finally {
                //解锁
                mainLock.unlock();
            }
            if (workerAdded) {
                //运行worker的线程
                //这里要注意,t并不是简单的thread,而是上面所说被包装的RunnableFuture的子类FutureTask
                t.start();
                //设置标识,worker开启成功
                workerStarted = true;
            }
        }
    } finally {
        //如果开始失败,执行一些补偿操作
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}


2.2.2 核心线程已经满了,那么添加到阻塞队列中

//主要是offer(command),将任务添加到结点中
if (isRunning(c) && workQueue.offer(command)) {
        //再次获取ctl的值
        int recheck = ctl.get();
        //如果此时状态不是RUNNING,则移除该command
        if (! isRunning(recheck) && remove(command))
            //回退操作
            reject(command);
         //此时线程数是0
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }

这里主要观察阻塞队列的逻辑,具体如何消费阻塞队列的任务,不同的阻塞队列有不同的逻辑,我们用ArrayBlockQueue来为例子。
ArrayBlockQueue底层是基于数组实现的。
2.2.2.1 ArrayBlockQueue的初始化

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    //创建指定长度的数组
    this.items = new Object[capacity];
    //创建一个非公平锁
    lock = new ReentrantLock(fair);
    //创建condition,用于获取
    notEmpty = lock.newCondition();
    //创建condition,用于释放
    notFull =  lock.newCondition();
}

2.2.2.2 给队列添加数据

public boolean offer(E e) {
    //校验,非空
    checkNotNull(e);
    //获取lock
    final ReentrantLock lock = this.lock;
    //操作前加锁
    lock.lock();
    try {
        //如果队列以及满了,返回false
        if (count == items.length)
            return false;
        else {
            //给队列天机数据
            enqueue(e);
            return true;
        }
    } finally {
        lock.unlock();
    }
}



private void enqueue(E x) {
    final Object[] items = this.items;
    //指定数组指定位置,复制x
    items[putIndex] = x;
    //如果此时长度已经满了,将putIndex 设置为0
    if (++putIndex == items.length)
        putIndex = 0;
    /数量加1
    count++;
    //notEmpty唤醒
    notEmpty.signal();
}

疑问:

1 ArrayBlockQueue阻塞队列是如何消费任务的,全程是看到调用了offer方法,看到唤醒了notEmpty,但是全程没看到哪里调用了让notEmpty唤醒的方法。希望有谁知道告知一下。

 

2.2.3 如果阻塞队列也满了,那么就创建非核心线程来执行该任务,如果创建失败,执行拒绝策略

//3 如果阻塞队列也满了,那么就创建非核心线程来执行该任务
    else if (!addWorker(command, false))
        //如果添加非核心线程失败,则执行下面的拒绝策略
        reject(command);


final void reject(Runnable command) {
    //执行拒绝策略
    handler.rejectedExecution(command, this);
}

参考书籍《java并发编程的艺术》

 

 

 

 

 

 

 

 

 

 

坚持一件事情,本来就不简单

©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页