本文简短总结一下java里面多线程的实现(重点会讲一下线程池 实际工作使用最多的)。
简单介绍一下概念:
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
线程的运行状态 有 :创建、就绪、运行、阻塞、终止。
如图:
简单对每个状态解释一下:
创建:New 就是新建了一个线程对象
就绪:Runnable 线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程变得可运行,等待获取CPU的使用权。
运行:Running 就绪状态的线程获得了cpu的使用权开始执行代码。
阻塞:Blocked 由于一些原因导致线程放弃了cpu的使用权。此时的线程只有进入到就绪态才有机会进行下一轮cpu资源的抢占。
阻塞有分3种阻塞情况:
等待阻塞:对运行线程执行wait()操作 此时线程会放弃资源。
同步阻塞:运行线程想要获取同步锁时,当前锁资源被其他线程占有
其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,需要注意的是 sleep()并不会放弃线程拥有的资源只是让出cpu执行权
终止:线程执行完成退出或者遇到异常退出。
那么在java中具体实现多线程有4中(有人说是3种)继承Thread类,实现Runable接口,线程池,实现Callable接口。
有人把实现Runable接口和实现Callable接口归结成一种那么就是3种:那么我们首先来讨论一下实现Runable接口和实现Callable接口区别;
我们先来分别定义两个接口的实现类:
1:Runnable实现
public class RunableTest implements Runnable {
@Override
public void run() {
System.out.println("这是实现Runnable接口 ");
}
public static void main(String []ages){
Thread thread= new Thread(new RunableTest());
thread.start();
}
}
2:Callable的实现
public class CallableTest implements Callable {
@Override
public Object call() throws Exception {
System.out.print("这是实现Callable接口");
return "111212";
}
public static void main(String []ages){
FutureTask<Integer> oneTask = new FutureTask<>(new CallableTest());
Thread thread= new Thread(oneTask );
thread.start();
}
}
从上面的代码上来看确实没什么大的区别,但是具体细看你会发现 实现Callable接口重写 call方法是竟然是个有返回值的方法,而实现Runnable接口的方法重写run方法是是一个返回viod的方法。对这就是他俩的最大区别 一个有返回值一个没有返回值(其实工作中我们很少用到这个返回值)
3:在来看 实现Thread类来实现多线程
public class ThreadTest extends Thread {
@Override
public void run() {
System.out.println("这是实现Thread实现多线程");
}
public static void main(String [] ages){
ThreadTest threadTest = new ThreadTest();
threadTest.start();
}
}
仔细看一下上面的多线程代码实现发现其实难的。(其实我也是这样觉得的嘻嘻…)接下来看一下 线程到底是怎么启动的。
我们打开start源码:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0) //备注1:判断当前要启动线程状态 不为0(0:NEW 新建状态) 就抛一个异常
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this); //添加进线程组 (这里也是线程安全的方法可以自己跟踪进去看源码)
boolean started = false;
try {
start0();//调用native方法(执行线程run方法)
started = true; //设置线程启动为true
} finally {
try {
if (!started) {
//启动失败 则在线程组中移除当前线程。
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
首先 我们能看到的的这是个线程安全的方法。之后看完上面的代码其实没发现并没有显示调用我们线程的具体实现类仅仅是将我们的线程加入线程组,在调用本地native方法要去执行这个线程。因此我们可以简单总结
1:我门不能对同一个线程类对象调用两次start()方法。会在备注1的位置报错。
2:当我们用start()启动一个线程的时候线程并不会直接执行。而是使得该线程变为可运行态(Runnable),具体什么时候运行是由操作系统决定的。
看看了线程的执行过程及实现,其实java还允许我们为线程指定优先级:
static int MAX_PRIORITY 线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY 线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY 分配给线程的默认优先级,取值为5。
其实在项目中我们实现多线程的时候很少看见上面的3中方式实现。原因有一下几点(个人总结)
1:实现Thread类来实现多线程这里其实破坏了java的扩展体系。因为java是单继承。
2:实现Callable接口或者实现Runnable接口虽然没有破坏扩展性且是通过接口实现,但是手动的开启多线程这种情况下不太适合大范围使用因为开启线程和关闭线程都浪费资源。
那么我们来java有没有给我们提供一个更好的技术呢?显然是有的 那就是池化技术的概念。
那么接下来我们来讨论一下线程池:
首先老规矩先解释一下什么是线程池:简单来讲就是事先固定维护一定数量的线程在需要的时候直接去取用完之后释放掉即可。至于线程在线程池里面怎么样那是由线程池来完成。这大大的简化了我们创建线程启动线程的麻烦。
1: 线程池也是有状态的如图(我在网上找的图勉强看一下):
解释一下这几个状态:
RUNNING:运行状态,该状态下线程池可以接受新的任务,也可以处理阻塞队列中的任务执行 shutdown 方法可进入 SHUTDOWN 状态执行 shutdownNow 方法可进入 STOP 状态
SHUTDOWN:待关闭状态,不再接受新的任务,继续处理阻塞队列中的任务当阻塞队列中的任务为空,并且工作线程数为0时,进入 TIDYING 状态
STOP:停止状态,不接收新任务,也不处理阻塞队列中的任务,并且会尝试结束执行中的任务当工作线程数为0时,进入 TIDYING 状态
TIDYING:整理状态,此时任务都已经执行完毕,并且也没有工作线程执行 terminated 方法后进入 TERMINATED 状态
TERMINATED:终止状态,此时线程池完全终止了,并完成了所有资源的释放
2:我们打开源码开一下看一下构建一个线程池需要哪些参数:
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime,//保持空闲时间
TimeUnit unit, //参数的时间单位
BlockingQueue<Runnable> workQueue,//用于在任务之前保存它们的队列
ThreadFactory threadFactory,//创建线程的工厂
RejectedExecutionHandler handler) //拒绝策略
{
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
接下来我们来结合线程池的核心参数来讲解
corePoolSize:
简单来讲工作线程的个数可能从0到最大线程数之间变化,当执行一段时间之后可能维持在 corePoolSize,但也不是绝对的,取决于核心线程是否允许被超时回收。
maximumPoolSize:
当核心线程数处理不完任务时那么就可能需要增加线程个数来处理任务(注意这里说的是可能后面会讲到为什么是可能),但是也不能无限增长所以当线程数到达maximumPoolSize就不在增长。
keepAliveTime:线程保持最大空闲时间
threadFactory:
看到这个参数很容易让我们想到BeanFactory其实没错就是你想的那样。线程就像bean一样也需要创建所以ThreadFactory就是帮我们创建线程德的。
handler:
拒接策略就是当线程来不及处理时怎么处理当前任务的(其实命名上来看这是个Execution)。
workQueue:
上面我们说了核心线程数和最大线程数,并且也介绍了工作线程的个数是在0和最大线程数之间变化的。但是不是一下子就创建了所有线程(就像你们项目组招人一样不可能一下子招到所有人需要一个过程),
把线程池装满,而是有一个过程,workQueue(阻塞队列)就在这时用上了,这个过程是这样的:
1:首先当前线程池接收到一个新的任务时首选去看线程池里面线程有没有达到corePoolSize没有到达直接新建一个线程来处理当前任务(切记当未达到corePoolSize时线程是不会重用之前的线程的).
当达到corePoolSize时这时候workQueue就配上用场了会把当前任务放到队列里面缓存一下。这里并不去看有没有达到最大线程数在创建新的线程。而是把当前任务放到队列里面因为之前的核心线程有可能已经有空闲的或者即将空闲的(谁让是核心呢 多做点事 …)。
2: 什么时候才会去创建新的线程呢,首先还要看workQueue采用哪种队列:
无界的队列:其实这个时候永远不会创新新的线程来执行任务不管核心线程有多忙。也就是说最大线程数maximumPoolSize失去了意义(所以上面我们提到是有可能创建新的线程)。
有界的队列:这是时候当前队列里面的任务已经满的时候在有新的任务来就会触发新的线程产生。
(其实这个时候还是看是不是公平策略如果是大家可能按照先进先出的特性来执行,如果不是那么就会出现强占)
3:线程是怎么被回收的
这个时候我们要看keepAliveTime,unit这两个参数,当线程池线程有空闲的时候且超时的时候会被回收掉,那么线程池是如何辨别那个是核心线程那个是非核心线程呢,答案是不能确定,
这个和当前线程池线程数量有关,当线程池线程小于corePoolSize数时大家都是核心线程,当线程数大于corePoolSize时 大家都是‘非核心线程’所以都有可能会被回收,
所以 每个线程想要保住自己“核心线程”的身份,必须充分努力,尽可能快的获取到任务去执行,这样才能逃避被回收的命运。
取任务的方法有两种,一种是通过 take() 方法一直阻塞直到取出任务,另一种是通过 poll(keepAliveTime,timeUnit) 方法在一定时间内取出任务或者超时,如果超时这个线程就会被回收,请注意核心线程一般不会被回收。
但是这个这个核心线程不是绝对不会被回收 如果我们设置了允许核心线程超时被回收的话,那么就没有核心线程这种说法了,所有的线程都会通过 poll(keepAliveTime, timeUnit) 来获取任务,一旦超时获取不到任务,就会被回收,只是一般很少会这样来使用。
4:那么当队列存满了且已经到到达了最大线程数会怎么样。这个时候我们的拒绝策略就上场了
线程池中还为我们提供了以下几个拒绝策略:
抛出异常(线程池默认的拒绝策略)
直接丢弃该任务
使用调用者线程执行该任务
弃任务队列中的最老的一个任务,然后提交该任务。
5:那么总结一下线程池的工作步骤(当提交一个新的任务时):
当工作线程数小于核心线程数时,直接创建新的核心工作线程
当工作线程数不小于核心线程数时,就需要尝试将任务添加到阻塞队列中去
如果能够加入成功,说明队列还没有满,那么需要做以下的二次验证来保证添加进去的任务能够成功被执行
验证当前线程池的运行状态,如果是非RUNNING状态,则需要将任务从阻塞队列中移除,然后拒绝该任务
验证当前线程池中的工作线程的个数,如果为0,则需要主动添加一个空工作线程来执行刚刚添加到阻塞队列中的任务
如果加入失败,则说明队列已经满了,那么这时就需要创建新的“临时”工作线程来执行任务
如果创建成功,则直接执行该任务
如果创建失败,则说明工作线程数已经等于最大线程数了,则只能拒绝该任务了
扩展一下java直接给我们提供很多成型的线程池对象基本可以满足我们的业务需求下面列举几个不在展开讨论:
newSingleThreadExecutor:
创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool(1)不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。简单理解就是在同一时间只能有一个线程在执行,即线程有序执行。
newFixedThreadPool:
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
newCachedThreadPool:
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。简单讲就是已经有线程存在且可能用的时候就会重复使用当前线程不会创建新的线程。
newScheduledThreadPool:
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
写在最后:大家没啥事都洗洗散了吧,这个博客应该很久不会再更新了。附上一句我喜欢的词 (人生若只如初见,何事秋风悲画扇。)