前言
近期面试时被问得比较多的线程池,现在进行一个总结
1.创建线程方式
java有以下四种创建多线程的方式
(具体可参考https://blog.csdn.net/jinxinxin1314/article/details/105695860)
- 1:继承Thread类创建线程
- 2:实现Runnable接口创建线程
- 3:使用Callable和FutureTask创建线程
- 4:使用线程池,例如用Executor框架创建线程
第一种方式肯定是差不多淘汰了。因为用继承约束很大。
Callable和Runnable都可以应用于executors。而Thread类只支持Runnable,因此一般使用Callable和Runnable接口
Callable的使用
Callable和Runnable使用差不多, 但是Callable有返回值, 需要用Future接收
在创建线程时,需要callable传入FutureTask,然后FutureTask传入Thread
FutureTask futureTask = new FutureTask(callable);
new Thread(futureTask).start();
在线程池接受时,将Callable传入线程池pool,然后pool.submit()的到Future对象,再通过Future对象get()
ExecutorService pool = Executors.newFixedThreadPool(2);
// 创建两个有返回值的任务
Callable c1 = test.new MyCallable("A");
// 执行任务并获取Future对象
Future f1 = pool.submit(c1);
String a = f1.get().toString();
public static void main(String[] args) {
Callable callable = new Callable() {
@Override
public Object call() throws Exception {
return "this is Callable's message";
}
};
/**
* 将callable丢进任务里面
*/
FutureTask futureTask = new FutureTask(callable);
/**
* 启动线程, 执行任务
*/
new Thread(futureTask).start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("执行任务后的结果: " + futureTask.get());
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
2.创建线程Demo
package thread;
import java.util.concurrent.*;
public class ThreadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建线程的第一种方法
Thread1 thread1 = new Thread1();
thread1.start();
// 创建线程的第二种方法
Thread2 thread2 = new Thread2();
Thread thread = new Thread(thread2);
thread.start();
// 创建线程的第三种方法
Callable<String> callable = new Thread3();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread3 = new Thread(futureTask);
thread3.start();
String s = futureTask.get();
System.out.println(s);
// 创建线程的第四种方法
Executor executor = Executors.newFixedThreadPool(5);
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread()+"创建线程的第四种方法");
}
});
((ExecutorService) executor).shutdown();
}
}
class Thread1 extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread()+"创建线程的第一种方法");
}
}
class Thread2 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread()+"创建线程的第二种方法");
}
}
class Thread3 implements Callable<String> {
@Override
public String call() throws Exception {
return Thread.currentThread()+"创建线程的第三种方法";
}
}
3.Runnable接⼝和Callable接⼝
Runnable 接⼝不会返回结果或抛出检查异常,但是 Callable 接⼝可以。
通常情况下线程无返回值用Runnable,有返回值用Callable
public interface Runnable {
/**
* 被线程执⾏,没有返回值也⽆法抛出异常
*/
public abstract void run();
}
public interface Callable<V> {
/**
* 计算结果,或在⽆法这样做时抛出异常。
* @return 计算得出的结果
* @throws 如果⽆法计算结果,则抛出异常
*/
V call() throws Exception; }
4.execute()和submit()方法
1.execute() ⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执⾏成功与否;
2.submit() ⽅法⽤于提交需要返回值的任务。线程池会返回⼀个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执⾏成功,并且可以通过 Future 的 get() ⽅法来获取返回值, get() ⽅法会阻塞当前线程直到任务完成,⽽使⽤ get(long timeout,TimeUnit unit) ⽅法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执⾏完。
3.submit方法可以接受Runnable对象或者是Callable对象而executor只能接受Runnable对象。
submit方法接受Callable对象和接受Runnable对象的区别
参考文章
之前说过Runnable对象调用run后没有返回结果。所以任务完成后,虽然会返回Future对象,但是实际上调用get方法得到的是null。而如果是Callable对象作为参数给submit方法,那么最后完成任务后的Future对象中可以get到call得结果。
5.submit结合Callable使用
使用场景:多个有返回值的线程进行取值https://jingyan.baidu.com/article/6525d4b13b1f0fac7d2e9431.html
public class CallableFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableFutureTest test = new CallableFutureTest();
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
// 创建两个有返回值的任务
Callable c1 = test.new MyCallable("A");
Callable c2 = test.new MyCallable("B");
// 执行任务并获取Future对象
Future f1 = pool.submit(c1);
Future f2 = pool.submit(c2);
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>" + f1.get().toString());
System.out.println(">>>" + f2.get().toString());
// 关闭线程池
pool.shutdown();
}
class MyCallable implements Callable {
private String oid;
MyCallable(String oid) {
this.oid = oid;
}
public Object call() throws Exception {
return oid + "任务返回的内容";
}
}
}
6.线程池核心参数:
核心线程数corePoolSize
最大线程数maximumPoolSize
缓冲队列workQueue
1.小于corePoolSize时,新建线程执行任务
2.大于等于corePoolSize时,新插入的任务进入workQueue排队(如果workQueue长度允许)
3.workQueue也满了,但是小于maximumPoolSize,对于新加入的任务,新建线程
4.workQueue满了,并且池中正在运行的线程数等于maximumPoolSize,对于新加入的任务,执行拒绝策略(线程池默认的拒绝策略是抛异常)
7.线程池构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
-
corePoolSize: 线程池核心线程数
-
maximumPoolSize:线程池最大数
-
keepAliveTime: 空闲线程存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。
-
unit: 时间单位
-
workQueue: 线程池所使用的缓冲队列
-
threadFactory:线程池创建线程使用的工厂
-
handler: 线程池对拒绝任务的处理策略
构造方法可参考文章
8.线程池构造demo
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5,
10,
1L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
new ThreadPoolExecutor.CallerRunsPolicy());
//处理策略默认抛出异常
9.线程池使用实例
1.编写MyRunnable.class实现Runnable接口创建线程
package com.apollo.demo2;
import java.util.Date;
public class MyRunnable implements Runnable {
private String command;
public MyRunnable(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start.Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + " End.Time = " + new Date());
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return this.command;
}
}
2.编写ThreadPoolExecutorDemo.class创建线程池executor,将新创建线程当作参数传入executor.execute()
package com.apollo.demo2;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//使⽤阿⾥巴巴推荐的创建线程池的⽅式
//通过ThreadPoolExecutor构造函数⾃定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建WorkerThread对象(WorkerThread类实现了Runnable 接⼝)
Runnable worker = new MyRunnable("" + i);
//执⾏Runnable
executor.execute(worker);
}
//终⽌线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
可以看到我们上⾯的代码指定了:
-
corePoolSize : 核⼼线程数为 5。
-
maximumPoolSize :最⼤线程数 10
-
keepAliveTime : 等待时间为 1L。
-
unit : 等待时间的单位为 TimeUnit.SECONDS。
-
workQueue :任务队列为 ArrayBlockingQueue ,并且容量为 100;
-
handler :饱和策略为 CallerRunsPolicy 。
10.缓冲队列ArrayBlockingQueue和LinkedBlockingQueue异同
参考文章https://www.cnblogs.com/lianliang/p/5765349.html
相同:
1、LinkedBlockingQueue和ArrayBlockingQueue都实现了BlockingQueue接口;
2、LinkedBlockingQueue和ArrayBlockingQueue都是可阻塞的队列
内部都是使用ReentrantLock和Condition来保证生产和消费的同步;
当队列为空,消费者线程被阻塞;当队列装满,生产者线程被阻塞
不同:
1.锁机制不同
LinkedBlockingQueue中的锁是分离的,生产者的锁PutLock,消费者的锁takeLock
而ArrayBlockingQueue生产者和消费者使用的是同一把锁;
2、他们的底层实现机制也不同
LinkedBlockingQueue内部维护的是一个链表结构
在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大
而ArrayBlockingQueue内部维护了一个数组
在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例
3、构造时候的区别
LinkedBlockingQueue有默认的容量大小为:Integer.MAX_VALUE,当然也可以传入指定的容量大小
ArrayBlockingQueue在初始化的时候,必须传入一个容量大小的值