实现多线程(并发)有几种实现方式?
很多人都知道有两种:
1.继承Thread重写run()方法
public class Thread01 extends Thread {
@Override
public void run() {
System.out.println("线程执行了。。。");
}
public static void main(String[] args) throws InterruptedException {
new Thread01().start();
}
}
2.实现Runable接口,实现run()方法
public class Runnable01 implements Runnable {
public void run() {
System.out.println("线程执行了。。。");
}
public static void main(String[] args) {
new Thread(new Runnable01()).start();
}
}
其实还有一种方式:
3.实现Callable接口,实现call()方法,call()方法将作为线程执行体,并且有返回值
public class Callable01 implements Callable<String> {
public String call() throws Exception {
return "线程执行了。。。";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask task = new FutureTask(new Callable01());
new Thread(task).start();
System.out.println(task.get());
}
}
注意:因为java语言是单继承多实现的,所以推荐使用实现Runable接口方式来实现并发。
Runnable和Callable的区别:
(1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
(3) call方法可以抛出异常,run方法不可以。
(4) 运行Callable任务可以拿到一个Future对象,通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果future.get()。
.run()和.start()方法有什么区别?
1.new Thread01().run()方法其实是方法级别的调用,根本不是启动线程
2.new Thread01().start()通过start方法启动线程,start才是开启一条线程,底层是一个native
方法
什么是线程池?
简单理解下:创建线程要花费很多资源和时间,如果任务来了才创建线程,那么响应的时间就会变慢,而且一个进程创建线程数是有限的。为了避免这些问题,在程序启动的时候就创建若干个线程来响应处理,它们就被成为线程池,池子里就是工作线程。从JDK1.5开始,JavaAPI提供了Executor框架让我们创建不同的线程池。
使用线程池的优点:
1.避免线程的创建和销毁带来的性能开销
2.避免大量的线程间因互相抢占系统资源导致的阻塞现象
3.能够对线程进行简单的管理并提供定时执行,间隔执行等功能
四种方式
四种方式其实就是创建线程池的四种方式:
1.单线程化的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
2.固定线程数的线程池
ExecutorService executorService = Executors.newFixedThreadPool(int size);
3.按需分配,具有伸缩性的线程池
ExecutorService executorService = Executors.newCachedThreadPool();
4.固定长度线程池,定时定期执行任务
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(int corePoolSize);
newScheduledThreadPool其实底层是返回了一个ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor它继承了ThreadPoolExecutor
最终其实还是调用父类ThreadPoolExecutor的构造方法
七大参数
注意到没有?上边四种创建方式其实本质上都是ThreadPoolExecutor()
来创建的,无非就是传参的不同而已。
ThreadPoolExecutor executor = new ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
参数:
corePoolSize:
核心线程数量
maximumPoolSize:
*线程池中创建的最大线程数;
keepAliveTime:
超时时间,超出核心线程数量以外的线程空余存活时间, 超时了没有人调用就会释放
unit:
TimeUnit枚举类型的值,代表存活的时间单位,可以取下列值:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue:
阻塞队列,用来存储等待执行的任务,决定了线程池的排队策略
threadFactory:
线程工厂,是用来创建新线程使用的工厂。默认Executors.defaultThreadFactory();
handler:
线程拒绝策略。当创建的线程数量已经到达最大线程数maximumPoolSize,且缓冲队列已满时,新任务会被拒绝。
四种拒绝策略
有以下取值:
new ThreadPoolExecutor.AbortPolicy():
丢弃任务并抛出RejectedExecutionException异常。 (默认的拒绝策略)
new ThreadPoolExecutor.DiscardPolicy()
:也是丢弃任务,但是不抛出异常。
new ThreadPoolExecutor.DiscardOldestPolicy():
队列满了,尝试去和最早的竞争,也不会 抛出异常,然后重新尝试执行任务(重复此过程)
new ThreadPoolExecutor.CallerRunsPolicy():
哪来的去哪里执行,由调用线程处理该任务
那么使用哪种创建方式呢?阿里的开发手册中有说:
所以说,最好使用ThreadPoolExecutor()来创建线程池,参数我们可以根据需求自定义。
那么池的最大的大小maximumPoolSize如何去设置?
那就需要了解下:CPU密集型
和IO密集型
CPU 密集型:判断服务器是几核的电脑,maximumPoolSize就设置为几,可以保持CPU的效率最高
IO密集型:判断你程序中十分耗IO的线程有几条,那maximumPoolSize就设置的比它大。
代码获取cpu核数:Runtime.getRuntime().availableProcessors()
线程池执行的方式
两种方式:
1.executorService.submit()
public class ThreadPools {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(new Runnable() {
public void run() {
System.out.println("使用submit,线程池启动了。。。");
}
});
executorService.shutdown();
}
}
2.executorService.execute()
public class ThreadPools {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new Runnable() {
public void run() {
System.out.println("使用execute,线程池启动了。。。");
}
});
executorService.shutdown();
}
}
补充一点:
其实submit()底层调用的还是execute()
看源码:AbstractExecutorService实现ExecutorService接口
既然两种执行方式都是调的execute(),那么他们肯定是有区别的:submit()方法是有返回值的,而execute()方法没返回值。看上图源码中,submit()方法最后return了一个ftask。
线程池的关闭方式
executorService.shutdown()
1、调用之后不允许继续往线程池内继续添加线程;
2、线程池的状态变为SHUTDOWN状态;
3、所有在调用shutdown()方法之前提交到ExecutorSrvice的任务都会执行;
4、一旦所有线程结束执行当前任务,ExecutorService才会真正关闭。
通俗点讲就是:不会立刻关闭线程池,也不能提交新的任务线程了,而是等队列中的任务依次执行完,队列空了没任务了才关闭。
public class ThreadPools {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new Runnable() {
public void run() {
System.out.println("使用execute,线程池启动了。。。");
}
});
executorService.shutdown();
}
}
executorService.shutdownNow()
1、该方法返回尚未执行的 任务task 的 List;
2、线程池的状态变为STOP状态;
3、阻止所有正在等待启动的任务, 并且停止当前正在执行的任务。
通俗点讲就是:立刻关闭线程池,停止当前正在执行的 task,并返回尚未执行的 task 的 list
public class ThreadPools {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new Runnable() {
public void run() {
System.out.println("使用execute,线程池启动了。。。");
}
});
List<Runnable> runnables = executorService.shutdownNow();
}
}
注意:以上两段代码的关闭方式是有细节上的问题滴。因为,使用shutdownNow方法,可能会引起报错,使用shutdown方法如果遇到一直处于阻塞状态的线程时可能会导致线程池关闭不了。所以呢?如何优雅并安全关闭线程池呢?
优雅并安全的关闭线程池-Demo:
public class ThreadPools {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new Runnable() {
public void run() {
long num = 0;
boolean flag = true;
while (flag && !Thread.currentThread().isInterrupted()) {
num += 1;
if (num == Long.MAX_VALUE) {
flag = false;
}
}
System.out.println(num);
}
});
executorService.shutdown();
try {
if (!executorService.awaitTermination(2, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
}
}
解析:先调用shutdown()
使线程池状态改变为SHUTDOWN
,线程池不允许继续添加线程,并且等待正在执行的线程执行完毕。
调用awaitTermination
设置定时任务,代码意思为 2s 后检测线程池内的线程是否全部执行完毕(就像是告诉它,“最后给你 2s 时间赶紧执行啊”),若没有执行完毕,则调用shutdownNow()
方法。