线程基本概念
并发与并行
- 并发:两个及两个以上的作业在同一 时间段 内执行
- 并行:两个及两个以上的作业在同一 时刻 执行
同步和异步
- 同步 :发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待
- 异步 :调用在发出之后,不用等待返回结果,该调用直接返回
什么是进程
进程可以理解为一个正在执行的程序,比如我们电脑中正在运行的微信、浏览器、网易云音乐等。
进程是系统资源分配的最小单位,独占内存空间,保存各自运行状态,相互间不干扰但是可以互相切换。
在 Windows 系统中可以通过查看任务管理器看到系统正在运行的进程,以及资源占用情况。
什么是线程
线程也叫做轻量级进程,是现代操作系统调度的最小单元。在一个进程中可以创建多个线程,每个线程都有自己的计数器、堆栈、局部变量等属性,并且都能访问到共享的内存变量。
一个 Java 程序从 main 方法开始执行,Java 天生就是多线程的程序,因为执行的 main 方法的是一个名称为 main 的线程。
线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的内存资源,相互间切换更快速,支持更细粒度的任务控制,使进程内的子任务得以并发执行。
虽然系统是把资源分给进程,但是 CPU 很特殊,是被分配到线程的,所以线程是 CPU 分配的基本单位。
查看 Java 的线程
/** * 查看 Java 线程 * * @author 薛伟 */ public class HelloWorld { public static void main(String[] args) { // 获取 Java 线程管理 MXBean ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 仅获取线程和线程的堆栈信息 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); for (ThreadInfo threadInfo : threadInfos) { // 打印线程信息 System.out.println(threadInfo.getThreadId() + " " + threadInfo.getThreadName()); } } }
main
: 这是 Java 程序的主线程,即程序启动时执行的线程。Reference Handler
: 负责处理引用对象的清理工作,当垃圾回收器发现有不再被引用的对象时,会将这些对象加入到 Reference Handler 的队列中进行处理。Finalizer
: 处理对象的 finalize 方法,该方法在对象被垃圾回收前调用。Signal Dispatcher
: 负责处理发送给 JVM 的操作系统信号,例如 SIGTERM 等信号。Attach Listener
: 允许工具(如 JConsole)通过 Attach API 连接到 JVM,用于监控和管理 JVM。Monitor Ctrl-Break
: 用于处理来自键盘的中断信号 Ctrl-Break,通常用于终止 JVM 进程。
进程和线程的区别
- 进程是资源分配的最小单位,线程是 CPU 调度的最小单位
- 线程不能看作独立应用,而进程可以看作独立应用
- 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
- 线程没有独立地址空间,多进程的程序比多线程程序健壮
- 进程的切换比线程切换开销大
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
Java 中进程和线程的关系
- Java 对操作系统提供的功能进行封装,包括进程和线程
- 每运行一个程序产生一个进程,进程包含至少一个线程
- 每个进程对应一个 JVM 实例,多个线程共享 JVM 里的堆
- Java 采用单线程编程模型,程序会自动创建主线程
- 主线程可以创建子线程,原则上要后于子线程完成执行
- 一个 Java 程序的运行是一个进程,包括 main 线程和多个其他线程同时运行
主要 API 方法
java.lang.Thread#sleep
当调用线程的 sleep 方法,使当前执行的线程休眠(暂时停止执行)指定的毫秒数,取决于系统计时器和调度程序的精度和准确性。如果任何线程中断了当前线程,会抛出 InterruptedException 异常,并清除当前线程的中断状态。
java.lang.Thread#interrupt
中断目标线程,给目标线程发一个中断信号,线程被打上中断标记。
java.lang.Thread#isInterrupted
判断目标线程是否被中断,不会清除中断标记。
java.lang.Thread#interrupted
判断目标线程是否被中断,会清除中断标记。
java.lang.Thread#yield
使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。CPU 会从众多的可执行态里重新选择(根据线程优先级,如果在当前可运行的所有线程中,当前线程的优先级最大,那么即使调用 yield,后面重新选择的时候还是会选择到当前线程),也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。
public class ThreadYieldDemo { public static void main(String[] args) throws Exception { Thread thread01 = new Thread(() -> { for (int i = 0; i < 10; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + " 线程做出让步,此时 i = " + i); // 使当前线程让出 CPU 使用权 Thread.yield(); } } }); Thread thread02 = new Thread(() -> { for (int i = 0; i < 10; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + " 线程做出让步,此时 i = " + i); // 使当前线程让出 CPU 使用权 Thread.yield(); } } }); thread01.start(); thread02.start(); } }
执行结果如下:
Thread-1 线程做出让步,此时 i = 0 Thread-1 线程做出让步,此时 i = 2 Thread-1 线程做出让步,此时 i = 4 Thread-1 线程做出让步,此时 i = 6 Thread-0 线程做出让步,此时 i = 0 Thread-1 线程做出让步,此时 i = 8 Thread-0 线程做出让步,此时 i = 2 Thread-0 线程做出让步,此时 i = 4 Thread-0 线程做出让步,此时 i = 6 Thread-0 线程做出让步,此时 i = 8
java.lang.Thread#join
一个线程依赖另一个线程,使用 join 方法依赖一个线程进来,当前线程必须等待依赖线程执行完毕,才能继续执行。
public class ThreadJoinDemo { public static void main(String[] args) throws Exception { Thread t1 = new Thread(() -> { System.out.println("t1 线程开始执行"); for (int i = 0; i < 5; i++) { System.out.println(i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("t1 线程结束执行"); }); t1.start(); // 等待 t1.join(); System.out.println("主线程结束"); } }
执行结果如下:
t1 线程开始执行 0 1 2 3 4 t1 线程结束执行 主线程结束
join 方法可以指定一个毫秒参数,表示从当前线程调用 join 开始计时,指定时间若目标线程还没有结束,当前线程就继续往下执行。
java.lang.Thread#setDaemon
将已经创建但是未启动的线程设置为守护线程。
java.lang.Thread#isDaemon
获取当前线程是否为守护线程。
java.lang.Object#wait
在线程获取到锁后,调用锁对象的本方法,线程释放锁并且把该线程放置到与锁对象关联的等待队列(等待线程池)。
wait 方法可以指定一个毫秒参数,等待指定的毫秒数,如果超过指定时间则自动把该线程从等待队列中移出。
java.lang.Object#notify
随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是 notify() 方法仅通知一个线程。
java.lang.Object#notifyAll
使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于 JVM 虚拟机的实现。
调用对象的 wait、notify、notifyAll 方法之前,必须获得对象锁,也就是必须写在 synchronized(obj) 代码段内。调用 notify 或者 notifyAll 后并不会真正释放对象锁, 必须等到 synchronized 方法或者语法块执行完才真正释放锁!
sleep() 方法和 wait() 方法对比
共同点 :两者都可以暂停线程的执行。
区别 :
sleep()
方法没有释放锁,而wait()
方法释放了锁wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()
或者notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒,或者也可以使用wait(long timeout)
超时后线程会自动苏醒sleep()
是Thread
类的静态本地方法,wait()
则是Object
类的本地方法
为什么 wait() 方法不定义在 Thread 中?
wait()
是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object
)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object
)而非当前的线程(Thread
)。
类似的问题:为什么 sleep()
方法定义在 Thread
中?
因为 sleep()
是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
可以直接调用 Thread 类的 run 方法吗?
new 一个 Thread
,线程进入了新建状态。调用 start()
方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()
会执行线程的相应准备工作,然后自动执行 run()
方法的内容,这是真正的多线程工作。 但是,直接执行 run()
方法,会把 run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。
Java 创建多线程
继承 Thread 类
public class XwThread01 extends Thread { @Override public void run() { System.out.printf("线程[%s]运行...%n", getName()); } public static void main(String[] args) { XwThread01 thread1 = new XwThread01(); XwThread01 thread2 = new XwThread01(); thread1.start(); thread2.start(); } }
实现 Runnable 接口
public class XwThread02 implements Runnable { @Override public void run() { System.out.printf("线程[%s]运行...%n", Thread.currentThread().getName()); } public static void main(String[] args) { Thread thread1 = new Thread(new XwThread02()); Thread thread2 = new Thread(new XwThread02()); thread1.start(); thread2.start(); } }
实现 Runnable 接口只是完成了线程任务的编写,启动线程需要使用new Thread(Runnable target).start()
。相比第一种继承 Thread 类的方式,这种方式使用了面向接口,将任务与线程进行分离,有利于解耦。
匿名内部类
public class XwThread03 { public static void main(String[] args) { Thread thread1 = new Thread(new Runnable() { @Override public void run() { System.out.printf("线程[%s]运行...%n", Thread.currentThread().getName()); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { System.out.printf("线程[%s]运行...%n", Thread.currentThread().getName()); } }); thread1.start(); thread2.start(); } }
Lambda 表达式
上面的匿名内部类可以转为更简洁的 Lambda 表达式的写法。
public class XwThread04 { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.printf("线程[%s]运行...%n", Thread.currentThread().getName())); Thread thread2 = new Thread(() -> System.out.printf("线程[%s]运行...%n", Thread.currentThread().getName())); thread1.start(); thread2.start(); } }
中断线程
中断就是线程的一个标识位,它表示一个运行中的线程是否被其他线程调用了中断操作,其他线程可以通过调用线程的 interrupt() 方法对其进行中断操作,线程可以通过调用 isInterrupted() 方法判断是否被中断,线程也可以通过调用 Thread 的 interrupted() 静态方法对当前线程的中断标识位进行复位。
注意:不要认为调用了线程的 interrupt() 方法,该线程就会停止,它只是做了一个标志位。
public class InterruptThread extends Thread { public void run() { while (true){ System.out.println("InterruptThread正在执行"); } } public static void main(String[] args) { InterruptThread interruptThread = new InterruptThread(); interruptThread.start(); // 调用线程的 interrupt() 请求中断操作 interruptThread.interrupt(); // 此时 isInterrupted() 方法返回 true System.out.println("interruptThread是否被中断,interrupt = " + interruptThread.isInterrupted()); } }
示例程序执行结果如下:
可以看到当调用了线程的 interrupt() 方法后,此时调用 isInterrupted() 方法会返回 true,但是该线程还是会继续执行下去。因为 interrupt() 方法并不会中断线程,而是只修改了标志位。
可中断的阻塞
针对线程处于由 sleep、 wait、join、LockSupport.park 等方法调用产生的阻塞状态时,调用 interrupt 方法,会抛出异常 InterruptedException,同时会清除中断标记位,自动改为 false。
不可中断的阻塞
- java.io 包中的同步 Socket I/O
- java.io 包中的同步 I/O
- Selector 的异步 I/O
- sychronized 加的锁
终止线程的运行
一个线程正常执行完 run 方法之后会自动结束,如果在运行过程中发生异常也会提前结束;所以利用这两种情况,我们还可以通过以下三种种方式安全的终止运行中的线程:
利用中断标志位
前面提到了调用了线程的 interrupt() 方法后,此时调用线程的 isInterrupted() 方法会返回 true。当不需要运行 InterruptThread 线程时,通过调用 InterruptThread.interrupt() 使得 isInterrupted() 返回 true,就可以让线程退出循环,正常执行完毕之后自动结束。
public class InterruptThread extends Thread { public void run() { // 利用中断标记位 while (!isInterrupted()){ System.out.println("InterruptThread正在执行"); } } public static void main(String[] args) throws InterruptedException { InterruptThread interruptThread = new InterruptThread(); interruptThread.start(); // 1秒后调用线程的 interrupt() 请求中断操作 sleep(1000); interruptThread.interrupt(); // 此时 isInterrupted() 方法返回 true System.out.println("interruptThread是否被中断,interrupt = " + interruptThread.isInterrupted()); } }
利用一个 boolean 变量
利用一个 boolean 变量和上述方法同理,如下:
public class InterruptThread extends Thread { private volatile boolean isCancel; public void run() { // 利用 boolean 变量 while (!isCancel){ System.out.println("InterruptThread正在执行"); } } public void cancel(){ isCancel = true; } public static void main(String[] args) throws InterruptedException { InterruptThread interruptThread = new InterruptThread(); interruptThread.start(); // 2秒后调用线程的 cancel() 中断线程 sleep(2000); interruptThread.cancel(); } }
当不需要运行 InterruptThread 线程时,通过调用 InterruptThread.cancel() 使 isCancel 等于 true,就可以让线程退出循环,正常执行完毕之后自动结束,这里要注意 boolean 变量要用 volatile 修饰保证内存的可见性。
响应 InterruptedException
通过调用一个线程的 interrupt() 来中断该线程时,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程,例如当你调用 Thread.sleep() 方法时,通常会让你捕获一个 InterruptedException 异常,如下:
public class InterruptThread extends Thread { public void run() { try{ while (true){ // Thread.sleep会抛出 InterruptedException Thread.sleep(100); System.out.println("InterruptThread正在执行"); } }catch (InterruptedException e){ e.printStackTrace(); // JVM会把中断标识位复位为 false System.out.println("当前线程是否被中断,interrupt = " + isInterrupted()); } } public static void main(String[] args) throws InterruptedException { InterruptThread interruptThread = new InterruptThread(); interruptThread.start(); // 1秒后调用线程的 interrupt() 请求中断操作 sleep(1000); interruptThread.interrupt(); } }
当不需要运行 InterruptThread 线程时,通过调用 InterruptThread.interrupt() 使得 Thread.sleep() 抛出InterruptedException,就可以让线程退出循环,提前结束。在抛出 InterruptedException 异常之前,JVM 会把中断标识位复位,此时调用线程的 isInterrupted() 方法将会返回 false。
线程的生命周期和状态
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
- NEW:初始状态,线程被创建出来但没有被调用 start
- RUNNABLE:运行状态,线程被调用了 start 等待运行的状态,Java 线程将操作系统中的就绪和运行两种状态笼统的称作“可运行的”
- BLOCKED:阻塞状态,需要等待锁释放
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待
- TERMINATED:终止状态,表示该线程已经运行完毕
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start()
方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
为什么 JVM 没有区分这两种状态呢? 线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务。但是这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度(也即回到 ready 状态)。线程切换的太快,区分这两种状态就没什么意义了。
当线程执行 wait()
方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。
TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)
方法或 wait(long millis)
方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。
当线程进入 synchronized
方法/块或者调用 wait
后(被 notify
)重新进入 synchronized
方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。
线程在执行完了 run()
方法之后将会进入到 TERMINATED(终止) 状态。
synchronized 后进入 BLOCKED 阻塞状态,但是 java.concurrent 包中的 Lock 接口却是等待状态,因为 Lock 接口对于阻塞的实现使用了 LockSupport 类相关的方法。
转态转换示例程序
线程 sleep 时的状态
public class ThreadState01 { static volatile boolean running = true; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { System.out.println("t1 线程开始执行..."); while (running) { System.out.println("t1 线程将要 sleep"); Thread.sleep(5000L); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 线程执行结束"); }); System.out.println("【1】未调用 start 之前的状态为:" + t1.getState()); t1.start(); System.out.println("【2】调用 start 之后的状态为:" + t1.getState()); Thread.sleep(2000L); running = false; System.out.println("【3】t1 此时的状态为:" + t1.getState()); Thread.sleep(4000L); System.out.println("【4】t1 此时的状态为:" + t1.getState()); } }
运行结果为:
【1】未调用 start 之前的状态为:NEW 【2】调用 start 之后的状态为:RUNNABLE t1 线程开始执行... t1 线程将要 sleep 【3】t1 此时的状态为:TIMED_WAITING t1 线程执行结束 【4】t1 此时的状态为:TERMINATED
当 new Thread 时,线程 t1 的状态为新建状态(NEW),调用 start 之后进入可执行状态(RUNNABLE),执行过程中打印信息后开始睡眠5秒;主线程睡眠2秒后将执行标志设置为 false,结合上面学到的相当于给 t1 线程请求中断操作,睡眠2秒后查看 t1 线程的状态,还在睡,所以状态为超时等待状态(TIMED_WAITING),主线程再睡4秒,此时 t1 已经在前一秒睡醒并结束执行,所以 t1 的状态为终止状态(TERMINATED)。
线程 join 时的状态
public class ThreadState02 { static volatile boolean running = true; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { System.out.println("t1 线程开始执行..."); try { System.out.println("t1 线程将要 sleep"); Thread.sleep(10000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 线程执行结束"); }); Thread t2 = new Thread(() -> { System.out.println("t2 线程开始执行..."); try { System.out.println("t2 线程执行 t1.join(5000L)"); t1.join(5000L); System.out.println("t2 线程执行 t1.join()"); t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2 线程执行结束"); }); System.out.println("【1】t1 此时的状态为:" + t1.getState()); System.out.println("【2】t2 此时的状态为:" + t2.getState()); t1.start(); t2.start(); System.out.println("【3】t1 此时的状态为:" + t1.getState()); System.out.println("【4】t2 此时的状态为:" + t2.getState()); Thread.sleep(1000L); System.out.println("【5】t1 此时的状态为:" + t1.getState()); System.out.println("【6】t2 此时的状态为:" + t2.getState()); Thread.sleep(5000L); System.out.println("【7】t1 此时的状态为:" + t1.getState()); System.out.println("【8】t2 此时的状态为:" + t2.getState()); Thread.sleep(5000L); System.out.println("【9】t1 此时的状态为:" + t1.getState()); System.out.println("【10】t2 此时的状态为:" + t2.getState()); } }
运行结果如下:
【1】t1 此时的状态为:NEW 【2】t2 此时的状态为:NEW 【3】t1 此时的状态为:RUNNABLE 【4】t2 此时的状态为:RUNNABLE t1 线程开始执行... t2 线程开始执行... t2 线程执行 t1.join(5000L) t1 线程将要 sleep 【5】t1 此时的状态为:TIMED_WAITING 【6】t2 此时的状态为:TIMED_WAITING t2 线程执行 t1.join() 【7】t1 此时的状态为:TIMED_WAITING 【8】t2 此时的状态为:WAITING t1 线程执行结束 t2 线程执行结束 【9】t1 此时的状态为:TERMINATED 【10】t2 此时的状态为:TERMINATED
最开始 t1 和 t2 的状态都为新建状态(NEW),调用 start 之后进入可执行状态(RUNNABLE),t1 线程执行睡眠10秒,此时进入线程 t2,在 t2 的线程里面执行 t1.join(5000L)
,接下来 t1 会抢占资源继续执行5秒,主线程睡眠,此时 t1 在睡眠,t2 还在等待 t1,所以 t1 和 t2 的状态都为超时等待状态(TIME_WAITING)。这时主线程睡眠,t2 线程开始执行 t1.join()
,那么 t2 线程进入等待状态(WAITING),等待 t1 线程执行完成后 t2 继续执行。执行到最后,t1 和 t2 都为终止状态(TERMINATED)。
线程 synchronized 时的状态
public class ThreadState03 { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { System.out.println("t1 线程开始执行..."); synchronized (ThreadState03.class) { System.out.println("t1 抢到锁,继续执行"); } System.out.println("t1 线程执行结束"); }); synchronized (ThreadState03.class) { t1.start(); Thread.sleep(3000L); System.out.println("t1 此时的状态为:" + t1.getState()); } } }
执行结果如下:
t1 线程开始执行... t1 此时的状态为:BLOCKED t1 抢到锁,继续执行 t1 线程执行结束
主线程启动,先抢到锁。此时t1.start()
启动了 t1 线程,这时候主线程睡眠,锁还没有释放。此时的 t1 状态为阻塞状态(BLOCKED)需要等待锁释放后继续执行。
线程 wait 时的状态
public class ThreadState04 { public static void main(String[] args) throws InterruptedException { Object o = new Object(); Thread t1 = new Thread(() -> { System.out.println("t1 线程开始执行..."); synchronized (o) { try { System.out.println("t1 抢到锁,将要执行 wait(2000L)"); o.wait(2000L); System.out.println("t1 抢到锁,将要执行 wait()"); o.wait(); System.out.println("t1 抢到锁,继续执行..."); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("t1 线程执行结束"); }); t1.start(); Thread.sleep(1000L); synchronized (o) { System.out.println("【1】t1 此时的状态为:" + t1.getState()); o.notify(); Thread.sleep(1000L); System.out.println("【2】t1 此时的状态为:" + t1.getState()); } Thread.sleep(1000L); System.out.println("【3】t1 此时的状态为:" + t1.getState()); synchronized (o) { o.notify(); System.out.println("【4】t1 此时的状态为:" + t1.getState()); } Thread.sleep(1000L); System.out.println("【5】t1 此时的状态为:" + t1.getState()); } }
执行结果如下:
t1 线程开始执行... t1 抢到锁,将要执行 wait(2000L) 【1】t1 此时的状态为:TIMED_WAITING 【2】t1 此时的状态为:BLOCKED t1 抢到锁,将要执行 wait() 【3】t1 此时的状态为:WAITING 【4】t1 此时的状态为:BLOCKED t1 抢到锁,继续执行... t1 线程执行结束 【5】t1 此时的状态为:TERMINATED
主线程启动后,执行t1.start()
,进入 t1,执行wait(1000L)
,此时 t1 让出锁。在 t1 超时等待的同时,主线程睡眠。之后主线程抢到锁,t1 的状态为超时等待状态(TIMED_WAITING)。这时主线程执行object.notify()
,但这时锁还没有释放,t1 还没有获取到锁,所以 t1 状态为阻塞状态(BLOCKED)。之后主线程释放锁,t1 获得锁,执行object.wait()
,这时 t1 的状态【WAITING】。然后回到主线程,并获得锁,执行object.notify()
,但这时锁还没有释放,t1 还没有获取到锁,所以 t1 状态为阻塞状态(BLOCKED)。最后 t1 获取到锁执行完成,状态变为终止状态(TERMINATED)。
线程的优先级
在 Java 中,每一个线程有一个优先级,在默认的情况下,一个线程继承它父线程的优先级。在操作系统中,线程可以划分优先级,线程优先级越高,获得 CPU 时间片的概率就越大,但线程优先级的高低与线程的执行顺序并没有必然联系,优先级低的线程也有可能比优先级高的线程先执行,但是高优先级线程被运行的总时间总是大于低优先级的线程,就是优先级高,被运行的机会大。
在 Java 中可以通过 setPriority(int newPriority)
方法来设置线程的优先级。
线程的优先级分为 1~10 一共 10 个等级,所有线程默认优先级为 5,如果优先级小于 1 或大于 10,则会抛出 java.lang.IllegalArgumentException
异常。
Java 提供了 3 个常量值可以用来定义优先级,源码如下:
/** * The minimum priority that a thread can have. */ public final static int MIN_PRIORITY = 1; /** * The default priority that is assigned to a thread. */ public final static int NORM_PRIORITY = 5; /** * The maximum priority that a thread can have. */ public final static int MAX_PRIORITY = 10;
实际执行中,大多数情况并不是优先级越高的线程越先执行(我觉得这个优先级屁用都没有…)所以程序的正确性不能依赖线程的优先级高低,因为操作系统可以完全不理会 Java 线程对于优先级的设定。
守护线程
在 Java 中有两类线程:用户线程(User Thread)、守护线程(Daemon Thread)。
所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
将线程转换为守护线程可以通过调用 Thread 对象的 setDaemon(true)
方法来实现。在使用守护线程时需要注意一下几点:
- setDaemon 必须在 start 之前设置,否则会跑出 IllegalThreadStateException 异常。不能把正在运行的常规线程设置为守护线程
- 在守护线程中产生的新线程也是守护的
- 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断
package world.xuewei; /** * 守护线程测试类 * * @author 薛伟 */ public class DaemonThread { public static void main(String[] args) { Thread daemonThread = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("MyDaemonThread finally run"); } }); daemonThread.setDaemon(true); daemonThread.start(); } }
运行上面的程序就发现,控制台并没有输出任何内容。main 线程在启动 daemonThread 后就立即结束了,此时虚拟机中已经没有了非 Daemon 的线程,虚拟机需要立即退出,所有守护线程立即终止,所以 daemonThread 中的 finally 并没有执行。我们不能依赖守护线程的 finally 来执行关闭或者清理资源的逻辑。
笔记大部分摘录自《Java核心技术卷I》、《Java并发编程的艺术》,含有少数本人修改补充痕迹。
参考文章:http://985.so/mm0eh、https://javaguide.cn、http://985.so/mmfh9、http://985.so/m2qnf