Thread是操作系统能够进行运算调度的最小单位。
它被包含在进程之中,是进程中的实际运作单位。在Java中实现多线程的有两种方式
第一种:
- 继承 java.lang.Thread
- 重写 run 方法
第二种:
- 实现 java.lang.Runnable
- 实现 run 方法
Thread与Runnable关系
本质上来讲,个人认为只有一种方式:实现Runnable接口。
构造器
Thread()对外提供了8个构造器,但是都是接收不同参数,然后调用init(ThreadGroup g, Runnable target, String name, long stackSize)方法。故我们只需分析次init()方法即可。
init()方法共有4个参数,分别代表
- ThreadGroup g 指定当前线程的线程组,未指定时线程组为创建该线程所属的线程组。线程组可以用来管理一组线程,通过activeCount() 来查看活动线程的数量。其他没有什么大的用处。
- Runnable target 指定运行其中的Runnable,一般都需要指定,不指定的线程没有意义,或者可以通过创建Thread的子类并重新run方法。
- String name 线程的名称,不指定自动生成。
- long stackSize 预期堆栈大小,不指定默认为0,0代表忽略这个属性。与平台相关,不建议使用该属性。
状态
线程从创建到最终的消亡,要经历若干个状态。一般来说,线程包括以下这几个状态:
创建(new)、就绪(runnable)、运行(running)、阻塞(blocked)、time waiting、waiting、消亡(dead)。
下面这副图描述了线程从创建到消亡之间的状态:
public方法
Thread Thread.currentThread() :获得当前线程的引用。获得当前线程后对其进行操作。
Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() :返回线程由于未捕获到异常而突然终止时调用的默认处理程序。
int Thread.activeCount():当前线程所在线程组中活动线程的数目。
void dumpStack() :将当前线程的堆栈跟踪打印至标准错误流。
int enumerate(Thread[] tarray) :将当前线程的线程组及其子组中的每一个活动线程复制到指定的数组中。
Map<Thread,StackTraceElement[]> getAllStackTraces() :返回所有活动线程的堆栈跟踪的一个映射。
boolean holdsLock(Object obj) :当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
boolean interrupted() :测试当前线程是否已经中断。
void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) :设置当线程由于未捕获到异常而突然终止,并且没有为该线程定义其他处理程序时所调用的默认处理程序。
void sleep(long millis) :休眠指定时间
void sleep(long millis, int nanos) :休眠指定时间
void yield() :暂停当前正在执行的线程对象,并执行其他线程。意义不太大
void checkAccess() :判定当前运行的线程是否有权修改该线程。
ClassLoader getContextClassLoader() :返回该线程的上下文 ClassLoader。
long getId() :返回该线程的标识符。
String getName() :返回该线程的名称。
int getPriority() :返回线程的优先级。
StackTraceElement[] getStackTrace() :返回一个表示该线程堆栈转储的堆栈跟踪元素数组。
Thread.State getState() :返回该线程的状态。
ThreadGroup getThreadGroup() :返回该线程所属的线程组。
Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() :返回该线程由于未捕获到异常而突然终止时调用的处理程序。
void interrupt() :中断线程。
boolean isAlive() :测试线程是否处于活动状态。
boolean isDaemon() :测试该线程是否为守护线程。
boolean isInterrupted():测试线程是否已经中断。
void join() :等待该线程终止。
void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。
void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
void run() :线程启动后执行的方法。
void setContextClassLoader(ClassLoader cl) :设置该线程的上下文 ClassLoader。
void setDaemon(boolean on) :将该线程标记为守护线程或用户线程。
void start():使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
String toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
start和run方法区别
start:
start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到CPU时间片,就会通过JVM调用run()方法,执行本线程的线程体。
这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
如果线程被多次start会抛出异常;threadStatus的状态由JVM控制。
run:
run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
public class ThreadTestDemo extends Thread {
public ThreadTestDemo(@NotNull String name) {
super(name);
}
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public class TheadTest {
public static void main(String[] args) {
Thread welcome = new ThreadTestDemo("A1");
welcome.run();
System.out.println(Thread.currentThread().getName());
Thread welcome1 = new ThreadTestDemo("A2");
welcome1.start();
}
}
运行结果:
main
main
A2
setDaemon方法使用
JAVA线程分为即实线程与守护线程,守护线程是优先级低,存活与否不影响JVM的退出的线程,实现守护线程的方法是在线程start()之前setDaemon(true),否则会抛出一个IllegalThreadStateException异常。不能操作文件、数据库等资源,避免主线程关闭而未能关闭守护线程的资源,并且它会在任何时候甚至在一个操作的中间发生中断。
优点及使用场景: 在主线程关闭后无需手动关闭守护线程,因为会自动关闭,避免了麻烦,Java垃圾回收线程就是一个典型的守护线程,简单粗暴的可以理解为所有为线程服务而不涉及资源的线程都能设置为守护线程。
示例Demo
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
try {
System.out.println(Thread.currentThread().getName());
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sleep 1s");
}
});
/* 默认为false,代表非守护线程
* true为守护线程,随主方法关闭时一起关闭
*/
thread.setDaemon(true);
thread.start();
System.out.println(Thread.currentThread().getName());
Thread.sleep(3100);
System.out.println("main thread is over");
}
}
中断方法(interrupt)
interrupt() 它基于「一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。」思想,是一个比较温柔的做法,它更类似一个标志位。其实作用不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。
interrupt() 并不能真正的中断线程,这点要谨记。需要被调用的线程自己进行配合才行。也就是说,一个线程如果有被中断的需求,那么就需要这样做:
- 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
- 在调用阻塞方法时正确处理InterruptedException异常。(例如:catch异常后就结束线程。)
Thread 类 interrupt 相关的几个方法:
// 核心 interrupt 方法
public void interrupt() {
if (this != Thread.currentThread()) // 非本线程,需要检查权限
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // 仅仅设置interrupt标志位
b.interrupt(this); // 调用如 I/O 操作定义的中断方法
return;
}
}
interrupt0();
}
// 静态方法,这个方法有点坑,调用该方法调用后会清除中断状态。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
// 这个方法不会清除中断状态
public boolean isInterrupted() {
return isInterrupted(false);
}
// 上面两个方法会调用这个本地方法,参数代表是否清除中断状态
private native boolean isInterrupted(boolean ClearInterrupted);
首先讲 interrupt() 方法:
- interrupt 中断操作时,非自身打断需要先检测是否有中断权限,这由jvm的安全机制配置;
- 如果线程处于sleep, wait, join 等状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常;
- 如果线程处于I/O阻塞状态,将会抛出ClosedByInterruptException(IOException的子类)异常;
- 如果线程在Selector上被阻塞,select方法将立即返回;
- 如果非以上情况,将直接标记 interrupt 状态;
注意:interrupt 操作不会打断所有阻塞,只有上述阻塞情况才在jvm的打断范围内,如处于锁阻塞的线程,不会受 interrupt 中断;
阻塞情况下中断,抛出异常后线程恢复非中断状态,即 interrupted = false
调用方法引起的状态变化
在上面已经说到了Thread类中的大部分方法,那么Thread类中的方法调用到底会引起线程状态发生怎样的变化呢?下面一幅图就是在上面的图上进行改进而来的:
上下文切换
对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。
由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。
因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
说简单点的:对于线程的上下文切换实际上就是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。