在java面试中有一道永远都绕不开的话题,那就是线程和线程池的问题。线程与线程池之间的关系就是一个石头跟多个石头之间的关系。那为什么有了线程还要有出现线程池这个概念呢?因为在程序中每创建和销毁一个线程是需要消耗大量的CPU资源的,如果是在一个不会频繁创建和销毁线程的小项目中,直接使用线程也不会出现什么问题,但是在一个并发量很高的系统中直接使用线程的话就会降低系统的性能并且还会出现OOM(内存溢出)现象。所以就出现了线程池的概念,线程池中的线程,使用完成后不会直接销毁,而是回收,等下次再用使就不必创建了。节约了系统资源。
创建线程的三种方式
Thread 类
Runnable 接口
Callable 接口
下面是三种创建线程的方法
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("-------执行自定义线程---------");
}
}
public class RunnableTest {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
}
}
class MyRunnable implements Runnable{
//还可以重写构造方法,传入一些参数
@Override
public void run() {
System.out.println("----------执行自定义线程-----------");
}
}
public class CallableTest {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<String>(myCallable);
//执行线程
new Thread(futureTask).start();
//等待线程执行结果,不过是阻塞等待
try {
String s = futureTask.get();
System.out.println("线程的执行结果是:"+s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String>{
@Override
public String call() throws Exception {
return "--------执行自定义线程--------";
}
}
看完了着三种线程的创建方法,那我们再说说他们之间的区别:
Thread 类没有什么可说的(它与Runnable几乎一样,因为Thread继承自Runnable)
Runnable 与 Callable之间的区别
- 两者最大的不同就是Runnable实现的线程执行完成没有返回值,而Callable实现的线程执行完成有返回值
- Callable的call方法抛出异常,把异常抛给主线程;而Runnable的run方法不允许抛出异常,异常只能再内部捕获解决。
CompletionService异步非阻塞获取并行任务的执行结果
通过上面的比较,我们会发现Callable比Runnable的功能要强大。的确是要强大,最重要的一点就是,callable可以有返回结果。但是我前面也说了,Future的get方法是阻塞的,所以问题就出现了。我们时候多线程就是为了能够让主线程不再阻塞,但如果你是用Callable实现线程,并且在主线程中通过Future的get方法获取结果的话,就又会把所有的阻塞堆积到主线程中,这就与我们的意愿所违背了。
所以我们解决这个问题就用到了CompletionService这个类。我们先简单了解一下这个类的作用:
CompletionService就相当于Executor+BlockingQueue,使用场景为当子线程并发了一系列的任务后,主线程需要实时的取回自宪曾任务的返回值并顺序的处理这些返回值,谁先返回就先处理谁。
如果不使用CompletionService的话,结果就是你先调用那个任务的get方法就会等待这个任务返回结果并处理,而不是处理那些已经执行完任务并有结果的线程任务。
CompletionService的实现是维护了一个保存Future对象的BlockingQueue。只有Future的状态是结束的时候才会加入到这个Queue中。CompletionService中的take方法其实就是从这个Queue中获取Future的过程。所以,这个Queue中的Future都是已经执行结束的,所以从这个Queue中获取执行结果就不会阻塞了。
public class CompletionServiceTest {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
ExecutorCompletionService<Integer> completionService = new ExecutorCompletionService<>(threadPool);
for(int i = 1; i<=5; i++){
final int index = i;
completionService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
if(index == 3){
Thread.sleep(3000);
}
return index;
}
});
}
threadPool.shutdown();
for(int i=0; i<5; i++){
try {
System.out.println("线程:"+completionService.take().get()+"执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}
ExecutorCompletionService的take/poll方法是对BlockingQueue对应的方法的封装, 关于BlockingQueue的take/poll方法:
i. take()方法, 如果队列中有数据, 就返回数据, 否则就一直阻塞;
ii. poll()方法: 如果有值就返回, 否则返回null
iii. poll(long timeout, TimeUnit unit)方法: 如果有值就返回, 否则等待指定的时间; 如果时间到了如果有值, 就返回值, 否则返回null
ExecutorCompletionService源码分析
参考文章多线程 | CompletionService异步非阻塞获取并行任务执行结果
虽然上面列出了三种线程的创建方式,但是我们平时使用的时候却很少通过这三种方式创建,而是一般都通过创建线程池的方式创建使用