1. 实现多线程有几种方式?有什么区别?
实现多线程有 3 种方式。
1.1 继承 Thread 类
继承 Thread 类,重新 run() 方法。实现代码如下:
public class ExtendsThread extends Thread{
@Override
public void run() {
System.out.println("run ExtendsThread");
}
}
使用线程:
public class LearningThread {
public static void main(String[] args) {
func1();
}
public static void func1(){
ExtendsThread extendsThread = new ExtendsThread();
System.out.println("run func1");
extendsThread.start();
System.out.println("run func1 end");
}
}
复制代码
1.2 实现 Runnable 接口
实现 Runnable 接口,实现 run() 方法,通过 Thread 类来开启线程。代码如下:
public class ImplRunnable implements Runnable {
@Override
public void run() {
System.out.println("run ImplRunnable");
}
}
使用线程:
public class LearningThread {
public static void main(String[] args) {
func2();
}
public static void func2(){
ImplRunnable implRunnable = new ImplRunnable();
System.out.println("run func2");
Thread thread = new Thread(implRunnable);
thread.start();
System.out.println("run func2 end");
}
}
复制代码
1.3 实现 Callable 接口
实现 Callable 接口,实现 call() 方法。代码如下:
public class ImplCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 2020;
}
}
使用线程:
public class LearningThread {
public static void main(String[] args) {
func3();
}
public static void func3() throws ExecutionException, InterruptedException {
Callable<Integer> integerCallable = new ImplCallable();
System.out.println("run func3");
FutureTask<Integer> futureTask = new FutureTask<>(integerCallable);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("run func3 end");
System.out.println("futureTask.get = " + futureTask.get());
}
}
复制代码
这三种方式都可以实现多线程,第一种由于 Java 的单继承,不建议使用。至于实现 Runnable 接口,与实现 Callable 接口,的区别是,实现 Callable 接口后通过 futureTask.get() 方法可以获取线程内的执行结果。而 Runnable 是没有返回值的。
2. 为什么要用线程池?
线程池主要是为了减少每次创建线程时的资源消耗,重复利用创建好的线程,提高资源利用率。使用线程池由于减少了线程创建的过程,在每次接到请求时可以及时响应,提高响应速度。通过线程池统一管理线程,方便线程的分配,调优和监控。
3. 创建线程池的时候有哪些参数?
在 Java 源码中,创建线程池的构造方法最多有 7 个参数。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
复制代码
corePoolSize: 核心线程数,线程池的工作线程数量。
maximumPoolSize: 最大线程数,线程池中可以存活的最多的线程数量。当队列满了之后,会启用非核心线程,此时的线程池的大小变为最大线程数。
keepAliveTime: 非核心线程如果没有任务的话,可以存活的时间。
unit: 非核心线程存活时间的单位。
workQueue: 工作队列。当线程池中的线程达到 corePoolSize ,如果再来任务,就会放到工作队列里。
threadFactory: 线程工厂,线程池创建线程时使用的工厂。
handler:拒绝策略。如果线程池的线程达到了 maximumPoolSize ,如果再来任务,则执行拒绝策略。
4. 线程池是如何工作的?
当有任务提交到线程池时,首先启动核心线程。随着任务的增加,当核心线程用完之后,再次提交的线程将会进入工作队列。当工作队列满了之后,如果再次提交到线程池任务,将会判断,核心线程数是否小于最大线程数,如果小于,将会启用非核心线程,当工作的线程达到最大线程数后,如果还继续提交任务到线程池,则会执行拒绝策略来拒绝任务。
5. 常见的拒绝策略有哪些?
JDK 自带了 4 种拒绝策略。
5.1 AbortPolicy
直接丢弃任务,抛出 RejectedExecutionException
public class ThreadPoolRunnable implements Runnable {
private int number = 0;
public ThreadPoolRunnable(int number) {
this.number = number;
}
@Override
public void run() {
System.out.println("run "+Thread.currentThread().getName()+", number = " + number);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使用线程池:
public static void func4(){
ThreadPoolExecutor executor = new ThreadPoolExecutor(2,3,10,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 7; i++) {
Runnable runnable = new ThreadPoolRunnable(i);
executor.execute(runnable);
}
executor.shutdown();
}
执行结果:
run pool-1-thread-1, number = 0
run pool-1-thread-3, number = 3
run pool-1-thread-2, number = 1
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.learning.thread.ThreadPoolRunnable@1d44bcfa rejected from java.util.concurrent.ThreadPoolExecutor@266474c2[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.learning.thread.LearningThread.func4(LearningThread.java:45)
at com.learning.thread.LearningThread.main(LearningThread.java:11)
run pool-1-thread-1, number = 2
复制代码
由于最大线程数是 3 ,队列大小是 1,所以线程池最多可以同时存在 4 个线程,当提交第 5 任务时,主线程抛出异常。
5.2 CallerRunsPolicy
调用启用线程池的线程进行处理,不过会阻塞主线程。
public static void func4(){
ThreadPoolExecutor executor = new ThreadPoolExecutor(2,3,10,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 7; i++) {
Runnable runnable = new ThreadPoolRunnable(i);
executor.execute(runnable);
}
executor.shutdown();
}
执行结果:
run pool-1-thread-1, number = 0
run main, number = 4
run pool-1-thread-3, number = 3
run pool-1-thread-2, number = 1
run main, number = 5
run pool-1-thread-3, number = 2
run pool-1-thread-3, number = 6
复制代码
可以看到 第 5 个任务,即 number = 4,是在主线程中执行的,由于主线程被阻塞,导致第 6 个任务不能立即提交,当主线程执行结束后再提交时,线程池里已经有可用的线程了,所以第 6 个任务是线程池执行的。
5.3 DiscardPolicy
直接拒绝任务,不抛出任何异常
public static void func4(){
ThreadPoolExecutor executor = new ThreadPoolExecutor(2,3,10,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy());
for (int i = 0; i < 7; i++) {
Runnable runnable = new ThreadPoolRunnable(i);
executor.execute(runnable);
}
executor.shutdown();
}
执行结果:
run pool-1-thread-2, number = 1
run pool-1-thread-3, number = 3
run pool-1-thread-1, number = 0
run pool-1-thread-2, number = 2
复制代码
5.4 DiscardOldestPolicy
抛弃队列中最先加入的任务,然后将当前任务提交到线程池。
public static void func4(){
ThreadPoolExecutor executor = new ThreadPoolExecutor(2,3,10,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.DiscardOldestPolicy());
for (int i = 0; i < 7; i++) {
Runnable runnable = new ThreadPoolRunnable(i);
executor.execute(runnable);
}
executor.shutdown();
}
执行结果:
run pool-1-thread-1, number = 0
run pool-1-thread-3, number = 3
run pool-1-thread-2, number = 1
run pool-1-thread-1, number = 6
复制代码
number 0、1,使用核心线程执行。
number 2,放入队列。
number 3,启用非核心线程执行。
number 4,抛弃 number 2,将 number 4 放入队列
number 5,抛弃 number 4,将 number 5 放入队列
number 6,抛弃 number 5,将 number 6 放入队列
执行 number 6
5.5 自定义拒绝策略
当然,如果上面的拒绝策略都不满足的话,我们也可以定义拒绝策略。
public class MyRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(executor.toString());
}
}
执行结果:
run pool-1-thread-2, number = 1
run pool-1-thread-3, number = 3
run pool-1-thread-1, number = 0
java.util.concurrent.ThreadPoolExecutor@1d44bcfa[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 0]
java.util.concurrent.ThreadPoolExecutor@1d44bcfa[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 0]
java.util.concurrent.ThreadPoolExecutor@1d44bcfa[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 0]
run pool-1-thread-2, number = 2
复制代码
6. 常见的线程池有哪些?各自有什么特点?
JDK 自带了几个常见的线程池。
6.1 FixedThreadPool
FixedThreadPool 被称为可重用固定线程数的线程池。创建的源码如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
复制代码
可以看到 FixedThreadPool 的的核心线程数和最大线程数都是传入的参数。使用的队列是 LinkedBlockingQueue 。由于 LinkedBlockingQueue 是一个无界队列(队列的容量为 Intger.MAX_VALUE),所以运行中的 FixedThreadPool 不会拒绝任务,所以当任务过多的时候可能会造成 OOM 。
6.2 SingleThreadExecutor
SingleThreadExecutor 是只有一个线程的线程池。创建源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
复制代码
SingleThreadExecutor 的核心线程数和最大线程数都为 1,所以这个线程池只有一个线程。使用的队列也是 LinkedBlockingQueue ,所以当任务过多时也会存在 OOM 的问题。
6.3 CachedThreadPool
CachedThreadPool 无固定大小的线程池,随着任务的不断提交,创建新的线程来执行。创建源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
复制代码
CachedThreadPool 的核心线程数为 0 ,最大线程数是 Integer.MAX_VALUE,可见所有的线程都是非核心线程。如果线程池的线程 60 秒,没有执行任务则会被销毁。由于使用了 SynchronousQueue,所以当主线程通过 SynchronousQueue.offer(Runnable task) 提交任务到队列后会阻塞,如果线程池中有可用的线程,则会执行当前任务,如果没有则会创建一个新的线程来执任务。
可见,如果任务太多的话,依然会造成 OOM ,与 LinkedBlockingQueue 不同的是,LinkedBlockingQueue 是由于任务对象太多,导致 OOM,ThreadPoolExecutor 则是由于 线程数太多导致 OOM 。
如何获取线程池中的返回结果?
可以使用 executor.submit(futureTask);
,提交一个 FutureTask。代码如下:
自定义的线程
public class ThreadPoolCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Random random = new Random();
int number = random.nextInt(100);
System.out.println(Thread.currentThread().getName() + ":" + number);
return number;
}
}
使用:
public static void func5() throws ExecutionException, InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 10,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.CallerRunsPolicy());
int result = 0;
for (int i = 0; i < 7; i++) {
Callable<Integer> callable = new ThreadPoolCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
executor.submit(futureTask);
result += futureTask.get();
}
executor.shutdown();
System.out.println("result = " + result);
}
执行结果:
pool-1-thread-1:41
pool-1-thread-2:83
pool-1-thread-1:15
pool-1-thread-2:21
pool-1-thread-1:26
pool-1-thread-2:38
pool-1-thread-1:98
result = 322
复制代码
评论