东西比较多,做个笔记。方便日后查阅学习。
一、进程和线程
进程
进程是系统进行资源分配和调度的基本单位,各个进程之间不会相互影响,因为系统给它们分配了不同的空间和资源,它分为单进程和多进程。可以将进程理解为一个正在执行的程序,比如一款游戏。
单进程与多进程的概述
单进程的计算机一次只能做一件事情,而多进程的计算机可以做到一次做不同的事情,比如一边听音乐,一边听打游戏,这两件事情虽然感觉起来是在同时一起进行的,但其实是CPU在做着程序间的高效切换,这才让我们觉得是同时进行的。
线程
线程是程序执行的最小单位,一个进程可由一个或多个线程组成,在一款运行的游戏中通常会有界面。线程就是程序执行的任务,它是程序使用CPU的基本单位,因此也可以说线程是依赖于进程的。
更新线程、游戏逻辑线程等,线程切换的开销远小于进程切换的开销。
单线程与多线程的概述
单线程也就是做的事情专一,不会分神去做别的事,也就是程序只有一条执行路径;多线程就是可以分出多条路去做同一件事情,也就是程序有多条执行路径,比如三个伙伴迷路了,大家分别去问路人路线,最后大家在目的地集合,因此多线程的存在,不是提高程序的执行速度,其实是为了提高应用程序的使用率,也可以说程序的执行其实都是在抢CPU的资源,也就是抢CPU的执行权,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权,但这一过程是随机的,不知道哪一个线程会在哪一个时刻占到这个资源,所以线程的执行有随机性。
蓝色框表示进程,黄色框表示线程。进程拥有代码、数据等资源,这些资源是共享的,3个线程都可
以访问,同时每个线程又拥有私有的栈空间。
二、线程的状态
线程的五种状态:
1)新建状态(New):线程对象实例化后就进入了新建状态。
2)就绪状态(Runnable):线程对象实例化后,其他线程调用了该对象的start()方法,虚拟机便会启动该线程,处于就绪状态的线程随时可能被调度执行。
ps:处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
3)运行状态(Running):线程获得了时间片,开始执行。只能从就绪状态进入运行状态。
它可以变成阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。
注: 当发生如下情况是,线程会从运行状态变为阻塞状态:
①、线程调用sleep方法主动放弃所占用的系统资源
②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
④、线程在等待某个通知(notify)
⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。
当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。
4)阻塞状态(Blocked):线程因为某个原因暂停执行,并让出CPU的使用权后便进入了阻塞状态。
ps: 在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。有三种方法可以暂停Threads执行:
等待阻塞:调用运行线程的wait()方法,虚拟机会把该线程放入等待池。
同步阻塞:运行线程获取对象的同步锁时,该锁已被其他线程获得,虚拟机会把该线程放入锁定池。
其他线程:调用运行线程的sleep()方法或join()方法,或线程发出I/O请求时,进入阻塞状态。
5)结束状态(Dead):线程正常执行完或异常退出时,进入了结束状态。
ps: 这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
三、使用线程
实现Runnable接口
通过实现Runnable接口创建线程类的具体步骤和具体代码如下:
• 定义Runnable接口的实现类,并重写该接口的run()方法;
• 创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。
1 | public class ThreadTest { |
实现Callable 接口
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
通过Callable和Future创建线程的具体步骤和具体代码如下:
• 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
• 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
• 使用FutureTask对象作为Thread对象的target创建并启动新线程。
• 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值其中,Callable接口(也只有一个方法)定义如下:
Callable接口(也只有一个方法)定义如下:
1 | public interface Callable { |
1 | public class SomeCallable<V> extends OtherClass implements Callable<V> { |
1 | Callable<V> oneCallable = new SomeCallable<V>(); |
继承Thread类
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。
任何线程只能启动一次,然后多次调用。
1 | public class ThreadTest { |
Thread类实现了Runnable接口,在Thread类中,有一些比较关键的属性,比如name是表示Thread的名字,可以通过Thread类的构造器中的参数来指定线程名字,priority表示线程的优先级(最大值为10,最小值为1,默认值为5),daemon表示线程是否是守护线程,target表示要执行的任务。
以下是关系到线程运行状态的几个方法:
1)start方法
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
2)run方法
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。所以,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
3)sleep方法
sleep方法有两个重载版本:
1 | sleep(long millis) //参数为毫秒 |
|
---|---|---|
2 | sleep( long``millis, int nanoseconds) //第一参数为毫秒,第二个参数为纳秒 |
sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。
当当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间内,该线程不会获得执行机会,即使系统中没有其他可执行线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
1 | public class Test { |
输出结果:
注:
(1)sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。如下面的例子:
1 | public class Test1 { |
(2)Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。
4)yield方法
yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入到就绪状态。即让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,当某个线程调用了yield()方法之后,只有优先级与当前线程相同或者比当前线程更高的处于就绪状态的线程才会获得执行机会。
注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
yield方法直接回到了就绪状态,是没有缓冲的阻塞的。
5)join方法
join方法有三个重载版本:
1 | join() |
---|---|
2 | join(long millis) //参数为毫秒 |
3 | join(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒 |
假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的事件。
实际上调用join方法是调用了Object的wait方法,这个可以通过查看源码得知:
wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。
由于wait方法会让线程释放对象锁,所以join方法同样会让线程释放对一个对象持有的锁。
6)wait方法
wait() 方法需要和 notify() 及 notifyAll() 两个方法一起介绍,这三个方法用于协调多个线程对共享数据的存取,所以必须在 synchronized 语句块内使用,也就是说,调用 wait(),notify() 和 notifyAll() 的任务在调用这些方法前必须拥有对象的锁。
注意,它们都是 Object 类的方法,而不是 Thread 类的方法。
wait() 方法与 sleep() 方法的不同之处在于,wait() 方法会释放对象的“锁标志”。当调用某一对象的 wait() 方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了 notify() 方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的 notifyAll() 方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。
除了使用 notify() 和 notifyAll() 方法,还可以使用带毫秒参数的 wait(long timeout) 方法,效果是在延迟 timeout 毫秒后,被暂停的线程将被恢复到锁标志等待池。
此外,wait(),notify() 及 notifyAll() 只能在 synchronized 语句中使用,但是如果使用的是 ReenTrantLock 实现同步,该如何达到这三个方法的效果呢?解决方法是使用 ReenTrantLock.newCondition() 获取一个 Condition 类对象,然后 Condition 的 await(),signal() 以及 signalAll() 分别对应上面的三个方法。
sleep、join、yeild方法之间的区别
sleep方法是一个静态方法,让当前正在执行的线程休眠(暂停执行),而且在睡眠的过程是不释放资源的,保持着锁。该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是 sleep() 方法不会释放“锁标志”,也就是说如果有 synchronized 同步块,其他线程仍然不能访问共享数据。
作用:
1、暂停当前线程一段时间;
2、让出CPU,特别是不想让高优先级的线程让出CPU给低优先级的线程
yeild方法同样也是一个静态方法,暂停当前正在执行的线程,线程由运行中状态进入就绪状态,重新与其他线程一起参与线程的调度。yield() 方法和 sleep() 方法类似,也不会释放“锁标志”,区别在于,它没有参数,即 yield() 方法只是使当前线程重新回到可执行状态,所以执行 yield() 的线程有可能在进入到可执行状态后马上又被执行,另外 yield() 方法只能使同优先级或者高优先级的线程得到执行机会,这也和 sleep() 方法不同
对于sleep或者wait方法,他们都将进入特定的状态,伴随着状态的切换,也就意味着等待某些条件的发生,才能够继续,比如条件满足,或者到时间等但是yield方法不涉及这些事情,他针对的是时间片的划分与调度,所以对开发者来说只是临时让一下,让一下他又不会死,就只是再等等。yield方法将会暂停当前正在执行的线程对象,并执行其他线程,他始终都是RUNNABLE状态
作用:
线程让步,顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行。但是,这种让步只对同优先级或者更高优先级的线程而言,同时,让步具有不确定性,当前线程也会参与调度,即有可能又被重新调度,那么就没有达到让出CPU的效果了。
ps:
①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。
③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。
JDK中提供三个版本的join方法:
1 | void join() //当前线程等该加入该线程后面,等待该线程终止。 |
作用:
join方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。
join相当于让其他的线程(特定的)进行插队处理,自己再继续处理。例如:主线程中调用启动线程(调用start),然后调用该线程的join方法,可以达到主线程等待工作线程运行结束才执行的效果,并且join要在start调用后。
sleep就是被监视了,在特定的条件下(中断或者是自己醒来)才能恢复到就绪状态。
yield就是一种礼让,自己直接就是就绪状态。https://www.cnblogs.com/noteless/p/10443446.html
7)interrupt方法
interrupt,顾名思义,即中断的意思。单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就说,它可以用来中断一个正处于阻塞状态的线程;另外,通过interrupt方法和isInterrupted()方法来停止正在运行的线程。
1 | public class Test { |
输出结果:
如上可知,interrupt方法可以中断处于阻塞状态的线程。
1 | public class Test { |
运行该程序会发现,while循环会一直运行直到变量i的值超出Integer.MAX_VALUE。所以说直接调用interrupt方法不能中断正在运行中的线程。
但是如果配合isInterrupted()能够中断正在运行的线程,因为调用interrupt方法相当于将中断标志位置为true,那么可以通过调用isInterrupted()判断中断标志是否被置位来中断线程的执行。比如下面这段代码:
1 | public class Test { |
运行会发现,打印若干个值之后,while循环就停止打印了。
但是一般情况下不建议通过这种方式来中断线程,一般会在MyThread类中增加一个属性 isStop来标志是否结束while循环,然后再在while循环中判断isStop的值。
1 | class MyThread extends Thread{ |
那么就可以在外面通过调用setStop方法来终止while循环。
8)interrupted方法
interrupted()函数是Thread静态方法,用来检测当前线程的interrupt状态,检测完成后,状态清空。通过下面的interrupted源码我们能够知道,此方法首先调用isInterrupted方法,而isInterrupted方法是一个重载的native方法private native boolean isInterrupted(boolean ClearInterrupted)
通过方法的注释能够知道,用来测试线程是否已经中断,参数用来决定是否重置中断标志。
1 | public static boolean interrupted() { |
9)stop方法
stop方法已经是一个废弃的方法,它是一个不安全的方法。因为调用stop方法会直接终止run方法的调用,并且会抛出一个ThreadDeath错误,如果线程持有某个对象锁的话,会完全释放锁,导致对象状态不一致。所以stop方法基本是不会被用到的。
关系到线程属性的几个方法
1)getId
用来得到线程ID
2)getName和setName
用来得到或者设置线程名称。
3)getPriority和setPriority
用来获取和设置线程优先级。
4)setDaemon和isDaemon
用来设置线程是否成为守护线程和判断线程是否是守护线程。
守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。
Thread类有一个比较常用的静态方法currentThread()用来获取当前线程。
Thread类中的方法同线程状态的关系
注意几点:
线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,譬如程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。
当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。
线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。
实现接口 VS 继承 Thread
实现接口会更好一些,因为:
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
使用ExecutorService、Callable、Future实现有返回结果的线程
ExecutorService、Callable、Future三个接口实际上都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,有了这种特征就不需要再为了得到返回值而大费周折了。而且自己实现了也可能漏洞百出。
可返回值的任务必须实现Callable接口。类似的,无返回值的任务必须实现Runnable接口。
执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。
注意:get方法是阻塞的,即:线程无返回结果,get方法会一直等待。
再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。
下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:
1 | import java.util.concurrent.*; |
代码说明:
上述代码中Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
1 | public static ExecutorService newFixedThreadPool(int nThreads) |
设置线程的优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。
注:Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~·0之间,也可以使用Thread类提供的三个静态常量:
1 | MAX_PRIORITY =10 |
1 | public class Test1 { |
注:虽然Java提供了10个优先级别,但这些优先级别需要操作系统的支持。不同的操作系统的优先级并不相同,而且也不能很好的和Java的10个优先级别对应。所以我们应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常量来设定优先级,这样才能保证程序最好的可移植性。
正确结束线程
Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用下面的方法:
- 正常执行完run方法,然后结束掉;
- 控制循环条件和判断条件的标识符来结束掉线程。
1 | class MyThread extends Thread { |
四、基础线程机制
执行器(executor)
执行器 ( Executor ) 类有许多静态工厂方法用来构建线程池 。
类图如下:
Executor 英文意思是执行器,顾名思义,就是执行任务,所以该接口只有一个执行任务的方法:
1 | void execute(Runnable command); |
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
主要有三种 Executor:
- CachedThreadPool:一个任务创建一个线程;
- FixedThreadPool:所有任务只能使用固定大小的线程;
- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
ExecutorService
ExecutorService 继承自 Executor,正如其名字一样,它定义了一个服务,定义了一个完成的线程池的行为。可以提交任务,执行任务,关闭服务。
ExecutorService
提供了两个方法来达到这个目的——shutdwon()
会等待正在执行的任务执行完而shutdownNow()
会终止所有正在执行的任务并立即关闭execuotr
。
1 | public class Executors1 { |
使用ExecutorService、Callable、Future实现有返回结果的线程
Callbale
也可以像runnbales
一样提交给 executor services
。但是callables
的结果怎么办?
因为submit()
不会等待任务完成,executor service
不能直接返回callable
的结果。不过,executor
可以返回一个Future
类型的结果,它可以用来在稍后某个时间取出实际的结果。
1 | ExecutorService executor = Executors.newFixedThreadPool(1); |
在将callable
提交给exector
之后,我们先通过调用isDone()
来检查这个future是否已经完成执行。
在调用get()
方法时,当前线程会阻塞等待,直到callable在返回实际的结果之前执行完成。
Future
与底层的executor service
紧密的结合在一起。记住,如果你关闭executor
,所有的未中止的future
都会抛出异常。
1 | executor.shutdownNow(); |
我们这次创建executor
的方式与上一个例子稍有不同。我们使用newFixedThreadPool(1)
来创建一个单线程线程池的 executor service
。 这等同于使用newSingleThreadExecutor
。
超时
任何future.get()
调用都会阻塞,然后等待直到callable
中止。在最糟糕的情况下,一个callable
持续运行——因此使你的程序将没有响应。我们可以简单的传入一个时长来避免这种情况。
1 | ExecutorService executor = Executors.newFixedThreadPool(1); |
运行上面的代码将会产生一个TimeoutException
:
1 | Exception in thread "main" java.util.concurrent.TimeoutException |
invokeAll
Executors
支持通过invokeAll()
一次批量提交多个callable
。这个方法结果一个callable
的集合,然后返回一个future
的列表。
1 | ExecutorService executor = Executors.newWorkStealingPool(); |
invokeAny
批量提交callable
的另一种方式就是invokeAny()
,它的工作方式与invokeAll()
稍有不同。在等待future
对象的过程中,这个方法将会阻塞直到第一个callable
中止然后返回这一个callable
的结果。
为了测试这种行为,我们利用这个帮助方法来模拟不同执行时间的callable
。这个方法返回一个callable
,这个callable
休眠指定 的时间直到返回给定的结果。
1 | Callable<String> callable(String result, long sleepSeconds) { |
我们利用这个方法创建一组callable
,这些callable
拥有不同的执行时间,从1分钟到3分钟。通过invokeAny()
将这些callable提交给一个executor
,返回最快的callable
的字符串结果-在这个例子中为任务2:
1 | ExecutorService executor = Executors.newWorkStealingPool(); |
上面这个例子又使用了另一种方式来创建executor
——调用newWorkStealingPool()
。这个工厂方法是Java8引入的,返回一个ForkJoinPool
类型的 executor
,它的工作方法与其他常见的execuotr稍有不同。与使用一个固定大小的线程池不同,ForkJoinPools
使用一个并行因子数来创建,默认值为主机CPU的可用核心数。
ScheduledExecutor
我们已经学习了如何在一个 executor
中提交和运行一次任务。为了持续的多次执行常见的任务,我们可以利用调度线程池。
ScheduledExecutorService
支持任务调度,持续执行或者延迟一段时间后执行。
下面的实例,调度一个任务在延迟3分钟后执行:
1 | ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); |
调度一个任务将会产生一个专门的future
类型——ScheduleFuture
,它除了提供了Future
的所有方法之外,他还提供了getDelay()
方法来获得剩余的延迟。在延迟消逝后,任务将会并发执行。
为了调度任务持续的执行,executors
提供了两个方法scheduleAtFixedRate()
和scheduleWithFixedDelay()
。第一个方法用来以固定频率来执行一个任务,比如,下面这个示例中,每分钟一次:
1 | ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); |
另外,这个方法还接收一个初始化延迟,用来指定这个任务首次被执行等待的时长。
请记住:scheduleAtFixedRate()
并不考虑任务的实际用时。所以,如果你指定了一个period
为1分钟而任务需要执行2分钟,那么线程池为了性能会更快的执行。
在这种情况下,你应该考虑使用scheduleWithFixedDelay()
。这个方法的工作方式与上我们上面描述的类似。不同之处在于等待时间 period
的应用是在一次任务的结束和下一个任务的开始之间。例如:
1 | ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); |
这个例子调度了一个任务,并在一次执行的结束和下一次执行的开始之间设置了一个1分钟的固定延迟。初始化延迟为0,任务执行时间为0。所以我们分别在0s,3s,6s,9s等间隔处结束一次执行。
如你所见,scheduleWithFixedDelay()
在你不能预测调度任务的执行时长时是很有用的。
线程执行器和不使用线程执行器的对比(优缺点)
1.线程执行器分离了任务的创建和执行,通过使用执行器,只需要实现Runnable接口的对象,然后把这些对象发送给执行器即可。
2.使用线程池来提高程序的性能。当发送一个任务给执行器时,执行器会尝试使用线程池中的线程来执行这个任务。避免了不断创建和销毁线程导致的性能开销。
3.执行器可以处理实现了Callable接口的任务。Callable接口类似于Runnable接口,却提供了两方面的增强:
a.Callable主方法名称为call(),可以返回结果
b.当发送一个Callable对象给执行器时,将获得一个实现了Future接口的对象。可以使用这个对象来控制Callable对象的状态和结果。
4.提供了一些操作线程任务的功能
后台(守护)线程
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程的用途为:
- 守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。
- Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。
setDaemon方法的详细说明:
1 | public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 |
注:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台县城时候一定要注意这个问题。
线程池
构建一个新的线程是有一定代价的,因为涉及与操作系统的交互 。如果程序中创建了大量的生命期很短的线程,应该使用线程池 ( thread pool ) 。
一个线程池中包含许多准备运行的空闲线程 。将 Runnable
对象交给线程池 ,就会有一个线程调用 run
方法。当 run
方法退出时,线程不会死亡 , 而是在池中准备为下一个请求提供服务 。另一个使用线程池的理由是减少并发线程的数目。
创建大量线程会大大降低性能甚至使虚拟机崩溃。如果有一个会创建许多线程的算法,应该使用一个线程数 “ 固定的 ”线程池以限制并发线程的总数。
ThreadPoolExecutor类
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类。(该类的结构层次可以参见上诉的执行器)
1、在ThreadPoolExecutor类中提供了四个构造方法:
1 | public class ThreadPoolExecutor extends AbstractExecutorService { |
从上面的代码可以得知,ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。
下面解释下一下构造器中各个参数的含义:
- corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
- maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
1 | TimeUnit.DAYS; //天 |
- workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
1 | ArrayBlockingQueue; |
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
- threadFactory:线程工厂,主要用来创建线程;
- handler:表示当拒绝处理任务时的策略,有以下四种取值:
1 | ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 |
线程池的执行策略
ThreadPoolExecutor 执行任务的逻辑示意图如下:
2、ThreadPoolExecutor类的结构层析:
从上面给出的ThreadPoolExecutor类的代码可以知道,ThreadPoolExecutor继承了AbstractExecutorService,我们来看一下AbstractExecutorService的实现:
1 | public abstract class AbstractExecutorService implements ExecutorService { |
AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。
我们接着看ExecutorService接口的实现:
1 | public interface ExecutorService extends Executor { |
而ExecutorService又是继承了Executor接口,我们看一下Executor接口的实现:
1 | public interface Executor { |
Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;
然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;
抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;
然后ThreadPoolExecutor继承了类AbstractExecutorService。
3、在ThreadPoolExecutor类中有几个非常重要的方法:
1 | execute() |
execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。
shutdown()和shutdownNow()是用来关闭线程池的。
还有很多其他的方法:
比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法,可以自行查阅API。
线程池的实现原理
https://www.cnblogs.com/exe19/p/5359885.html
五、中断
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
InterruptedException
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
1 | public class InterruptExample { |
1 | public static void main(String[] args) throws InterruptedException { |
1 | Main run |
interrupted()
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
1 | public class InterruptExample { |
1 | public static void main(String[] args) throws InterruptedException { |
1 | Thread end |
Executor 的中断操作
调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。
1 | public static void main(String[] args) { |
1 | Main run |
如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
1 | Future<?> future = executorService.submit(() -> { |