现代多核处理器的发展以及业务规模的扩大,使多线程得到越来越广泛的应用,本篇文章主要以实战以及源码的角度进行分析,提升大家的多线程认知,有不对的地方还请海涵,指正。
多线程初级阶段我们知道简单的写一个多线程需要实现Runable或Callable接口,简单点的new一个Thread去跑,更多的是使用线程池,避免频繁创建销毁线程,通常都是用Executors提供的几个静态方法,大概分为四类:单线程线程池、固定线程数线程池、可缓存重用的线程池以及定时任务线程池。我们打开这些静态方法会发现其实最终创建的是ThreadPoolExecutor、ScheduledThreadPoolExecutor、这俩对象,我们先分析ThreadPoolExecutor这个对象。
/** * Creates a new {@code ThreadPoolExecutor} with the given initial * parameters. * * @param corePoolSize the number of threads to keep in the pool, even * if they are idle, unless {@code allowCoreThreadTimeOut} is set * @param maximumPoolSize the maximum number of threads to allow in the * pool * @param keepAliveTime when the number of threads is greater than * the core, this is the maximum time that excess idle threads * will wait for new tasks before terminating. * @param unit the time unit for the {@code keepAliveTime} argument * @param workQueue the queue to use for holding tasks before they are * executed. This queue will hold only the {@code Runnable} * tasks submitted by the {@code execute} method. * @param threadFactory the factory to use when the executor * creates a new thread * @param handler the handler to use when execution is blocked * because the thread bounds and queue capacities are reached */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueworkQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); }
前面三个线程池通过控制了一定的参数创建了ThreadPoolExecutor,那这几个参数至关重要了,我们先来分析这些参数,看jdk的注释已经非常明确了。
corePoolSize :表示一直留在线程池的线程数量,即使是空闲状态;
maximumPoolSize :线程池最大数量;
keepAliveTime :如果大于corePoolSize 的线程存在,超过这个时间的闲置线程会被回收。
unit:keepAliveTime 的时间单位;
workQueue:这个比较重要,表示如果线程池里的线程都在运行,再次提交任务会到这个队列里,我们看到这里是一个BlockingQueue接口,那支持的队列就多了,每种队列的特性也不同,像常用的ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等都支持。
threadFactory:这个参数也是个接口,主要配合guava的ThreadFactorybuilder来标识创建的线程名称(一定规则)、设置是否daemon等。
handler:超出线程池的处理能力的处理接口,这个通常有拒绝策略、丢弃策略等,默认拒绝策略,我们也可以实现自己的策略。
我们再看下newFixedThreadPool,newCachedThreadPool这两个方法:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); } public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue ()); }
很容易得到这两个静态方法的区别:
1、newFixedThreadPool确实是创建的固定线程池,队列用的是LinkedBlockingQueue,不限制长度,那么这种方式可能存在LinkedBlockingQueue过大,导致堆内存占满,所以在实际开发中这种不可取,至少要设置下队列的大小,防止业务高峰期服务宕机。
2、newCachedThreadPool,corePoolSize :0,maximumPoolSize 为Integer.MAX_VALUE,大概21亿,队列SynchronousQueue,这个队列是没有数据缓冲的队列,即队列里只能有一个任务,那意味着如果我们不停的往线程池提交任务,这个线程池的线程会无限大,同样业务高峰期会造成宕机。
单线程池的就不说了,那我们实际业务中肯定不能用jdk提供的,我们得自己创建线程池,配合我们自己的拒绝策略,起码保证服务不宕机。
下面的代码是实际开发中常用的创建方式,实际中可以根据硬件以及业务场景(cpu密集型、IO密集型)控制线程数量以及队列大小,或其他队列实现。线程数的大小一般设置为cpu核心的两倍,网上也有对应公式,这里就不做讨论了。
final static ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat("search-%d") .build(); private static final int CORE_POOL_SIZE = 20; private static final int MAXIMUM_POOL_SIZE = 20; private static final int QUEUE_MAX_CAPACITY = 256; private static final long KEEP_ALIVE_TIME = 0L; private static ExecutorService executorService = new ThreadPoolExecutor(CORE_POOL_SIZE , MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(QUEUE_MAX_CAPACITY), threadFactory, new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { log.error("线程池饱和策略启动,拒绝服务"); throw new BusinessException("系统繁忙,请重试!"); } });
设置线程名字的作用是在定位问题时,能在线程堆栈中方便查看,同样在压测过程中我们也可以通过一些工具:visualvm等查看线程的数量,或者用jmap参数打印堆栈查询。
前面提到我们有俩参数来设置线程数量大小,核心线程数、最大线程数,还有个队列,假设业务高峰期到来,核心线程数都在运行了,更多的任务来临的时候会不会直接创建线程直到最大线程数去处理任务呢,之后再多的任务过来时就到了队列里面,之前我一直是这样认为的,实际上是错的,我们必须明确了解我们的线程池是怎样工作的,这样在做性能优化时才更得心应手,我们来看源码:
/* * 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. */
这是源码的注释,很清楚了,先创建线程到核心线程数,如果不够用,加到队列里,如果队列满了,再创建线程到最大线程,如果创建失败了,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);
确实是这样。
多线程不得不提的就是CountDownLatch,这个类主要是做多线程控制的,比如我们启动多线程调用多个接口,需要等待这些接口返回,其中有一个接口没有返回,也不行,我们用一个简单的案例来说明怎么用。
CountDownLatch countDownLatch = new CountDownLatch(2); executorService.submit(new TestCallable(countDownLatch)); countDownLatch.await(2, TimeUnit.SECONDS);
TestCallable里面每执行完在finally中执行countDownLatch.countDown(),这时会减一,只有为0时阻塞才会放开,如果有一个线程没执行完,countDownLatch.await(5, TimeUnit.SECONDS)会阻塞,同时第二参数也是超时时间,超过时间阻塞将消失,继续走下面的逻辑。
当然还有个Cyclicbarrier也能做到控制的效果,cyclicbarrier主要特性是可以重复使用,它提供一个reset()方法进行重置。
我们的多线程程序上线了,总要压测下看看会不会出问题,那怎么分析?前面提到jmap以及visualvm工具,一个是jdk自带的,一个是图形界面的,我们先找个线程堆栈来看下
出现的问题主要体现在cpu利用率过高、线程数量过大且处于占满状态
"search-1" prio=6 tid=0x000000000e8d5000 nid=0x1ff4 waiting on condition [0x00000000100fe000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at com.jd.ql.sms.gis.TestThread$2.run(TestThread.java:34) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471) at java.util.concurrent.FutureTask.run(FutureTask.java:262) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:745) Locked ownable synchronizers: - <0x00000007c1d1d2e0> (a java.util.concurrent.ThreadPoolExecutor$Worker)
前面是线程名字,是通过ThreadFactory设置的,-1表示第一个线程。nid是一个16进制的线程号,Linux命令:top -H -p pid 可以查看每个线程的cpu使用情况,线上如果报cpu利用率过高,我们可以通过这个命令查到线程号,转成16进制(printf %x pid)然后到堆栈中查询,然后定位到代码,这样我们就能分析到具体问题了。
java.lang.Thread.State:线程当前的状态,可以通过搜索我们设置的名字来看下线程的状态。