1、什么是线程
在讨论什么是线程前有必要先说下什么是进程,因为线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运动活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
操作系统在分配资源时是把资源分配给进程的,但是CPU 资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。
在Java中,当我们启动main函数时其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称为主线程。
进程和线程的关系如图 1-1所示。
由图 1-1可以看到,一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
程序计数器是一块内存区域,用来记录当前线程要执行的指令地址。那么为何要将程序计数器设计为线程私有的呢?前面说了线程是占用CPU执行的基本单位,而CPU一般是使用时间片轮转方式让线程轮询占用的,所以当前线程CPU时间片用完后,要让出CPU,等下次轮到自己的时候再执行。那么如何知道之前程序执行到哪里了呢如何恢复执行线程的执行现场?其实程序计数器就是为了记录该线程让出CPU时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。另外需要注意的是,如果执行的是native方法,那么PC计数器记录的是underfined地址,只有执行的是Java代码时pc计数器记录的才是下一条指令的地址。
另外每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存储线程的调用栈帧。
堆,是一个进程中最大的一块内存(当然后面jdk版本使用直接内存另说),堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例。等
方法区,则是用来存放JVM加载的类、运行时常量池等。也是线程共享的。
1.1、进程与程序之间的关系
进程(process)是程序的运行实例。进程与程序之间的关系就好比播放中的视频(如《摩登时代》这部电影)与相应的视频文件(如MP4文件)之间的关系,前者从动态的角度刻画事务而后者从静态的角度刻画事物。运行一个Java程序的实质是启动一个Java虚拟机进程,也就是说一个运行的Java程序就是一个Java虚拟机进程。(Javaweb应用例外。一个Java web服务器是一个进程,它可以同时运行多个Java web应用。)
1.2、进程的基本原理
1.3、线程的基本原理
1.4、线程的核心原理
2、创建线程方式
Java中有三种线程创建方式:
- 继承Thread类并重写run的方法
- 实现Runnable接口的run方法
- 使用FutureTake方式
2.1、继承Thread类并重写run的方法
public class Demo02 {
public static void main(String[] args) throws InterruptedException {
// 创建线程
final SubThread subThread = new SubThread();
// 启动线程
subThread.start();
//休眠2秒
Thread.sleep(2000);
System.out.println(subThread);
// 直接调用 : 证明 subThread对象还存在
subThread.run();
// 再次调用启动线程报错 :证明由继承Thread类创建的线程只能被调用一次,启动一次,当执行完方法后线程就处于终止状态!!!
subThread.start();
}
static class SubThread extends Thread{
@Override
public void run() {
System.out.println("继承Thread,覆写run方法");
}
}
}
其实调用start方法后线程并没有马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状态。一旦run方法执行完毕,该线程就处于终止状态。不能被再次调用start方法启动
使用继承方式的好处是,在run()方法内获取当前线程直接使用this 就可以了,无线使用Thread.currentThread()方法;
不好:
1、Java不支持多继承;
2、任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而Runnable则没有这个限制。下面看实现Runnable接口的run方法方式。
2.2、实现Runnable接口的run方法
如上图代码,两个线程公用一个task任务代码逻辑,如果需要,可以给RunnableTask添加参数进行任务区分。另外,RunnableTake可以继承其他类。
但是上面介绍的两种方式都有一个缺点,就是任务 没有返回值。下面看最后一种使用FutureTask的方式。
2.3、使用FutureTake方式
public static void main(String[] args) {
// #1、创建 异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
// #2、创建线程、启动线程执行异步任务
new Thread(futureTask).start();
try {
// #3、等待异步任务执行完毕,并返回结果
String result = futureTask.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
static class CallerTask implements Callable<String>{
@Override
public String call() throws Exception {
return "future接口返回结果";
}
}
2.4、小结:
Thread 类不支持 多继承,可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递,而如果使用Runnable接口方式,则只能使用主线程里面被声明为final的变量。以上两种都无法拿到返回结果,但是Futuretask方式可以。
3、Object API的方法 - 线程通知与等待
Object类中 wait()/notify()/notifyAll()对比下面章节Thread类接口API方法。!!!
Java中的Object类是所有类的父类,鉴于继承机制,Java把所有类都需要的方法放到Object类里面,其中就包含本章节要讲的通知与等待系列函数。
3.1、wait()函数
3.1.1、概述
当一个“线程”调用一个共享变量(引用类型) 的 wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:
- 其他线程调用了该共享对象的notify()或者notifyAll()方法。
- 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptException异常返回。
另外,需要注意的是:如果调用wait()方法的线程没有事先获取该共享对象的监视器锁,则调用wait()方法时,调用线程会抛出IllegalMonitorStateException 异常。
那么一个线程如何才能获取一个共享变量的监视器锁呢?
(1)执行synchronized同步代码块时,使用该共享对象作为参数。
synchronized (共享对象){
// do something
}
(2)调用该共享对象的方法,并且该方法使用了synchronized修饰。
synchronized void add (int a, int b){
// do something
}
3.1.2、虚假唤醒
另外需要注意的:一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),即使该线程没有被其他线程调用 notify()、notifyAll()、方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。
虽然虚假唤醒在应用中很少发生,弹药防患于未然,通过多重校验,不停的去测试判断该线程被唤醒的条件是否满足,不满足继续等待,也就是说在一个循环中调用wait()方法进行防范。退出循环的条件是满足唤醒该线程的条件。
synchronized (obj){
// 条件不满足,一直将自己挂起等待
while(条件不满足){
obj.wait();
}
}
如上代码是经典的调用共享对象 obj ,的wait()方法的实例,首先通过同步块获取obj上面的监视器锁,然后在while循环内调用obj 的wait()方法。
3.1.3、举例说明 wait - notify/notifyAll
通过一个生产者和消费者例子来加深理解。如下代码,其中queue为共享变量,生产者线程在调用queue的wait()方法前,使用synchronized关键字拿到了该共享变量queue的监视器锁,所以调用wait()方法才不会抛出IllegalMonitorStateException 异常。如果当前队列没有空闲容量则会调用queued 的wait()方法挂起当前线程,这里使用while循环是避免上面说的虚假唤醒问题。假如被虚假唤醒了,通过判断还是是否继续将自己挂起。
Queue queue = new ArrayBlockingQueue(8);
synchronized (queue){
// 消费队列满,则等待队列空闲
while (queue.size() == MAX_SIZE) {
try{
// 挂起当前线程,并释放通过同步块获取的queue上的锁,
// 让消费者线程可以获取该锁(queue上面的锁都有可能,也可能还是生产者线程),然后获取队列里面的元素
queue.wait();
}catch (Exception e) {
e.printStackTrace();
}
}
// 空闲则生成元素
queue.add(ele);
// 并通知除当前线程外的 其他所有线程
queue.notifyAll();
}
}
synchronized (queue){
// 消费队列为空
while (queue.size() == 0) {
try{
// 挂起当前线程,并释放通过同步块获取的queue上的锁,
// 让消费者线程可以获取该锁(queue上面的锁都有可能,也可能还是生产者线程),然后获取队列里面的元素
queue.wait();
}catch (Exception e) {
e.printStackTrace();
}
}
// 消费元素
queue.take();
// 并通知除当前线程外的 其他所有线程
queue.notifyAll();
}
}
如上代码中假如生产者线程A 首先通过synchronized获取到了queue上的锁,那么后续“所有”企图生产元素的线程和“所有”消费线程将会在获取该监听器锁的地方被阻塞挂起。
线程A 获取锁后发现队列已经满会调用queue.wait()方法挂起阻塞自己,然后释放获取的queue对象上的锁,这里考虑一下为什么要释放锁?如果不释放会怎么怎么样?由于其他生产者线程和消费者线程都已经被挂起,而线程A 也被挂起,不释放就会产生死锁状态。
这里线程A 释放queue上的锁就会打破死锁必要条件之一的持有并等待原则。释放锁之后,其他锁监听就会重新争抢锁资源。
2、可以通过一个嵌套锁对象证明 :
当前线程调用 共享对象的wait()方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。
3.1.4、举例说明wait - interrupt
public class DemoWait {
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("-----start---------");
synchronized (obj) {
obj.wait();
}
// 中断后的代码
System.out.println("-----end---------");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
Thread.sleep(1000);
System.out.println("---begin interrupt threadA ---");
//相当于主线程main通过中断的方式结束threadA线程 , 中断后threadA在obj.wait()处抛出异常而返回并终止
threadA.interrupt();
System.out.println("---end interrupt threadA ---");
}
}
-----start---------
---begin interrupt threadA ---
---end interrupt threadA ---
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.yzzg.demo.DemoWait$1.run(DemoWait.java:21)
at java.lang.Thread.run(Thread.java:748)
3.2、wait(long timeout) 函数
3.3、wait(long timeout, int nanos) 函数
3.4、notify()函数
一个线程调用共享对象的notify()方法后,会唤醒一个在该共享对象上调用wait系列方法后被挂起的线程。一个共享对象上面可能有多个线程在等待,具体唤醒哪个等待的线程是随机的。
此外,被唤醒的线程不能马上从wait方法返回并继续执行(意思是一个线程可能从一个waiting状态转换为blocked状态),它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享对象上的监视器锁后,被唤醒的线程也不一定会抢到监视器锁,这是因为被唤醒的线程还有和其他阻塞的线程竞争锁资源,只有该线程竞争到了共享对象的监视器锁后才可以继续执行。
类似wait系列方法,只有当前线程获取了共享对象的监视器锁后,才可以调用共享对象的notify方法,否则会抛出异常。
3.5、notifyAll()函数
不同于在共享变量上调用notify函数会唤醒被阻塞该变量上的一个线程,notifyAll方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。
4、Thread API的方法
4.1、让线程睡眠的sleep()方法
Thread类中有一个静态的sleep方法,当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU的调度,获取到CPU资源后就可以继续运行了,如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep方法的地方抛出interruptException异常而返回。
4.1.1、举例说明 - interrupt中断
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println("------start--子线程开始---------");
Thread.sleep(10000);
System.out.println("------end--子线程结束---------");
}
});
// 启动子线程
thread.start();
//主线程休眠2s
Thread.sleep(2000);
// 主线中断子线程
thread.interrupt();
}
------start--子线程开始---------
Exception in thread "Thread-0" java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.yzzg.demo.DemoWailt$1.run(DemoWailt.java:21)
at java.lang.Thread.run(Thread.java:748)
4.2、yield()方法
让出CPU执行权yield()方法
Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际上就是暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。我们知道CPU是为每个线程分配一个时间片来占有CPU的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread类的静态方法yield方法时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。
当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态Runnable,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权。
- JVM 层面 的线程状态 从Running状态转换为Runnable状态(可分为两个子状态 就绪状态Ready、运行状态)而不是阻塞状态。
- 操作系统层面的 线程状态 从Running状态 转换为 就绪状态(操作系统中的状态)而不能是阻塞状态。
4.3、join()方法
等待线程执行终止的 join()方法
在项目实战中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程加载完毕再汇总处理。Thread类中有个join方法就可以做这个事情,前面介绍的等待通知方法是Object类中的,而join方法是Thread类直接提供的。
public static void main(String[] args) throws InterruptedException {
// 线程1
Thread threadOne = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
Thread.sleep(1000);
System.out.println("子线程 1 over");
}
});
// 线程2
Thread threadTwo = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
Thread.sleep(1000);
System.out.println("子线程 2 over");
}
});
// 启动子线程
threadOne.start();
threadTwo.start();
// 加入 等待所有子线程执行完毕执行主线程
threadOne.join();
threadTwo.join();
System.out.println("所有子线程执行完毕");
}
如上代码在主线程里面启动两个子线程,然后分别join,主线程首先会在调用threadOne.join();被阻塞挂起,等待threadOne执行完毕返回,又在threadTwo.join();方法处被阻塞挂起,等待threadTwo.执行完毕返回。这里只延时join方法作用,这种情况可以使用CountDownLatch 是个不错的选择。
4.4、线程中断
4.4.1、interrupt()方法:
1、通过中断标记使正常运行的程序退出循环
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread() + "hello");
}
}
});
// 启动子线程
thread.start();
// 主线程休眠1s
Thread.sleep(1000);
System.out.println("主线程 中断子线程");
// 主线中断子线程
thread.interrupt();
// 等待子线程执行完毕
thread.join();
System.out.println("主线程结束");
}
2、中断 wait()系列、join方法系列、sleep方法 而被阻塞挂起的线程,都会抛出异常InterruptException!!!!!
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("------start threadOne thread ------ ");
Thread.sleep(2000000);
System.out.println("threadOne awaking --- 这里没有执行");
} catch (InterruptedException e) {
System.out.println("中断异常捕获");
return;
}
System.out.println("threadOne-leaving 非正常--- 这里没有执行");
}
});
// 启动线程
threadOne.start();
// 确保子线程进入休眠状态
TimeUnit.SECONDS.sleep(1);
// 主线程 中断子线程的休眠,让子线程从sleep函数中返回
System.out.println("主线程 中断子线程的休眠");
threadOne.interrupt();
//--- 这里好像没什么用??
threadOne.join();
System.out.println("主线程 结束");
}中断 wait()系列、join方法系列、sleep方法 而被阻塞挂起的线程,都会抛出异常InterruptException
4.4.2、isInterrupted()方法:
4.4.3、interrupted()方法:
5、线程的生命周期状态(标准的Java语言线程状态)
在Java语言中,一个线程从其创建、启动到其运行结束的整个生命周期可能经历若干状态。
Java 的线程状态可以通过监控工具查看,也可通过Thread.getState()方法获取。Thread.State 是一个枚举类型Enum。定义了线程状态包括以下几种。
5.1、NEW : 新建
一个已经创建而未启动的线程处于该状态。由于一个线程实例 只能被启动一次,因此该线程只可能有一次处于该状态。只是通过关键字 new 创建了一个对象,还未调用start方法。
5.2、RUNNABLE : 可执行状态:包括操作系统的就绪、运行两种状态
该状态可以被看作为一个复合状态。它包括两个子状态:ready 和 Running 。
前者表示处于该状态的线程可以被线程调度器scheculer 进行调度而使之处于running状态。
后者表示处于该状态的线程是已经获取到CPU执行权正在运行的状态,即相应线程对象的run方法的代码指令正在由处理器执行。
执行Thread.yield()的线程,其状态可能会有Running转换为ready,处于ready状态的线程也是被称为活跃线程。
5.3、BLOCKED: 阻塞
一个线程发起一个阻塞式 I/O(Blocking I/O)操作后,或者申请一个由其他线程持有的独占资源(比如锁)时,相应的线程会处于该状态。处于Blocked状态的线程并不会占用CPU处理器资源(这个怎么理解?)。当阻塞式I/O操作完成后,或者线程获得了其他申请的资源,该线程的状态又可以转换为Runnable状态。
- I/O阻塞
- 等待锁资源
它们都是活着的可主动与其他线程争抢资源
5.4、WAITING: 等待
一个线程执行了某些特定方法之后就会处于这种等待其他线程执行另外一些特定操作的状态。
能够使其执行线程变更为Waiting状态的方法包括:Object.wait()、Thread.join()、和LockSuppert.park(Object) 。被称为挂起状态/等待状态
能够使相应的线程从Waiting变更为Runnable的相应方法包括:notify、notifyAll、和 LockSuppert.unpark(Object)。
调用上面方法会不会释放CPU执行权?/会不会释放锁资源?
会释放CPU执行权,会释放锁。
1、线程的waiting(等待)状态表示线程在等待被唤醒。处于waiting状态的线程不会被分配CPU时间片。执行以下两个操作,当前线程将处于waiting状态:
(1) 执行没有时间限制(timeout)参数的thread.join()调用:在线程合并场景中,若线程A调用B.join()去合入B线程,则在B执行期间线程A处于Waiting状态,一直等待线程B执行完成。
(2) 执行没有时间限制(timeout)参数的Object.wait()调用:指一个拥有Object对象锁的线程,进入相应的代码临界区后,调用相应对象的wait()方法去等待其“对象锁”(object monitor)上的信号,若对象锁上没有信号,则当前线程处于waiting状态。
5.5、Timed_Waiting : 限时等待
该状态和Waiting类似,差别就是处于该状态的线程并非无限制的等待其他线程执行操作,而是处于带有时间限制的等待状态。时间到后自动会将该线程状态转换为Runnable。
5.6、Terminated: 终止
已经执行结束的线程处于该状态。由于一个线程实例只能够被启动一次因此一个线程也只能够有一次处于该状态。Thread.run()正常返回或者由于抛出异常而提前终止都会导致相应线程处于该状态。
一个线程在其整个生命周期中,只可能有一次处于NEW状态和Terminated状态。
状态流转图
重要!!!重要!!!重要 !!!理解下面线程的流转状态几乎完全理解
1、该图并不是Java语言标准的状态流转,上面等待池应该分为两种等待waiting和限时等待wait。
2、Java中线程一旦被创建启动,是否运行、什么时候运行都是操作系统调度决定的,所以在Java语言中一个线程从运行状态 到 ——> 某个状态(终止状态和异常停止除外)再到 运行状态之前必须要先到可运行状态Runnable。
3、调用带时间参数的,称为TimeWaiting状态。不带时间参数的称为waiting状态。
4、阻塞IO/同步机制,这两种会是线程状态 进入 Blocked 状态。
5.7、Blocked 与 Waiting 区别关系深入理解
5.7.1、waiting:主动为之,程序员主动调用wait()方法释放cpu执行权和释放锁 进入等待队列 需要notify()唤醒进入同步队列竞争锁
5.7.2、blocked:被动的,在竞争锁的时候失败,被阻塞,在同步队列里继续竞争锁。
5.7.3、sleep(long):方法 是进入了(Timed_Waiting)超时等待状态,时间到了自己返回原状态,释放CPU执行权和不释放锁
BLOCKED状态
线程处于BLOCKED状态的场景。
- 当前线程在等待一个monitor lock,比如等待执行synchronized代码块或者使用synchronized标记的方法。
在synchronized块中循环调用Object类型的wait方法,如下是样例 synchronized(this) { while (flag) { obj.wait(); } // some other code }
WAITING状态
线程处于WAITING状态的场景。
调用Object对象的wait方法,但没有指定超时值。
- 调用Thread对象的join方法,但没有指定超时值。
- 调用LockSupport对象的park方法。
提到WAITING状态,顺便提一下TIMED_WAITING状态的场景。
TIMED_WAITING状态
线程处于TIMED_WAITING状态的场景。
- 调用Thread.sleep(long time)方法。
- 调用Object对象的wait(long time)方法,指定超时值。
- 调用Thread对象的join方法,指定超时值。
- 调用LockSupport对象的parkNanos(long time)方法。
- 调用LockSupport对象的parkUntil(long time)方法。
参考:Java 高并发核心编程 卷2 : 多线程、锁、JMM、JUC