1. Runnable接口
步骤:
- 定义类实现Runnable接口:覆盖Runnable接口中的run方法,将线程要运行的代码存放在该run方法中。
- 通过Thread 类建立线程对象:将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
- 调用Thread类的start方法开启线程。
public class example {
public static void main(String[] args){
MyThread myThread=new MyThread();
Thread thread=new Thread(myThread);
thread.start();
while(true)
{
System.out.println("Main方法在运行");
}
}
}
class MyThread implements Runnable{
public void run(){
while(true){
System.out.println("MyThread类的run()方法在运行");
}
}
}
2. Callable和Future接口
不论是继承Thread
类还是实现Runnable
接口,构造的线程都有一个弊端,那就是既没有返回值也不会抛出异常,这一点看run()方法返回值是void就知道。
(1)Callable接口
public interface Callable<V> {
V call() throws Exception;
}
返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call 的方法。Callable 接口类似于 Runnable
,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。
Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值
(2)Future接口
Future
表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get
方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel
方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future
但又不提供可用的结果,则可以声明 Future<?>
形式类型、并返回 null
作为底层任务的结果。示例:
interface ArchiveSearcher { String search(String target); }
class App {
ExecutorService executor = ...
ArchiveSearcher searcher = ...
void showSearch(final String target)
throws InterruptedException {
Future<String> future
= executor.submit(new Callable<String>() {
public String call() {
return searcher.search(target);
}});
displayOtherThings(); // do other things while searching
try {
displayText(future.get()); // use future
} catch (ExecutionException ex) { cleanup(); return; }
}
}
方法摘要:
V get()
:获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。V get(Long timeout , TimeUnit unit)
:获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。boolean isDone()
:如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。boolean isCancelled()
:如果任务完成前被取消,则返回true。boolean cancel(boolean mayInterruptRunning)
:
- 如果任务还没开始,执行cancel(…)方法将返回false;
- 如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;
- 当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;
- 当任务已经完成,执行cancel(…)方法将返回false。
mayInterruptRunning
参数表示是否中断执行中的线程。
(3)FutureTask类
public class FutureTask<V> extends Object implements RunnableFuture<V>
但是我们必须明白Future只是一个接口,我们无法直接创建对象,因此就需要其实现类FutureTask。FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值,那么这个组合的使用有什么好处呢?假设有一个很耗时的返回值需要计算,并且这个返回值不是立刻需要的话,那么就可以使用这个组合,用另一个线程去计算返回值,而当前线程在使用这个返回值之前可以做其它的操作,等到需要这个返回值时,再通过Future得到。
(1)FutureTask通过Thread封装,再调用start()方法启动:
public class CallableAndFuture {
public static void main(String[] args) {
Callable<Integer> callable = new Callable<Integer>() {
public Integer call() throws Exception {
return new Random().nextInt(100);
}
};
FutureTask<Integer> future = new FutureTask<Integer>(callable);
new Thread(future).start();
try {
Thread.sleep(5000);// 可能做一些事情
System.out.println(future.get()); // 注意这里用get方法获取结果
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
(2)使用线程池(具体概念稍后介绍)
package com.zejian.Executor;
import java.util.concurrent.Callable;
public class CallableDemo implements Callable<Integer> {
private int sum;
@Override
public Integer call() throws Exception {
System.out.println("Callable子线程开始计算啦!");
Thread.sleep(2000);
for(int i=0 ;i<5000;i++){
sum=sum+i;
}
System.out.println("Callable子线程计算结束!");
return sum;
}
}
package com.zejian.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableTest {
public static void main(String[] args) {
//创建线程池
ExecutorService es = Executors.newSingleThreadExecutor();
//创建Callable对象任务
CallableDemo calTask=new CallableDemo();
//提交任务并获取执行结果
Future<Integer> future = es.submit(calTask); // submit返回一个Future对象
//关闭线程池
es.shutdown();
try {
Thread.sleep(2000);
System.out.println("主线程在执行其他任务");
if(future.get()!=null){
//输出获取到的结果
System.out.println("future.get()-->"+future.get());
}else{
//输出获取到的结果
System.out.println("future.get()未获取到结果");
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("主线程在执行完成");
}
}
3. ThreadPoolExecutor类
构造方法
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。在ThreadPoolExecutor类中提供了四个构造方法:
public class ThreadPoolExecutor extends AbstractExecutorService {
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
...
}
下面解释下一下构造器中各个参数的含义:
(1)corePoolSize
核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
(2)maximumPoolSize
线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程。注意,如果workQueue使用了无限工作队列则这个参数没有意义。
(3)keepAliveTime
表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
(4)unit
参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
- TimeUnit.DAYS; //天
- TimeUnit.HOURS; //小时
- TimeUnit.MINUTES; //分钟
- TimeUnit.SECONDS; //秒
- TimeUnit.MILLISECONDS; //毫秒
- TimeUnit.MICROSECONDS; //微妙
- TimeUnit.NANOSECONDS; //纳秒
(5)workQueue
一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
- ArrayBlockingQueue:一个基于数组结构的
有界
阻塞队列,必须指定容量,按FIFO排序元素。 - LinkedBlockingQueue:一个基于链表的阻塞队列,可以有容量限制,也可以无限。吞吐量高于ArrayBlockingQueue。Executes.newFixedThreadPool()使用了这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。它的用处是让优先级高的线程先执行,优先级由任务的Comparator决定。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等待另一个线程的移除操作,否则插入操作一直处于阻塞状态。Executes.newCachedThreadPool()使用了这个队列。属于无界队列。
线程池的排队策略与BlockingQueue有关。
(6)threadFactory
线程工厂,主要用来创建线程.
(7)handler
表示当拒绝处理任务时的策略,有以下四种取值:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
(8)工作线程Worker
线程池在创建线程时会将其封装为工作线程Worker,Worker在执行完任务后,还会循环地获取工作队列里的任务来执行。
重要方法
execute
void execute(Runnable command)
execute()方法实际上是Executor中声明的方法,用于提交不需要返回值的任务。在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
执行步骤:
(1)如果当前运行的线程数量 < corePoolSize
,则创建新线程来执行任务。创建新线程需要给线程池加全局锁。
(2)如果当前运行的线程数量 >= corePoolSize
,则加入阻塞队列BlockingQueue
。
(3)如果BlockingQueue
已满,则创建新的线程来处理任务(需要全局锁)。
(4)如果创建新线程会使当前运行的线程超出maximumuPoolSize
,则交给饱和策略去处理。
submit
<T> Future<T> submit(Callable<T> task)
提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。
Future<?> submit(Runnable task)
提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
<T> Future<T> submit(Runnable task, T result)
提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
submit()方法是在ExecutorService中声明的方法,用于提交需要返回值的任务。在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。
shutdown和shutdownNow
- shutdown()后线程池将变成shutdown状态,此时不接收新任务,但会处理完正在运行的 和 在阻塞队列中等待处理的任务。
- shutdownNow()后线程池将变成stop状态,此时不接收新任务,不再处理在阻塞队列中等待的任务,还会尝试中断正在处理中的工作线程。
三种常用线程池
FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, // corePoolSize 和maximumPoolSize 都是nThreads
0L, TimeUnit.MILLISECONDS, // keepAliveTime 0
new LinkedBlockingQueue<Runnable>()); // 使用无界阻塞队列
}
- 维护着固定数量的工作线程。
- 由于
corePoolSize
和maximunPoolSize
都为用户设定的线程数量nThreads
,所以当线程数少于nThreads
时新提交的任务都会触发工作线程的创建。 - 当线程池已经满了之后,所有提交的任务都会塞到无界的阻塞队列,这意味着该线程池永远不会拒绝任务。
keepAliveTime
参数在这里没有意义。- 弊端是在没有任何请求的时候也维持着
nThreads
的工作线程。
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, // corePoolSize 0, maximumPoolSize 对于 SynchronousQueue 没有意义
60L, TimeUnit.SECONDS, // keepAliveTime 60s
new SynchronousQueue<Runnable>());
}
- newCachedThreadPool可以减少不必要的线程创建和销毁上的消耗,请求到来,如果有空闲未超过60s的线程则复用;如果没有空闲线程则立刻创建新的线程,因为SynchronousQueue的容量为0;如果线程空闲超过60s则被销毁。
- 如果有批量的hello world过来,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程;如果保持每分钟(keepAliveTime 60s)有1000个hello world 请求,则一共会有1000个线程在,但是如果不用线程池,则每分钟创建1000个线程,每分钟关闭1000个线程。
- 自动回收不使用的线程(终止并从缓存中移除那些已有 60 秒钟未被使用的线程),在无可用线程的情况下自动的为新来的task创建新线程。 在小任务量,任务时间执行短的场景下能提高性能。
SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(){
return new ThreadPoolExecutor(1,1, // 维持一个工作线程
0L,TimeUnit.MILLISECONDS, // keepAliveTime 参数无意义
new LinkedBlockingQueue<Runnable>()); // 无界队列
}
keepAliveTime
参数只有在maximumPoolSize > corePoolSize
的情况下有效。- 可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
4. Java定时/周期任务
Timer
Timer是线程安全的,但是只能串行执行Task:
Timer类可以保证多个线程可以共享单个Timer对象而无需进行外部同步,所以Timer类是线程安全的。但是由于每一个Timer对象对应的是单个后台线程,用于顺序执行所有的计时器任务,一般情况下我们的线程任务执行所消耗的时间应该非常短,但是由于特殊情况导致某个定时器任务执行的时间太长,那么他就会“独占”计时器的任务执行线程,其后的所有线程都必须等待它执行完,这就会延迟后续任务的执行,使这些任务堆积在一起,具体情况我们后面分析。
核心方法(void schedule
):
schedule(TimerTask task, Date time)
:安排在指定的时间执行指定的任务。schedule(TimerTask task, Date firstTime, long period)
:安排指定的任务在指定的时间开始进行重复的固定延迟执行。schedule(TimerTask task, long delay)
:安排在指定延迟后执行指定的任务。schedule(TimerTask task, long delay, long period)
:安排指定的任务从指定的延迟后开始进行重复的固定延迟执行。同时也重载了scheduleAtFixedRate方法,scheduleAtFixedRate方法与schedule相同,只不过他们的侧重点不同,区别后面分析。scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
:安排指定的任务在指定的时间开始进行重复的固定速率执行。scheduleAtFixedRate(TimerTask task, long delay, long period)
:安排指定的任务在指定的延迟后开始进行重复的固定速率执行。
TimerTask
TimerTask类是一个抽象类,由Timer 安排为一次执行或重复执行的任务。它有一个抽象方法run()方法,该方法用于执行相应计时器任务要执行的操作。因此每一个具体的任务类都必须继承TimerTask,然后重写run()方法。
另外它还有两个非抽象的方法:
boolean cancel():取消此计时器任务。
long scheduledExecutionTime():返回此任务最近实际执行的安排执行时间。
public class TestTimer {
public static void main(String[] args) {
Timer timer = new Timer();
Task task = new Task();
timer.schedule(task, new Date(), 1000);
}
}
class Task extends TimerTask{
@Override
public void run() {
System.out.println("Do work...");
}
}
Timer的缺陷
(1)不能捕获异常
如果TimerTask抛出的了未检查异常则会导致Timer线程终止,同时Timer也不会重新恢复线程的执行,他会错误的认为整个Timer线程都会取消。同时,已经被安排但尚未执行的TimerTask也不会再执行了,新的任务也不能被调度。故如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。
(2)单线程
一个Timer在后台对应一个线程,虽然它是线程安全的,可以schedule多个TimerTask,但也只能串行执行。如果一个Task耗时超过了两个Task的间隔,会阻塞下一个Task的执行。
对于Timer的缺陷,我们可以考虑 ScheduledThreadPoolExecutor 来替代。Timer是基于绝对时间的,对系统时间比较敏感,而ScheduledThreadPoolExecutor 则是基于相对时间;Timer是内部是单一线程,而ScheduledThreadPoolExecutor内部是个线程池,所以可以支持多个任务并发执行。
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor是JDK1.5以后推出的类,用于实现定时、重复执行的功能,官方文档解释要优于Timer。
构造方法
(1) ScheduledThreadPoolExecutor(int corePoolSize)
使用给定核心池大小创建一个新定定时线程池 。
(2)ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactorythreadFactory)
使用给定的初始参数创建一个新对象,可提供线程创建工厂。
调度方法
(1)schedule(Callable callable, long delay, TimeUnit unit)
延迟delay时间后开始执行callable。
(2)scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
延迟initialDelay时间后开始执行command,并且按照period时间周期性重复调用,当任务执行时间大于间隔时间时,之后的任务都会延迟,此时与Timer中的schedule方法类
(3)scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
延迟initialDelay时间后开始执行command,并且按照period时间周期性重复调用,这里的间隔时间delay是等上一个任务完全执行完毕才开始计算,与Timer中scheduleAtFixedRate情况不同。
示例
import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Task implements Callable<String> {
private String name;
public Task(String name) {
super();
this.name = name;
}
@Override
public String call() throws Exception {
System.out.printf("%s: Starting at : %s\n",name,new Date());
return "hello world";
}
public static void main(String[] args) {
ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1);
System.out.printf("Main: Starting at: %s\n",new Date());
for(int i = 0; i < 5; i++) {
Task task = new Task("Task " + i);
executor.schedule(task, i, TimeUnit.SECONDS);
}
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Main: Ends at: %s\n",new Date());
}
}
DelayedWorkQueue
ScheduledThreadPoolExecutor是基于DelayedWorkQueue实现的,它是对它是对DelayQueue的优化。DelayQueue是Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。
该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null。
当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于等于 0 的值时,将发生到期。即使无法使用 take 或 poll 移除未到期的元素,也不会将这些元素作为正常元素对待。例如,size 方法同时返回到期和未到期元素的计数。此队列不允许使用 null 元素。