实现多线程(并发)有几种实现方式?
很多人都知道有两种:
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接口方式来实现并发。为啥不用实现Callable接口的方式呢,个人理解是因为用的人少吧,毕竟写起来太麻烦啦。
##### 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()方法其实是方法级别的调用,根本不是启动线程
**注意:**看这个demo,debug运行查看只有一个‘main@1’这条主线程在执行。
2.new Thread01().start()通过start方法启动线程
**注意:**看这个demo,debug运行时是不是有两条线程在执行‘main@1’和‘Thread-0@663’。
什么是线程池?
简单理解下:创建线程要花费很多资源和时间,如果任务来了才创建线程,那么响应的时间就会变慢,而且一个进程创建线程数是有限的。为了避免这些问题,在程序启动的时候就创建若干个线程来响应处理,它们就被成为线程池,池子里就是工作线程。从JDK1.5开始,JavaAPI提供了Executor框架让我们创建不同的线程池。
线程池的优点
1.避免线程的创建和销毁带来的性能开销
2.避免大量的线程间因互相抢占系统资源导致的阻塞现象
3.能够对线程进行简单的管理并提供定时执行,间隔执行等功能
创建线程池的方式
1.ExecutorService executorService = Executors.newFixedThreadPool(int size); //启动固定线程数的线程池
2.ExecutorService executorService = Executors.newCachedThreadPool(); //按需分配的线程池
3.ScheduledExecutorService executorService = Executors.newScheduledThreadPool(int corePoolSize);//定时,定期执行任务的线程池
4.ExecutorService executorService = Executors.newSingleThreadExecutor();//单线程化的线程池
## 注意:
四种创建方式底层其实都是new ThreadPoolExecutor()来创建的,无非就是传参的不同而已
ThreadPoolExecutor executor = new ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
所以说加上ThreadPoolExecutor executor = new ThreadPoolExecutor(参数);就是五种创建线程池的方式了。
参数:
*corePoolSize:*线程池的大小。线程池创建之后不会立即去创建线程,而是等待线程的到来。当当前执行的线程数大于改值是,线程会加入到缓冲队列;
*maximumPoolSize:*线程池中创建的最大线程数;
*keepAliveTime:*空闲的线程多久时间后被销毁。默认情况下,改值在线程数大于corePoolSize时,对超出corePoolSize值得这些线程起作用。
*unit:*TimeUnit枚举类型的值,代表keepAliveTime时间单位,可以取下列值:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
*workQueue:*阻塞队列,用来存储等待执行的任务,决定了线程池的排队策略,有以下取值:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
*threadFactory:*线程工厂,是用来创建线程的。默认Executors.defaultThreadFactory();
*handler:*线程拒绝策略。当创建的线程超出maximumPoolSize,且缓冲队列已满时,新任务会拒绝,有以下取值:
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 (默认的拒绝策略)
DiscardPolicy:也是丢弃任务,但是不抛出异常。
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
CallerRunsPolicy:由调用线程处理该任务
看源码:
newFixedThreadPool(int size):
newSingleThreadExecutor():
newCachedThreadPool():
newScheduledThreadPool(int corePoolSize):new了一个ScheduledThreadPoolExecutor,ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,然后调用父类的构造方法
线程池执行的方式
两种方式:
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()方法。
线程池的架构
回到创建线程池的方式看一眼,你品,你细品。Executors是啥?其实就是一个工具类,帮你创建不同的线程池的。
篇幅太长了,就先到这吧,随后会整理一篇关于线程池运行原理的文章。