一、创建线程的三种方式
1.1继承Thread类创建线程类
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
/*第一种方式:继承Thread类重写run*/
public class ThreadDemo {
public static void main(String[] args) {
ThreadToExtends t = new ThreadToExtends();
t.start();
}
}
class ThreadToExtends extends Thread
{
@Override
public void run() {
System.out.println("Thread 1");
System.out.println(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getPriority());
}
}
1.2 通过Runnable接口创建线程类
- 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程。
/*第一种方式:继承Thread类重写run*/
public class ThreadDemo {
public static void main(String[] args) {
ThreadToImpl t = new ThreadToImpl();
Thread thread = new Thread(t);
thread.start();
}
}
class ThreadToExtends extends Thread
{
@Override
public void run() {
System.out.println("Thread 1");
System.out.println(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getPriority());
}
}
1.3通过Callable和Future创建线程
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
- 实现Callable接口需要抛出异常,或者内部进行try-catch
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadCallable t = new ThreadCallable();
FutureTask<String> task = new FutureTask<String>(t);
Thread t1 = new Thread(task);
t1.start();
System.out.println(task.get());
}
}
class ThreadCallable implements Callable<String>
{
@Override
public String call() throws Exception {
String str = "Callable";
return str;
}
}
二、三种方式的对比
1.采用实现Runnable、Callable接口的方式创建多线程时,
优势是:
- 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
- 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
- 编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
2、使用继承Thread类的方式创建多线程时,
优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。
3、Runnable和Callable的区别
(1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
(3) call方法可以抛出异常,run方法不可以。
(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
二、线程池的原理
线程池的意义在于:
1、减少在创建和销毁线程上所花的时间以及系统资源的开销,提升任务执行性能。
2、控制进程中线程数量的峰值,避免系统开销过大。
通过线程池,可创建一定数量的线程,并由线程池管理。在需要执行任务时,直接使用其中的线程。任务执行完成后,线程保留,并可用于执行下一个任务。如果任务比线程多,则等待线程空闲。
2.1 线程池的理解
java.uitl.concurrent.ThreadPoolExecutor
类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。下面我们来看一下ThreadPoolExecutor类的具体实现源码。
ThreadPoolExecutor提供了四个构造方法,我们看下最重要的一个构造函数并逐个了解每个参数的意义(面试考点):
public class ThreadPoolExecutor extends AbstractExecutorService {
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
}
函数的参数含义如下:
-
corePoolSize: 线程池维护线程的最少数量
核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中; -
maximumPoolSize:线程池维护线程的最大数量
它表示在线程池中最多能创建多少个线程; -
keepAliveTime: 线程池维护线程所允许的空闲时间
表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)
方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0; -
unit: 线程池维护线程所允许的空闲时间的单位
参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
- workQueue: 线程池所使用的缓冲队列用来存放待执行的任务
一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue;
PriorityBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
workQueue的类型为BlockingQueue,通常可以取下面三种类型:
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
- LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
- synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和SynchronousQueue。
线程池的排队策略与BlockingQueue有关。
-
threadFactory:线程工厂,主要用来创建线程
-
handler: 线程池对拒绝任务的处理策略
有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
2.2 线程池执行的过程
1.线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2.当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
4.当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
2.3ThreadPoolExecutor的继承关系
继承关系-
Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;
-
然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;
-
抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;
-
然后ThreadPoolExecutor继承了类AbstractExecutorService。
在ThreadPoolExecutor类中有几个非常重要的方法:
execute()
submit()
shutdown()
shutdownNow()
-
execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
-
submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。
-
shutdown()和shutdownNow()是用来关闭线程池的。
2.4 线程池举例说明
举个简单的例子:
假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。
因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;
当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;
如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来;
然后就将任务也分配给这4个临时工人做;
如果说着14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。
当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。
这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。
也就是说corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施。
2.5线程池容量的动态调整
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:
setCorePoolSize()和setMaximumPoolSize(),
-
setCorePoolSize:设置核心池大小
-
setMaximumPoolSize:设置线程池最大能创建的线程数目大小
当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。
2.6线程池的关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
- shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
- shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
三、线程池的使用示例
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,200,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5));
for(int i=0;i<15;i++)
{
ThreadToImpl threadTo = new ThreadToImpl(i);
executor.execute(threadTo);
System.out.println("线程池中线程数目:"+executor.getPoolSize()+
",队列中等待执行的任务数目:"+executor.getQueue().size()+
",已执行完毕的任务数目:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
class ThreadToImpl implements Runnable
{
private int taskNum;
public ThreadToImpl(int taskNum)
{
this.taskNum = taskNum;
}
@Override
public void run() {
System.out.println("正在执行task "+taskNum);
try {
Thread.currentThread().sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task "+taskNum+"执行完毕");
}
}
输出结果:
正在执行task 1
线程池中线程数目:1,队列中等待执行的任务数目:0,已执行完毕的任务数目:0
线程池中线程数目:2,队列中等待执行的任务数目:0,已执行完毕的任务数目:0
正在执行task 2
线程池中线程数目:3,队列中等待执行的任务数目:0,已执行完毕的任务数目:0
正在执行task 3
线程池中线程数目:4,队列中等待执行的任务数目:0,已执行完毕的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:0,已执行完毕的任务数目:0
正在执行task 4
正在执行task 5
线程池中线程数目:5,队列中等待执行的任务数目:1,已执行完毕的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:2,已执行完毕的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:3,已执行完毕的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:4,已执行完毕的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:5,已执行完毕的任务数目:0
线程池中线程数目:6,队列中等待执行的任务数目:5,已执行完毕的任务数目:0
线程池中线程数目:7,队列中等待执行的任务数目:5,已执行完毕的任务数目:0
正在执行task 11
正在执行task 12
线程池中线程数目:8,队列中等待执行的任务数目:5,已执行完毕的任务数目:0
正在执行task 13
线程池中线程数目:9,队列中等待执行的任务数目:5,已执行完毕的任务数目:0
正在执行task 14
线程池中线程数目:10,队列中等待执行的任务数目:5,已执行完毕的任务数目:0
正在执行task 15
task 1执行完毕
task 2执行完毕
task 5执行完毕
正在执行task 7
task 3执行完毕
正在执行task 6
正在执行task 9
task 4执行完毕
正在执行task 8
正在执行task 10
task 13执行完毕
task 12执行完毕
task 11执行完毕
task 14执行完毕
task 15执行完毕
task 8执行完毕
task 10执行完毕
task 7执行完毕
task 6执行完毕
task 9执行完毕
从执行结果可以看出,当线程池中线程的数目大于5时,便将任务放入任务缓存队列里面,当任务缓存队列满了之后,便创建新的线程。如果上面程序中,将for循环中改成执行20个任务,就会抛出任务拒绝异常了。
四、关于Executors提供的四种线程池
Executors 提供了一系列工厂方法用于创建线程池,返回的线程池都实现了 ExecutorService 接口。
// 创建固定数目线程的线程池。
public static ExecutorService newFixedThreadPool(int nThreads)
// 创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。
// 如果现有线程没有可用的,则创建一个新线 程并添加到池中。
// 终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newCachedThreadPool()
// 创建一个单线程化的Executor。
public static ExecutorService newSingleThreadExecutor()
// 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
这四种方法都是用的 Executors 中的 ThreadFactory 建立的线程。
4.1 newCachedThreadPool()
- 缓存型池子,先查看池中有没有以前建立的线程,如果有,就 reuse ,如果没有,就建一个新的线程加入池中
- 缓存型池子通常用于执行一些生存期很短的异步型任务 ,因此在一些面向连接的 daemon 型 SERVER 中用得不多。但对于生存期短的异步任务,它是 Executor 的首选。
- 能 reuse 的线程,必须是 timeout IDLE 内的池中线程,缺省 timeout 是 60s,超过这个 IDLE 时长,线程实例将被终止及移出池。
4.2 newFixedThreadPool(int)
- newFixedThreadPool 与 cacheThreadPool 差不多,也是能 reuse 就用,但不能随时建新的线程。
- 其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子。
- 和 cacheThreadPool 不同,FixedThreadPool 没有 IDLE 机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的 TCP 或 UDP IDLE 机制之类的),所以 FixedThreadPool 多数针对一些很稳定很固定的正规并发线程,多用于服务器。
- 从方法的源代码看,cache池和fixed 池调用的是同一个底层 池,只不过参数不同:
1.fixed 池线程数固定,并且是0秒IDLE(无IDLE)。
2.cache 池线程数支持 0-Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60 秒 IDLE 。
4.3 newScheduledThreadPool(int)
- 调度型线程池
- 这个池子里的线程可以按 schedule 依次 delay 执行,或周期执行
4.4 SingleThreadExecutor()
- 单例线程,任意时间池中只能有一个线程
- 用的是和 cache 池和 fixed 池相同的底层池,但线程数目是 1-1,
0 秒 IDLE(无 IDLE)
一般来说,CachedTheadPool 在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的 Executor 的首选,只有当这种方式会引发问题时(比如需要大量长时间面向连接的线程时),才需要考虑用 FixedThreadPool。
4.5 总结
-
newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;
-
newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,也使用的LinkedBlockingQueue;
-
newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
五、优化线程池线程数量
线程池的理想大小取决于被提交任务的类型以及所部署系统的特性。在代码中不会固定线程池的大小,而应该通过某种配置机制来来提供,或者根据Runtime.getRuntime().availableProcessors()来动态计算。
如果一台服务器上只部署这一个应用并且只有一个线程池(N为CPU总核数):
- 如果是CPU密集型应用,则线程池大小设置为N+1
- 如果是IO密集型应用,则线程池大小设置为2N+1
线程等待时间所占比例越高,需要越多线程。
线程CPU时间所占比例越高,需要越少线程。
【黄金公式】最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
假设值
tasks :每秒的任务数,假设为500~1000
taskcost:每个任务花费时间,假设为0.1s
responsetime:系统允许容忍的最大响应时间,假设为1s
计算
-
corePoolSize = 每秒需要多少个线程处理?
threadcount = tasks/(1/taskcost) =taskstaskcout = (500~1000)0.1 = 50~100 个线程。corePoolSize设置应该大于50
根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可 -
queueCapacity = (coreSizePool/taskcost)*responsetime
计算可得 queueCapacity = 80/(1/0.1) = 80。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行
切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
- maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)
计算可得 maxPoolSize = (1000-80)/10 = 92
(最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
-
rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
-
keepAliveTime和allowCoreThreadTimeout采用默认通常能满足
网友评论