前言
最近在准备面试,所以重新复习下多线程基础方面的知识,后续还会跟进JUC包下常用的类等。多线程基础方面知识也好久没看了,这边学习了廖雪峰老师的课,在这做下笔记,下文大多都是引用廖雪峰老师的笔记,记录一些平时比较模糊的地方。
多线程基础
特别注意:直接调用Thread实例的run()
方法是无效的。
可以对线程设置优先级Thread.setPriority(int n) //1~10, 默认值5
。优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们绝不能通过设置优先级来确保高优先级的线程一定会先执行。
线程状态
在Java程序中,一个线程对象只能调用一次start()
方法来启动新线程,并在新线程中执行run()
方法。一旦run()
方法执行完毕,线程就结束了。因此Java线程的状态有下面几种
- New:新创建的线程,尚未执行。
- Runnable:运行中的线程,正在执行
run()
方法的Java代码。 - Block:运行中的线程,因为某些操作被阻塞而挂起。
- Waiting:运行中的线程,因为某些操作在等待中。
- Timed Waiting:运行中的线程,因为执行
sleep()
方法正在计时等待。 - Terminated:线程已终止,因为
run()
当线程启动后,他可以在Runnable
、Block
、Waiting
和Timed Waiting
这几个状态之间切换,直到最后变成Terminated
状态线程终止。
线程终止的原因有:
- 线程正常终止:
run()
方法执行到return
语句返回; - 线程以外终止:
run()
方法因为未捕获的异常导致线程终止; - 对某个线程的
Thread
实例调用stop()
方法强制终止(强烈不推荐使用)。
一个线程还可以等待另一个线程知道运行结束。例如main
线程在启动a
线程后,可以通过a.join()
等待a
线程结束后再继续运行。
如果a
线程已经结束,对实例a
调用a.join()
会立刻返回。此外,join(long)
的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。
1 | public class JoinTest { |
1 | start |
线程中断
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()
方法,目标线程需要反复检测自身状态是否是interrupted
状态,如果是,就立刻结束运行。
1 | public class Main { |
1 | 1 hello! |
上述代码中,main
线程通过调用t.interrupt()
方法中断t
线程,但是要注意,interrupt()
方法仅仅向t
线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t
线程的while
循环会检测isInterrupted()
,所以上述代码能正确响应interrupt()
请求,使得自身立刻结束运行run()
方法。
1 | public class Main { |
1 | 1 hello! |
中断线程的第二种方法就是,通过标记位来进行中断。
注意到HelloThread
的标志位boolean running
是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。
为什么要对线程间共享的变量用关键字volatile声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!
volatile
关键字的主要就是通知JVM:
- 每次访问变量时,总是获取主内存的最新值。
- 每次修改变量后,立刻回写到主内存中。
volatile
关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
如果我们去掉volatile
关键字,运行上述程序,发现效果和带volatile
差不多,这是因为在x86
的架构下,JVM回写主内存的速度非常快,但是,换成ARM
的架构,就会有显著的延迟。
守护线程
Java程序入口是main
线程,main
线程又能启动其他线程,只有当所有线程都运行结束的时候,JVM
才会退出,如果一个线程没有退出,JVM
进程就不会退出,所以,必须保证所有线程都能及时结束。
当一个死循环的线程被创建,说名这个线程不能被结束。那JVM进程就无法结束,但谁负责结束这个线程呢。
我们可以将这个线程设置为守护线程,因为所有非守护线程执行完毕之后,无论有没有守护线程,虚拟机都会自动退出。
1 | Thread a = new TestThread(); |
我们可以在线程start()
前设置setDaemon(true)
可以将该线程标志为守护线程。
特别注意:守护线程不能持有需要关闭的资源,因为JVM退出时,守护线程没有任何机会来关闭资源,很有可能导致数据丢失。