为什么要使用多线程
1.提高程序响应效率:用户通过前端页面进行请求,后台响应时间比较慢时,可以将后台程序写成多线程,主线程执行完,响应用户,子线程在后台继续执行直到结束。可以避免长时间无响应的尴尬。
2.并行操作使用线程:如C/S架构的服务器端可以并发响应用户需求。
3.在多CPU系统中,使用线程提高CPU利用率。
4.改善程序结构,将一个既长又复杂的进程分为多个线程,成为几个独立或半独立的部分运行。
相比于进程,线程切换更快,花销更小,线程之前共享数据空间,通信更加方便
多线程有哪些缺点
1.线程数量越多,系统切换上下文时的消耗越大,会影响性能。
2.线程之间共享的变量要考虑锁的问题,容易产生死锁的情况。
3.线程越多,就需要更多地内存空间。
多线程在CPU密集型的作业下的确不能提高性能甚至更浪费时间,但是在IO密集型的作业下则可以提升性能(或者更准确点说叫平均响应时间) --引自知乎
JAVA多线程的实现
java多线程的实现方式主要有三种:继承thread类,实现Runnable接口,实现Callable接口,前两种实现方式当线程执行结束后没有返回值,最后一种实现方式可以有返回值。
下面分别讲这三种方式的实现
1.继承thread类
thread类在本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,具体实现如下
public Class ThreadTest extends Thread{
private static Integer size = 0;
@Override
public void run(){
System.out.prientln("size is "+size);
}
}
上面的ThreadTest类实现了多线程,在合适的地方用start()方法启动
public class Main{
public static void main(String[] args){
ThreadTest test1 = new ThreadTest();
ThreadTest test2 = new ThreadTest();
test1.start();
test2.start()
}
}
在main方法中创建了两个线程test1,test2,并用start() 方法启动
2.实现Runnable接口
如果你的类已经extends了一个类,那就没有办法再extends Thread类了,此时,就可以实现Runnable接口,实现如下:
public class ThreadTest1 implements Runnable{
private static Integer size = 0;
@Override
public void run(){
System.out.prientln("size is "+size);
}
}
启动实现Runnable的多线程类的时候,首先需要实例化一个Thread,然后传入自己的
ThreadTest1 的实例,实现如下:
public class Main{
public static void main(String[] args){
ThreadTest1 test1 = new ThreadTest1();//创建ThreadTest1 实例
ThreadTest1 test2 = new ThreadTest1 ();
Thread thread1= new Thread(test1);//实例化Thread实例,并传入ThreadTest1 实例test1
Thread thread2 = new Thread(test2);
thread1.start();//启动线程
threat2.start();
}
}
3.实现Callable<Object>接口
如果需要实现由返回结果的线程,可以使用ExecutorService、Callable、Future
实现如下
public class ThreadTest2 implements Callable<Object>{
private Integer size ;
ThreadTest2(Integer size){
this.size = size;
}
@Override
public Object call(){
return "size is "+size;
}
}
public class Main{
public static void main(String[] args){
ExecutorService pool = Executors.newFixedThreadPool(1);//初始化线程池
ThreadTest2 test = new ThreadTest2 (1);//创建线程实例
Future f = pool.submit(test);//执行该线程,并获取Future对象
String s = f.get().toString;//future.get()获取返回值
}
}
Future的用法有很多(用来处理线程执行时的状态,可以取消,可以获取执行的返回值)
V get() :获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。
V get(Long timeout , TimeUnit unit) :获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。
boolean isDone() :如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。
boolean isCanceller() :如果任务完成前被取消,则返回true。
boolean cancel(boolean mayInterruptRunning) :如果任务还没开始,执行cancel(…)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(…)方法将返回false。mayInterruptRunning参数表示是否中断执行中的线程。
通过方法分析我们也知道实际上Future提供了3种功能:
(1)能够中断执行中的任务
(2)判断任务是否执行完成
(3)获取任务执行完成后额结果。
线程池详解
以上就是多线程实现常用的三种方式,合理地使用多线程提高了程序执行的效率,与用户的交互更加友好,为了更好地使用多线程,引入了线程池,下面对线程池进行了详解,在上面实现callable接口的多线程中就已经使用了线程池。
在学习线程池之前,先介绍一下为什么要使用线程池:
在Java中,如果每当一个请求到达就创建一个新线程,开销是相当大的。在实际使用中,每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源,甚至可能要比花在处理实际的用户请求的时间和资源要多得多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个JVM里创建太多的线程,可能会导致系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务,这就是“池化资源”技术产生的原因。
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。另外,通过适当地调整线程池中的线程数目可以防止出现资源不足的情况。
使用线程池的风险
虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。
下面就对线程池进行详解
java线程池中最核心的就是ThreadPoolExecutor类,我们就从这个类讲起。ThreadPoolExecutor有四个构造方法:
public class ThreadPoolExecutor extends AbstractExecutorService {
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
}
ThreadPoolExecutor 继承了AbstractExecutorService 类,并且提供了四个构造方法,看过这四个构造方法的源码会发现,前三个构造方法最终调用了最后一个构造方法进行初始化的,下面解释一下构造器中各个参数的含义:
- corePoolSize:核心池的大小,默认情况下,在创建线程池后,线程池内并没有任何线程,当有任务到来时,才会创建线程执行任务,除非调用prestartAllCoreThreads()或者prestartCoreThread()方法,这两个方法是预创建线程,即在任务没到来之前就创建corePoolSize 个或1个线程。当默认时,有任务到来,就创建线程,如果当前线程数达到corePoolSize 时,就会把新来的任务放到缓存队列中。
- maximumPoolSize: 线程池最大线程数,顾名思义,就是线程池最多能创建多少个线程。
- keepAliveTime:线程没有任务执行多长时间后会终止。就是当线程池内的数目大于corePoolSize时,如果一个线程的空闲时间大于keepAliveTime时,就会被终止,直到线程池内的线程数小于等于corePoolSize。默认情况下,只有在任务数大于corePoolSize时,keepAliveTime才会起作用,直到线程池内的线程数不大于corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit:参数keepAliveTime的时间单位,取值有7种,在TimeUtil类中
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
- workQueue:阻塞队列,用来存储等待执行的任务的队列,一般阻塞队列有以下几种选择
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
一般使用LinkedBlockingQueue和SynchronousQueue。
- threadFactory :线程工厂,主要用来创建线程。
- handler:表示拒绝处理任务时的策略,有以下四种取值
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
这里说一下ThreadPoolExecutor与其父类,以及父类实现的类的关系
Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;
然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;
抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;
然后ThreadPoolExecutor继承了类AbstractExecutorService。
在ThreadPoolExecutor类中有几个非常重要的方法:
execute()
submit()
shutdown()
shutdownNow()
其中execute()是顶层类Executor声明的一个方法,用来向线程池中提交任务,
submit()是ExecutorService中声明的方法,在AbstractExecutorService中具体实现,这个方法也是向线程池中提交任务的,与execute()不同的是,submit()方法能够返回任务执行的结果,submit()的实现上还是调用了execute()方法,它利用了Future来获取任务执行的结果。
shutdown() shutdownNow()是用来关闭线程池的。
还有很多其他的方法:比如,getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法,有兴趣的朋友可以自行查阅API。
线程池实现原理
线程池的运行状态
线程池一共有五种状态, 分别是:
RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务;
SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用shutdown()方法进入该状态);
STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态;
TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。
进入TERMINATED的条件如下:
- 线程池不是RUNNING状态;
- 线程池状态不是TIDYING状态或TERMINATED状态;
- 如果线程池状态是SHUTDOWN并且workerQueue为空;
- workerCount为0;
- 设置TIDYING状态成功。
状态转换图如图所示:

未完待续
本文详细介绍了多线程的优缺点,包括提高程序响应效率、并行操作及CPU利用率,同时也指出了线程数量过多可能导致的问题。文章深入探讨了Java中实现多线程的三种方法:继承Thread类、实现Runnable接口和Callable接口。此外,还全面解析了线程池的概念、工作原理及其在多线程应用中的重要性,包括线程池的运行状态和线程池实现类ThreadPoolExecutor的构造方法。

被折叠的 条评论
为什么被折叠?



