ThreadPoolExecutor-线程池的理解

1.再说线程池之前先了解下线程、用户级线程、内核级线程概念

  • 线程可以通过继承Thread,实现Runnable接口以及实现Callable接口来创建线程,它是比进程更小执行单位。
  • 线程的生命周期和状态
  • ThreadPoolExecutor-线程池的理解_第1张图片
  •  用户级线程:
    • 用户线程指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快,操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。
    • 优点:当有多个处理机也就是多个CPU时,一个进程的多个线程可以同时执行
    • 缺点:线程切换相对于用户级线程来说更为耗时。
  • 内核级线程:
    • 由操作系统内核创建和撤销。内核维护进程及线程的上下文信息以及线程切换。一个内核线程由于I/O操作而阻塞,不会影响其它线程的运行。Windows NT和2000/XP支持内核线程。
    • 优点:线程的调度不需要内核直接参与,控制简单;创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多;允许每个进程定制自己的调度算法,线程管理比较灵活。这就是必须自己写管理程序,与内核线程的区别。
    • 缺点:资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用。基于时间片轮转调度。

2.什么是线程池

  • 它是一种池化技术,我们可以先创建一些线程放在容器中(池中),当任务来的时候,池中的线程就可以直接去处理任务。

3.线程池作用:

  • 避免频繁的创建和销毁线程所造成的开销,节约了系统资源。
  • 提高程序的响应速度。
  • 通过线程池对线程进行统一管理

4.什么时候使用线程池,应用场景?

  • 任务量多,但是单个任务的处理时间较短。

5.常见线程池分类

  • newCachedThreadPool() //可缓存的线程池
    • 没有核心线程,如果任务来了,那么就创建非核心线程去处理任务。如果过了60s以后,还没有任务需要处理,那么非核心线程将会被销毁。适用于大量负载较轻的任务。
    • 由于没有核心线程,允许创建线程最大数为Integer.MAX_VALUE;因此当任务数极其多的情况下,会耗尽CPU和内存的资源。使用SynchronousQueue存储任务。
  • newFixedThreadPool(int n) //固定大小的线程池
    • 线程池最大容量为n,核心线程的数量也为n,只要有任务,那么n个核心线程可以一直复用。任务来了,先判断线程池里面的线程数是否小于核心线程数,如果小于就创建核心线程处理任务。适用于负载较重的服务器。
    • 底层使用LinkedBlockingQueue(它是一个无界队列)来存储任务,当工作线程数大于核心线程数以后,新来的任务会加入LinkedBlockingQueue,从而有可能造成OOM
  • newSingleThreadExecutor() //单个线程的线程池
    • 核心线程数和线程池容量都为1,也就是一次只能处理一个任务。每次都是去阻塞队列里面取任务有序的处理任务。
    • 底层也是用LinkedBlockingQueue存储任务,有可能造成OOM。
  • newScheduledThreadPool(int corePoolSize) //可周期性调度的线程池
    • 可以延时或者周期性的处理异步任务。
    • 使用DelayedWorkQueue存储任务,超时时间为0.
  • 底层调用:
public ThreadPoolExecutor(int corePoolSize,    //核心线程数
						  int maximumPoolSize, //线程池容量
						  long keepAliveTime,  //timeout时间,非核心线程存活时间
						  TimeUnit unit,    //时间单位
						  BlockingQueue 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;
		}

6. 线程池的状态:

 ThreadPoolExecutor-线程池的理解_第2张图片

  •  shutdown() VS shutdownNow()
    • shutdown():关闭线程池,线程池状态设置为SHUTDOWN。线程池不会接收新任务,但是阻塞队列里面的任务会被执行完。
    • shutdownNow():关闭线程池,线程池状态设置为STOP。线程池会终止正在运行的任务,并停止处理阻塞队里里面的任务,它有返回结果,返回的是阻塞队列里面的所有任务(List)。

7. 源码流程分析 ThreadPoolExecutor.execute()

  • public void execute(Runnable command) {
    	if (command == null)	//任务为空抛异常
    		throw new NullPointerException();
    	/*
    	 * Proceed in 3 steps:
    	 *
    	 * 1. If fewer than corePoolSize threads are running, try to
    	 * start a new thread with the given command as its first
    	 * task.  The call to addWorker atomically checks runState and
    	 * workerCount, and so prevents false alarms that would add
    	 * threads when it shouldn't, by returning false.
    	 *
    	 * 2. If a task can be successfully queued, then we still need
    	 * to double-check whether we should have added a thread
    	 * (because existing ones died since last checking) or that
    	 * the pool shut down since entry into this method. So we
    	 * recheck state and if necessary roll back the enqueuing if
    	 * stopped, or start a new thread if there are none.
    	 *
    	 * 3. If we cannot queue task, then we try to add a new
    	 * thread.  If it fails, we know we are shut down or saturated
    	 * and so reject the task.
    	 */
    	int c = ctl.get();	//判断线程池中工作线程数量是否小于核心线程数量
    	if (workerCountOf(c) < corePoolSize) {	//创建工作线程,并启动工作线程执行任务
    		if (addWorker(command, true))
    			return;
    		c = ctl.get();	
    	}
    	if (isRunning(c) && workQueue.offer(command)) {		//如果线程池是RUNNING状态并且这个时候还来任务,那么就把任务加入阻塞队列中
    		int recheck = ctl.get();
    		if (! isRunning(recheck) && remove(command))
    			reject(command);
    		else if (workerCountOf(recheck) == 0)
    			addWorker(null, false);
    	}
    	else if (!addWorker(command, false))	//创建非核心线程处理任务
    		reject(command);	//核心线程、最大线程数以及阻塞队列都满了,那么就是用拒绝策略来处理任务
    }

     

  • 任务来->核心线程处理->加入阻塞队列,如果阻塞队列满了->交给非核心线程(maxPoolSize-corePoolSize)线程处理,如果这一块也满了->使用4种拒绝策略处理
  • 拒绝策略
    • AbortPolicy                  -- 当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常。
    • CallerRunsPolicy         -- 当任务添加到线程池中被拒绝时,会在线程池当前正在运行的Thread线程池中处理被拒绝的任务。
    • DiscardOldestPolicy    -- 当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。
    • DiscardPolicy               -- 当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务。
    • 线程池默认的处理策略是AbortPolicy!

8.几个常见对比:

  • Runnable和Callable?
    • Runnable接口不会有任何的返回结果或者异常检查;JDK1.5之后才引入的Callable接口,Callable有返回结果,它是用于处理Runnable接口无法处理的场景。
  • submit()和execute的区别?
    • submit():执行完以后返回Future类型的对象,我们可以根据该返回值判断任务是否执行完。
    • execute():没有返回值,我们不知道任务是否执行完。

9.线程池大小确定

  • 在实际开发中,我们应该使用new ThreadPoolExecutor(***,***,***,...,)自定义参数来创建线程池。如何确定线程池大小,主要根据下面两种情况:
  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

10.源码分析之后的疑惑

  • ThreadPoolExecutor$Worker的run方法时谁调用的?
    • 是ThreadPoolExecutor里面的addWorker(Runnable firstTask, boolean core)方法里面的:w = new Worker(firstTask); final Thread t = w.thread; 之后t.start()调用的。
  • Worker(Runnable firstTask) {	
                setState(-1); // inhibit interrupts until runWorker
                this.firstTask = firstTask;
                this.thread = getThreadFactory().newThread(this); //由ThreadFactory产生线程,该线程创建成功之后调用该线程的start方法会运行Runnable的任务。
    			//而这里传入的参数是this,也就是Worker的实例,而Worker又实现了Runnable接口,因此会调用Worker的run方法。
            }
    		
    public interface ThreadFactory {
    
        /**
         * Constructs a new {@code Thread}.  Implementations may also initialize
         * priority, name, daemon status, {@code ThreadGroup}, etc.
         *
         * @param r a runnable to be executed by new thread instance
         * @return constructed thread, or {@code null} if the request to
         *         create a thread is rejected
         */
        Thread newThread(Runnable r);	//@param r a runnable to be executed by new thread instance
    }

     

    为了验证上面自己的想法,我本地写了一个验证类:

  • package com.sap.leo;
    
    import java.util.concurrent.ThreadFactory;
    import java.util.concurrent.atomic.AtomicInteger;
    //自定义ThreadFactory来创建线程
    public class LeoTestThreadFactory implements ThreadFactory
    {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;
    
        public LeoTestThreadFactory()
        {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                    Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                    poolNumber.getAndIncrement() +
                    "-thread-";
        }
    
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                    namePrefix + threadNumber.getAndIncrement(),
                    0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }
    
    package com.sap.leo;
    
    
    import java.util.concurrent.ThreadFactory;
    import java.util.concurrent.locks.AbstractQueuedSynchronizer;
    //测试类
    public class LeoTest
    {
        private volatile static ThreadFactory threadFactory;
    
        public static void main(String[] args) throws InterruptedException {
            TaskDemo task = new TaskDemo();
            Worker worker = new Worker(task);
            Thread t = worker.getT();
            t.start();
            Thread.sleep(50*1000);
        }
    
        private static  class TTT implements Runnable
        {
            @Override
            public void run()
            {
                System.out.println("TTT.run()...");
            }
        }
    
        private static final class Worker extends AbstractQueuedSynchronizer
        {
            private Thread t;
            private Runnable task;
    
            public Worker(Runnable task)
            {
                //使用自定义的ThreadFactory来创建线程,这里传递的任务是TTT
                //因此该线程启动的时候会调用TTT的run方法执行相应的任务
                this.t = getThreadFactory().newThread(new TTT());
                this.task = task;
            }
    
            public Thread getT() {
                return t;
            }
    
            public void setT(Thread t) {
                this.t = t;
            }
    
            public Runnable getTask() {
                return task;
            }
    
            public void setTask(Runnable task) {
                this.task = task;
            }
    
            public void run()
            {
                runWorker(this);
            }
    
    
        }
    
        public static void runWorker(Worker worker)
        {
            Runnable task = worker.getTask();
            task.run();
        }
    
        public static ThreadFactory getThreadFactory() {
            return new LeoTestThreadFactory();
        }
    }
    

    输出结果:

    • ThreadPoolExecutor-线程池的理解_第3张图片

  • 线程是如何实现复用的?
    • 在ThreadPoolExecutor的runWorker(Worker w)方法中,由于时候用了while死循环,因此只要阻塞队列不为空,那么创建的工作线程就会一直去阻塞队列里面获取任务来执行,这就实现了线程复用。
    • final void runWorker(Worker w) {
      	Thread wt = Thread.currentThread();
      	Runnable task = w.firstTask;
      	w.firstTask = null;
      	//为何要先解锁???
      	w.unlock(); // allow interrupts
      	boolean completedAbruptly = true;
      	try {
      		//只要阻塞队列不空那么就从里面取任务
      		while (task != null || (task = getTask()) != null) {
      			//处理任务过程中需要手动加锁
      			w.lock();
      			// If pool is stopping, ensure thread is interrupted;
      			// if not, ensure thread is not interrupted.  This
      			// requires a recheck in second case to deal with
      			// shutdownNow race while clearing interrupt
      			if ((runStateAtLeast(ctl.get(), STOP) ||
      				 (Thread.interrupted() &&
      				  runStateAtLeast(ctl.get(), STOP))) &&
      				!wt.isInterrupted())
      				wt.interrupt();
      			try {
      				beforeExecute(wt, task);
      				Throwable thrown = null;
      				try {
      					//处理任务
      					task.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;
      				w.completedTasks++;
      				//任务处理完手动解锁
      				w.unlock();
      			}
      		}
      		completedAbruptly = false;
      	} finally {
      		processWorkerExit(w, completedAbruptly);
      	}
      }
      我们再看看getTask()方法
    • private Runnable getTask() {
      	boolean timedOut = false; // Did the last poll() time out?
      
      	for (;;) {
      		int c = ctl.get();
      		int rs = runStateOf(c);
      
      		// Check if queue empty only if necessary.
      		if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
      			decrementWorkerCount();
      			return null;
      		}
      
      		int wc = workerCountOf(c);
      
      		// Are workers subject to culling?
      		boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
      
      		if ((wc > maximumPoolSize || (timed && timedOut))
      			&& (wc > 1 || workQueue.isEmpty())) {
      			if (compareAndDecrementWorkerCount(c))
      				return null;
      			continue;
      		}
      
      		try {
      			Runnable r = timed ?
      				workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
      				workQueue.take();		//如果限定的超神时间,那么就用poll方法获取任务,没有的话就用take方法获取任务。获取完任务会把任务宠阻塞队列里面删除
      			if (r != null)
      				return r;
      			timedOut = true;
      		} catch (InterruptedException retry) {
      			timedOut = false;
      		}
      	}
      }

      我们再来看看一个实现的阻塞队列:ArrayBlockingQueue(有界队列),它底层使用Object[]来存储任务的

  • 在runWorker(Worker worker)方法里面,为何任务执行的之前要先解锁?(暂未解决)

你可能感兴趣的:(Java,基础知识,线程池)