并发编程-线程池

线程池

为什么需要线程池?

每次新开线程去执行任务,运行完任务销毁线程,都会消耗资源(操作系统级别的线程)

线程池就可以提前创建线程、保留线程,节约资源、节省掉开辟线程和销毁线程的消耗、提高效率

如何设置线程池的核心线程数和最大线程数?

CPU密集型任务:依靠CPU去进行计算。线程池开CPU核心数+1个线程(任务线程中断/阻塞的情况,保证CPU仍然有任务执行,充分压榨CPU的性能所以+1)

IO密集型任务:网络相关,文件IO,网络IO,磁盘IO。例:等待MySQL返回结果,等待接口返回响应;等待阻塞时不需要用到CPU。理论上执行IO的时间越长,可以开辟更多的线程。线程数和IO时间有关,和CPU核数无关

混合任务:根据业务场景拆分

理论上:线程数 = CPU核心数 * (1 + 线程等待时间/线程运行总时间)

通常先计算出理论的线程数,再去压测尽量模拟生产环境,得到最优的线程数。

线程池参数设置?

队列大小:队列必须是阻塞队列能容忍最多几秒拿到结果,能接受的最多的排队时间,根据任务平均执行时间计算。(假设通过压测得到最优线程数为100,平均任务执行时间1s,也就是说1s可以处理完100个任务。这种情况下,队列长度如果设置500,就代表从进入队列到拿到结果最多要经历5s的时间,等待5s可以接受就能设置队列长度为500,接受不了就必须砍掉队列长度,根据实际容忍排队时间来设置队列长度)

核心线程数:假设通过压测得到最优线程数为500,核心业务/请求数多的可以设置为500;其他业务/几分钟请求一次这种,可以设置20或者其他,设置小一些没关系

最大线程数:假设通过压测得到最优线程数为500,可以设置最大线程数为500,也可以设置其他压测结果ok的。

最大空闲时间:1分钟,半分钟都可以

ThreadPoolExecutor中执行任务的方法?

1、void execute(Runnable command)

package java.util.concurrent;

// 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)) {
		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);
}

2、Future submit(Runnable task)

submit中最终还是调用的execute()方法,区别是会返回一个Future对象,用来获取任务执行结果

package java.util.concurrent;

// ThreadPoolExecutor extends AbstractExecutorService
// AbstractExecutorService#submit
public Future submit(Runnable task) {
	if (task == null) throw new NullPointerException();
	RunnableFuture ftask = newTaskFor(task, null);
	execute(ftask);
	return ftask;
}

execute(Runnable command)方法执行步骤:

并发编程-线程池_第1张图片

注意:提交一个Runnable时,不管当前线程池中的线程是否空闲,只要数量小于核心线程数就会创建新线程。 

注意:ThreadPoolExecutor相当于是非公平的,比如队列满了之后提交的Runnable可能会比正在排队的Runnable先执行。

非公平体现在:线程数大于核心线程数,小于最大线程数,阻塞队列满的情况下,新添加的任务可能先于队列中的任务执行

线程池中的线程是如何关闭的?

1、Thread类提供了一个stop(),但是标记了@Deprecated,为什么不推荐用stop()方法来停掉线程呢?

因为stop()方法太粗暴了,一旦调用了stop(),就会直接停掉线程,但是调用的时候根本不知道线程刚刚在做什么,任务做到哪一步了,这是很危险的。

注意:stop()会释放线程占用的synchronized锁(不会自动释放ReentrantLock锁,这也是不建议用stop()的一个因素)。

2、非核心线程,超过最大空闲时间后,自动销毁关闭

(注意:核心线程和非核心线程实际上没有区别,哪个线程CAS获取到锁,就会销毁哪个线程)

3、线程的内部发生异常时,线程关闭销毁。(销毁后小于核心线程数时,立刻补充创建一个空任务的线程)

4、推荐:自定义一个变量(默认false,修改为true 后结束线程),或者通过中断 interrupt 停掉线程,线程池就是用的 interrupt。自定义变量的好处是:线程自身可以控制到底要不要停止,何时停止

注意:线程sleep过程中如果被中断了会接收到异常。

// ThreadPoolExecutor#shutdownNow
public List shutdownNow() {
	List tasks;
	final ReentrantLock mainLock = this.mainLock;
	mainLock.lock();
	try {
		checkShutdownAccess();
		advanceRunState(STOP);
		interruptWorkers();
		tasks = drainQueue();
	} finally {
		mainLock.unlock();
	}
	tryTerminate();
	return tasks;
}

// ThreadPoolExecutor#interruptWorkers
private void interruptWorkers() {
	final ReentrantLock mainLock = this.mainLock;
	mainLock.lock();
	try {
		for (Worker w : workers)
			w.interruptIfStarted();
	} finally {
		mainLock.unlock();
	}
}

// ThreadPoolExecutor#Worker#interruptIfStarted
void interruptIfStarted() {
	Thread t;
	if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
		try {
			t.interrupt();
		} catch (SecurityException ignore) {
		}
	}
}

线程池为什么一定得是阻塞队列?

线程池中的线程在运行过程中,执行完创建线程时绑定的第一个任务后,就会不断的从队列中获取任务并执行,那么如果队列中没有任务了,线程为了不自然消亡,就会阻塞在获取队列任务时,等着队列中有任务过来就会拿到任务从而去执行任务。

通过这种方法能最终确保,线程池中能保留指定个数的核心线程数

workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();

线程阻塞在这里,核心线程可以保活;并且阻塞队列中有任务时,可以立刻取出来执行

// ThreadPoolExecutor#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();
			if (r != null)
				return r;
			timedOut = true;
		} catch (InterruptedException retry) {
			timedOut = false;
		}
	}
}

某个线程在从队列获取任务时,会判断是否使用超时阻塞获取,我们可以认为非核心线程会poll(),核 心线程会take(),非核心线程超过时间还没获取到任务后面就会自然消亡了。

线程发生异常,会被移出线程池吗?

会的,那有没有可能核心线程数在执行任务时都出错了,导致所有核心线程都被移出了线程池?

在源码中,当执行任务时出现异常时,最终会执行processWorkerExit(),执行完这个方法后,当前线程也就自然消亡了,但是!processWorkerExit()方法中会额外再新增一个线程,这样就能维持住固定的核心线程数。

Tomcat是如何自定义线程池的?

Tomcat中用的线程池为org.apache.tomcat.util.threads.ThreadPoolExecutor,注意类名和JUC下的 一样,但是包名不一样。

Tomcat创建这个线程池:

public void createExecutor() {
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);
}

TaskQueue的入队逻辑:

public boolean offer(Runnable o) {
    //we can't do any checks
    if (parent==null) {
        return super.offer(o);
    }

    // 如果线程池的线程个数等于最大线程池数才入队。注意和JUC下面线程池入队的区别
    //we are maxed out on threads, simply queue the object
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) {
        return super.offer(o);
    }

    // 正在处理的任务数小于线程池中的线程个数,会直接入队,而不会去创建线程。优先使用空闲线程
    //we have idle threads, just add it to the queue
    if (parent.getSubmittedCount()<=(parent.getPoolSize())) {
        return super.offer(o);
    }

    // 线程池的线程个数小于最大线程池数,返回false,入队失败。这里表示创建线程直到达到最大线程数
    //if we have less threads than maximum force creation of a new thread
    if (parent.getPoolSize()

总结Tomcat的线程池:随着任务的提交,会优先创建线程,直到线程个数等于最大线程数才会入队。

创建Tomcat的ThreadPoolExecutor时,自动创建核心线程。JUC的ThreadPoolExecutor不会提前创建核心线程

当线程数小于最大线程数,并且存在空闲线程时,优先入队使用空闲线程来执行任务,充分利用线程资源

思考:线程池的五种状态是如何流转的?

总结:

1、CPU密集型任务:CPU核心数+1,这样既能充分利用CPU,也不至于有太多的上下文切换成本

2、IO型任务:建议压测,或者先用公式计算出一个理论值(理论值通常都比较小)

3、对于核心业务(访问频率高),可以把核心线程数设置为我们压测出来的结果,最大线程数可以等于核心线程数,或者大一点点,比如我们压测时可能会发现500个线程最佳,但是600个线程时也还行,此时600就可以为最大线程数

4、对于非核心业务(访问频率不高),核心线程数可以比较小,避免操作系统去维护不必要的线程,最大线程数可以设置为我们计算或压测出来的结果。

你可能感兴趣的:(java)