什么是线程
在操作系统中,进程是资源分配的基本单位,线程是资源调度的基本单位,因为真正占用CPU的是线程。

对于整个Java应用程序来说是一个进程,里面有很多线程。在操作系统中说,线程不独立拥有资源(进程是拥有资源的基本单位),但是线程还是会拥有自己独立的一些资源的比如说程序计数器,栈等。
程序计数器:用来记录当前线程要执行的指令地址。我的理解是,第一为了确定线程要执行的后面的指令地址,第二是为了确保线程切换的时候,线程能记住它的执行状态(当前执行到哪了),当下一次再次获得CPU资源的时候,线程能从它私有的程序计数器中获取指令地址继续执行。但是如果执行的是native方法,那么pc计数器记录的是undefined地址,只有执行Java代码的时候记录的才是下一条指令的地址。
栈:对于任何语言来说,线程是需要有栈的,这个栈中存放是就是栈帧。因为,对于CPU来说,是没有方法层面的,当高级语言进行方法调用的时候其实对于CPU来说还是执行相应地址中的指令,所以在高级语言中需要有一个方法调用栈,每执行一次方法调用的时候将参数,返回地址存入栈帧中并压入方法栈,等到被调用的方法执行完再出栈取出返回地址继续执行指令。而因为栈帧和方法同生死共命运,所以局部变量,参数等都是放入栈帧中的。
总结:线程中这个栈是方法调用栈,其中基本单位是栈帧,每次该线程进行方法调用的时候创建栈帧并压入方法调用栈,栈帧中有相应的方法参数,返回地址,和局部变量(Java中局部变量在栈中的原因)
堆:堆是进程中最大的一块内存,堆是被进程中的所有线程所共享的。堆中主要存放的是new操作创建的对象实例(Java对象在堆中)。
方法区:用来存放JVM加载的类,常量及静态变量等信息,也是所有线程共享的。
线程创建与运行
线程创建有三种方式
实现Runnable接口并重写run方法
继承Thread类并重写run方法
使用FutureTask创建
使用继承的好处是,获取当前线程直接使用this就可以了,但是Java不支持多继承,所以该类继承了Thread类之后不能再继承其他类了(降低了可扩展性),而且继承Thread类即将任务代码和线程耦合了。
实现Runnable接口并重写run方法,最后将该实现接口实例作为参数传入Thread构造函数中,这种方法将任务代码和线程之间解耦,并且解决了多继承的问题。
而对于上面两种创建线程的方法来说,线程是没有返回值的,因为run方法是void方法。要使线程具有返回值可以通过FutureTask创建线程。
具体的步骤是:
创建一个类实现Callable接口并重写call()方法
- 将该类作为参数传入Thread构造方法并启动线程
- 最后通过FutureTask.get()等待任务执行完毕返回结果
线程等待与通知
wait()函数
它是Object中的一个方法,在并发编程中,它的调用者其实是共享变量,因为只有获得了synchronized隐式锁的线程才能使用wait方法,如果没有则会抛出IllegalMonitorStateException。
当线程调用这个wait方法的时候会被阻塞挂起,只有
- 其他线程调用了该共享变量的notify或者notifyAll(也就是说wait方法会释放当前共享变量的锁)
其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常并返回。
为了防范虚假唤醒,所以wait方法有它的编程范式
1 | synchronized (obj) { |
wait(long timeout)
增加wait时间限制,如果超出时间限制,不管上文提到的两个情况是否满足,该线程还是会因为超时而返回。
wait(long timeout, int nanos)
差不多,内部其实调用的是wait(long timeout)函数
notify()
会随机唤醒一个在该共享变量下调用wait()方法而阻塞的线程。
被唤醒的线程不能直接执行,还需要重新获得共享变量的锁,才能继续执行。
notifyAll()
通知所有在该共享变量下因为调用wait而阻塞的线程。
等待线程执行终止的join方法
join方法是Thread直接提供的无参且返回值为void的方法。
作用是等待线程执行完毕。
让线程睡眠的sleep()方法
会让线程暂时让出指定时间的执行权,也就是在这期间不参与CPU调度,但是线程持有的监视器资源(比如锁)是不会释放的。
让出CPU执行权的yield()方法
当一个线程调用这个方法的时候就是在暗示线程调度器 当前线程请求让出自己的CPU使用,但是,线程调度器可以无条件忽略这个暗示。我们知道时间片轮转会让某个线程持有CPU资源一段时间,线程如果还没使用完这个时间就不想使用了,可以调用这个方法来告诉线程调度器可以进行下一轮的线程调度了。
线程中断
线程中断是线程之间的一种协作模式,通过设置中断标志不能直接终止线程的执行,而是 线程根据中断状态自己去处理 。
下面三个方法是关于线程中断的
void interrupt()方法 中断线程,例如线程A执行时,线程B可以通过调用线程A的interrupt()方法来设置线程A的中断标志为true,并立即返回,注意:仅仅是设置中断标志,线程A并没有真正被中断。若线程A调用了wait(),join(),sleep()方法而被阻塞挂起的时候,线程B调用线程A的interrupt()方法会在 调用这些方法的地方抛出InterruptedException而返回(注意:这里是wait()这些方法,而不是interrupt()方法) 。
boolean isInterrupted()方法 检测当前线程是否被中断,如果是返回true,否则false。
boolean interrupted()方法 检测 当前线程 是否被中断,如果是返回true,否则false。并且它还会清除中断状态。它是Thread的静态方法,不管在哪调用返回的都是当前执行的线程的中断状态并清楚中断状态。
如果某个线程为了等待某些条件发生而阻塞(一般会调用sleep,wait或join函数),比如这个线程调用了sleep(3000)函数去等待某种条件发生,但是在1秒的时候条件已经满足,这个时候可以调用该线程的interrupt()方法来 强制sleep()抛出InterruptedException而返回,线程恢复到激活状态。
理解线程上下文切换
正如一开始所讲的程序计数器,程序技术器中保存了相应的下一个执行指令地址,栈中保存了执行的一些信息。这些就是线程上下文切换所需要的资源。
线程上下文切换的时机
当前线程CPU时间使用完处于就绪状态。
当前线程被其他线程中断。
线程死锁
死锁的四个必要条件:1. 互斥,2. 请求并占有, 3. 不可剥夺, 4. 环路等待
避免:破坏一个必要条件。
守护线程与用户线程
Java中的线程分为两类:分别为daemon线程和user线程。JVM在启动时会调用main函数,main函数所在的线程是user线程,其实JVM在启动时还启动了好多daemon线程比如垃圾回收线程。
用户线程和守护线程的区别:当最后一个用户线程结束的时候JVM会退出,而守护线程的消亡不会影响到JVM的退出。
比如在main函数中启动一个无限循环的用户线程,当主线程执行完毕的时候JVM不会退出,但是如果把这个无限循环的线程改为守护线程,那么在主线程结束后JVM会自动退出。
如何设置守护线程?
1 | deamonThread.setDaemon(true); |
main线程运行结束后,JVM会自动启动一个叫做DestroyJavaVM的线程,该线程会等待所有 用户线程 结束后终止JVM进程。
而Tomcat的NIO实现NioEndPoint中会开启一组接受线程来接受用户的连接请求以及一组处理线程负责具体处理用户请求,这些线程线程被设置成了守护线程,即当tomcat收到shutdown命令之后并且没有其他用户线程存在的情况下,tomcat进程会马上消亡而不会等待处理线程处理完当前的请求。
ThreadLocal
线程本地化,可以将共享变量复制到线程本地存储空间。这是一种无锁的同步方式。
使用方式: 创建ThreadLocal变量,在线程运行方法中设置刚刚的threadLocal变量实例。
实现原理

当ThreadLocal实例在线程执行的时候第一次调用set或者get方法的时候会在线程中创建相应的threadLocals(这个是一个ThreadLocalMap,一种定制化的HashMap,里面存放了许多线程本地化变量),key为当前ThreadLocal实例的引用,值为自己设置的值。后面调用就直接会在线程的threadLocals这个Map中进行操作。
注意当本地变量不再使用的时候最好使用remove将其删除,避免内存溢出。还要注意的是ThreadLocal不支持继承,也就是说子线程不会拥有父线程的threadLocals变量。
但是可以使用InheritableThreadLocal类,可以追溯Thread创建的源码,Thread在创建的过程中会判断父线程的inheritableThreadLocals(上图的Thread中有这个私有变量)是否为空,如果不为空那么将其复制到子线程的inheritableThreadLocals中去。