【Java 线程池 概念+深析】简单理解

加油,每天一篇博客,听一遍好运来


在这里插入图片描述

1.简介

线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的。在jdk1.5之后这一情况有了很大的改观。Jdk1.5之后加入了java.util.concurrent包,这个包中主要介绍java中线程以及线程池的使用。为我们在开发中处理线程的问题提供了非常大的帮助。

2.线程池
2.1 线程池的作用

线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

2.2 为什么要用线程池

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

3.线程池的创建
3.1 线程池实例

线程池的最上层接口是Executor,这个接口定义了一个核心方法execute(Runnablecommand),这个方法是用来传入任务的,最后被ThreadPoolExecutor类实现。而且ThreadPoolExecutor是线程池的核心类

我来写一段代码给大家先熟悉熟悉线程池:

class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"正在执行....");
    }
}
public class singleThreadExecutorTest{
    public static void main(String[] args) {
        //阻塞队列
        LinkedBlockingDeque queue = new LinkedBlockingDeque(3);
        ThreadPoolExecutor pool = new ThreadPoolExecutor(1,2,3, TimeUnit.SECONDS, queue);

        //执行第一个任务
        pool.execute(new MyThread());

        //队列有三个任务等待
        pool.execute(new MyThread());
        pool.execute(new MyThread());
        pool.execute(new MyThread());

        //执行第五个任务
        pool.execute(new MyThread());
        
        //执行第六个任务
        // pool.execute(new MyThread()); 会出现什么问题?
        pool.shutdown();
    }
}

上述:ThreadPoolExecutor(…)里面的参数都是什么意思,我们看看源码:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                               long keepAliveTime,
                               TimeUnit unit,
                               BlockingQueue<Runnable> workQueue,
                               RejectedExecutionHandler handler) 

corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数量
keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
unit:存活时间的单位
workQueue:存放任务的队列
handler:超出线程范围和队列容量的任务的处理程序

图解分析:
在这里插入图片描述
过程:当我们启动第一个任务,会先判断核心线程是否满了,没满就创建核心线程执行这个任务,满了就把任务添加到任务队列中,当任务队列也满的时候,则创建临时线程执行任务,当我们的线程数量达到最大值的时候,会执行拒绝策略,说白了就是:我没多了线程执行你新增的任务了,我不干了!

比较重要的几个类:

ExecutorService真正的线程池接口。
ScheduledExecutorService能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutorExecutorService的默认实现。
ScheduledThreadPoolExecutor继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

在Executors类里面提供了一些静态工厂,生成一些常用的线程池:

3.1 四种线程池的使用
  1. newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

public class singleThreadExecutorTest {
    public static void main(String[] args) {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            singleThreadExecutor.execute(new Runnable() {
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName()+" "+index);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

2.newFixedThreadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

public class newFixedThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int index = i;
            fixedThreadPool.execute(new Runnable() {
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName()+" "+index);
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

3.newCachedThreadPool

1.一个在需要处理任务时才会创建线程的线程池,如果一个线程处理完任务了还没有被回收,那么线程可以被重复使用。
2.当我们调用execute方法时,如果之前创建的线程有空闲可用的,则会复用之前创建好的线程,否则就会创建新的线程加入到线程池中。
3.创建好的线程如果在60s内没被使用,那么线程就会被终止并移出缓存。因此,这种线程池可以保持长时间空闲状态而不会消耗任何资源。

方式1:我们在在执行execute之前休眠一段时间,部分线程可以被复用

public class Test {
    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cachedThreadPool.execute(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName()+" "+index);
                }
            });
        }
        cachedThreadPool.shutdown();
    }
}

方式2:不加休眠时间

public class Test {
    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            cachedThreadPool.execute(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName()+" "+index);
                }
            });
        }
        cachedThreadPool.shutdown();
    }
}

大家可以运行上述代码,我们在execute之前休眠一段时间目的是:我们加入任务的时候,可能上一个任务结束,线程还未被回收,那我们就可以重复利用这个线程
4.newScheduledThreadPool

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

public class Test1 {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        for (int i = 0; i < 15; i++) {
            scheduledThreadPool.execute(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
        scheduledThreadPool.shutdown();
    }
}

3.2 线程池实现原理

在这里插入图片描述
1、原理:
提交一个任务到线程池中,线程池的处理流程如下:
1、判断**线程池里的核心线程**是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。

2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

3、判断**线程池里的线程**是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务

4.ThreadPoolExecutor详解

1、ThreadPoolExecutor的execute()方法
在这里插入图片描述
2.addWork方法

 private boolean addWorker(Runnable firstTask, boolean core) {
  //第一段逻辑:线程数+1
 retry:
 for (;;) {
            int c = ctl.get();//获取线程池容量
            int rs = runStateOf(c);//获取状态
​
            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&//即:SHUTDOWN,STOP,TIDYING,TERMINATED
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))//即:rs==RUNNING,firstTask!=null,queue==null
                return false;//如果已经关闭,不接受任务;如果正在运行,且queue为null,也返回false
            for (;;) {
                int wc = workerCountOf(c);//获取当前的工作线程数
                //如果工作线程数大于等于容量或者大于等于核心线程数(最大线程数),那么就不能再添加worker
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))//cas增加线程数,失败则再次自旋尝试
                    break retry;
                c = ctl.get();  // Re-read ctl //再次获取工作线程数
                if (runStateOf(c) != rs)//不相等说明线程池的状态发生了变化,继续自旋尝试
                    continue retry;
  }
  }
​
 //第二段逻辑:将线程构造成Worker对象,并添加到线程池
 boolean workerStarted = false;//工作线程是否启动成功
 boolean workerAdded = false;//工作线程是否添加成功
 Worker w = null;
 try {
            w = new Worker(firstTask);//构建一个worker
            final Thread t = w.thread;//去除worker中的线程
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;//获取重入锁
                mainLock.lock();//上锁
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());//获得锁之后,再次检查状态
​
                    //只有当前线程池是正在运行状态,[或是 SHUTDOWN 且 firstTask 为空],才能添加到 workers 集合中
                    if (rs < SHUTDOWN ||
                       (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);//将新创建的 Worker 添加到 workers 集合中
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;//更新线程池中线程的数量
                        workerAdded = true;//添加线程(worker)成功
                   }
               } finally {
                    mainLock.unlock();
               }
                if (workerAdded) {
                    t.start();//这里就会去执行Worker中的run()方法
                    workerStarted = true;//启动成功
               }
  }
  } finally {
            if (! workerStarted)
                addWorkerFailed(w);//如果启动线程失败,需要回滚
  }
 return workerStarted;
  }

这个方法主要就是做两件事:将线程数+1、将线程构造成Worker对象,加入到线程池中,并调用start()方法启动线程

3、Worker对象

 private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        /**
         * This class will never be serialized, but we provide a
         * serialVersionUID to suppress a javac warning.
         */
        private static final long serialVersionUID = 6138294804551838833L;

        /** Thread this worker is running in.  Null if factory fails. */
        final Thread thread;//真正执行的task
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;//需要执行的task
        /** Per-thread task counter */

上面这个方法继承了AbstractQueuedSynchronizer,前面我们讲述AQS同步队列的时候知道,AQS就是一个同步器

Worker中state初始化状态设置为-1,原因是在初始化Worker对象的时候,在线程真正执行runWorker()方法之前,不能被中断。而一旦线程构造完毕并开始执行任务的时候,是允许被中断的,所以在线程进入runWorker()之后的第一件事就是将state设置为0(无锁状态),也就是允许被中断。

我们再看看Worker的构造器:
在这里插入图片描述
addWork方法执行到这句:w = new Worker(firstTask);//构建一个worker 的时候,就会调用构造器创建一个Worker对象,state=-1,并且将当前任务作为firstTask,后面再运行的时候会优先执行firstTask。
上面addWorker方法在worker构造成功之后,就会调用worker.start方法,这时候就会去执行Worker中的run()方法,这也是一种委派的方式

run()方法中调用了runWorker(this)方法,这个方法就是真正执行任务的方法:

4、runWorker(this)

final void runWorker(Worker w) {
 Thread wt = Thread.currentThread();
 Runnable task = w.firstTask;
 w.firstTask = null;
 /**
  * 表示当前worker线程允许中断,因为new Worker默认的 state=-1,此处是调用
  * Worker类的 tryRelease()方法,state置为 0,
  * 而 interruptIfStarted()中只有 state>=0 才允许调用中断
  */
 w.unlock(); // allow interrupts
 boolean completedAbruptly = true;
 try {
            while (task != null || (task = getTask()) != null) {
                /**
                 * 加锁,这里加锁不仅仅是为了防止并发,更是为了当调用shutDown()方法的时候线程不被中断,
                 * 因为shutDown()的时候在中断线程之前会调用tryLock方法尝试获取锁,获取锁成功才会中断
                 */
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                //ifnot, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                /**
                 * 如果是以下两种情况,需要中断线程
                 * 1.如果state>=STOP,且线程中断标记为false
                 * 2.如果state<STOP,获取中断标记并复位,如果线程被中断,那么,再次判断state是否STOP
                 *   如果是的话,且线程中断标记为false
                 */
                if ((runStateAtLeast(ctl.get(), STOP) ||//状态>=STOP
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();//中断线程
                try {
                    beforeExecute(wt, task);//空方法,我们可以重写它,在执行任务前做点事情,常用于线程池运行的监控和统计
                    Throwable thrown = null;
                    try {
                        task.run();//正式调用run()执行任务
                   } catch (RuntimeException x) {
                        thrown = x; throw x;
                   } catch (Error x) {
                        thrown = x; throw x;
                   } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                   } finally {
                        afterExecute(task, thrown);//执行任务之后调用,也是个空方法,我们可以重写它,在执行任务后做点事情,常用于线程池运行的监控和统计
                   }
               } finally {
                    task = null;//将任务设置为空,那么下次循环就会通过getTask()方法从workerQueue中取任务了
                    w.completedTasks++;//任务完成数+1
                    w.unlock();
               }
  }
            completedAbruptly = false;
  } finally {
            //核心线程会阻塞在getTask()方法中等待线程,除非设置了允许核心线程被销毁,
            // 否则正常的情况下只有非核心线程才会执行这里
            processWorkerExit(w, completedAbruptly);//销毁线程
  }
  }

主要执行步骤为:

1、首先释放锁,因为进入这个方法之后线程允许被中断
2、首先看看传入的firstTask是否为空,不为空则优先执行
3、如果firstTask为空(执行完了),则尝试从getTask()中获取任务,getTask()就是从队列l里面获取任务
4、如果获取到任务则开始执行,执行的时候需要重新上锁,因为执行任务期间也不允许中断
5、任务运行前后分别有一个空方法,我们可以在有需要的时候重写这两个方法,实现付线程池的监控
6、如果获取不到任务,则会执行processWorkerExit方法销毁线程

6、getTask()方法

privateRunnablegetTask(){
 //上一次获取任务是否超时,第一次进来默认false,第一次自旋后如果超时就会设置为true,则第二次自旋就会返回null
 booleantimedOut=false;//Didthelastpoll()timeout?
​
 for(;;){
            intc=ctl.get();
            intrs=runStateOf(c);
​
            //Checkifqueueemptyonlyifnecessary.
            /**
             *1.线程池状态为shutdown,那么就必须要等到workQueue为空才行,因为shutdown()状态是需要执行队列中剩余任务的
             *2.线程池状态为stop,那么就不需要关注workQueue中是否有任务
             */
            if(rs>=SHUTDOWN&&(rs>=STOP||workQueue.isEmpty())){
                decrementWorkerCount();//线程池中的线程数-1
                returnnull;//返回null的话,那么runWorker方法中就会跳出循环,执行finally中的processWorkerExit方法销毁线程
   }
​
            intwc=workerCountOf(c);
            //Areworkerssubjecttoculling?
            //1.allowCoreThreadTimeOut-默认false,表示核心线程数不会超时
            //2.如果总线程数大于核心线程数,那就说明需要有线程被销毁
            booleantimed=allowCoreThreadTimeOut||wc>corePoolSize;
​
            /**
             *1.线程数量超过maximumPoolSize可能是线程池在运行时被调用了setMaximumPoolSize()
             *被改变了大小,否则已经addWorker()成功的话是不会超过maximumPoolSize。
             *2.timed&&timedOut如果为true,表示当前操作需要进行超时控制,并且上次从阻塞队列中
             *获取任务发生了超时.其实就是体现了空闲线程的存活时间
             */
            if((wc>maximumPoolSize||(timed&&timedOut))
                &&(wc>1||workQueue.isEmpty())){
                if(compareAndDecrementWorkerCount(c))
                    returnnull;
                continue;
   }
​
            try{
                Runnabler=timed?
                    workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)://等待指定时间后返回
                    workQueue.take();//拿不到任务会一直阻塞(如核心线程)
                if(r!=null)
                    returnr;//如果拿到任务了,返回给worker进行处理
                timedOut=true;//走到这里就说明到了超期时间还没拿到任务,设置为true,第二次自旋就可以直接返回null
   }catch(InterruptedExceptionretry){
                timedOut=false;
   }
   }
   }

1、首先判断状态是不是对的,如果是SHUTDOWN之类不符合要求的状态,那就直接返回null,并把线程数-1,而返回null之后前面的方法就会跳出while循环,执行销毁线程流程。
2、判断下是不是有设置超时时间或者最大线程数超过了核心线程数
3、根据上面的判断决定是执行带有超时时间的poll方法还是take方法从队列中获取元素。 情况一:如果是执行带超时时间的poll方法,那么时间到了如果还没取到元素,那么就返回空,这种情况说明当前系统并不繁忙,所以返回null之后线程就会被销毁; 情况二:如果是执行take方法,根据第2点的判断知道,除非我们人为设置了核心线程可以被回收,否则核心线程就是会执行take方法,如果获取不到任务就会一直阻塞等待获取到任务为止。

7、processWorkerExit方法
这是销毁线程的方法,上面的getTask()方法返回空,就会执行线程销毁方法,因为getTask()当中已经把线程数-1了,所以这里可以直接执行线程销毁工作。
在这里插入图片描述
直接调用的是workers集合的remove()方法,后面还有就是尝试中止和一些异常异常情况的补偿操作。

RejectedExecutionHandler:饱和策略
当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中策略:

1、AbortPolicy:直接抛出异常
2、CallerRunsPolicy:只用调用所在的线程运行任务
3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
4、DiscardPolicy:不处理,丢弃掉。

5.面试题

问题一
Q:为什么不建议直接使用Executors来构建线程池?

A:用Executors
使得我们不用关心线程池的参数含义,这样可能会导致问题,比如我们用newFixdThreadPool或者newSingleThreadPool.允许的队列长度为Integer.MAX_VALUE,如果使用不当会导致大量请求堆积到队列中导致OOM的风险而newCachedThreadPool,允许创建线程数量为
Integer.MAX_VALUE,也可能会导致大量
线程的创建出现CPU使用过高或者OOM的问题。而如果我们通过ThreadPoolExecutor来构造线程池的话,我们势必要了解线程池构造中每个
参数的具体含义,会更加谨慎。

问题二
Q:如何合理配置线程池的大小?

A:要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析: 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
任务的优先级:高、中和低。 任务的执行时间:长、中和短。 任务的依赖性:是否依赖其他系统资源,如数据库连接。 CPU密集型:
CPU密集型的特点是响应时间很快,cpu一直在运行,这种任务cpu
的利用率很高,那么线程数的配置应该根据CPU核心数来决定,CPU核心数=最大同时执行线程数,假如CPU核心数为4,那么服务器最多能同时执行4个线程。过多的线程会导致上
下文切换反而使得效率降低。那线程池的最大线程数可以配置为cpu核心数+1。

IO密集型: 主要是进行IO操作,执行IO操作的时间较长,这是cpu会处于空闲状态,
导致cpu的利用率不高,这种情况下可以增加线程池的大小。可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置cpu核心数的2倍。
一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/ 线程CPU时间 )* CPU数目

附:获取CPU个数方法:Runtime.getRuntime().availableProcessors()

问题三
Q:线程池中的核心线程什么时候会初始化?

A:默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
在实际中如果需要线程池创建之后立即创建线程,可以通过如下两个方法:

prestartCoreThread():初始化一个核心线程。 prestartAllCoreThreads():初始化所有核心线程

问题四
Q:线程池被关闭时,如果还有任务在执行,怎么办?

A:线程池的关闭有两个方法:

shutdown() 不会立即终止线程池,要等所有任务缓存队列中的任务都执行完后才终止,但是不会接受新的任务 shutdownNow()
立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务任务

问题五
Q:线程池容量是否可以动态调整?

A:可以通过两个方法动态调整线程池的大小。

setCorePoolSize():设置最大核心线程数 setMaximumPoolSize():设置最大工作线程数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值