Java并发编程-多线程基础(二)
文章目录
- 线程状态
- 等待/阻塞状态
- 启动和终止线程
- 构造线程
- 启动线程
- 中断
- 本质
- 状态管理
- 阻塞方法触发异常
- 终止线程
线程状态
Java线程在运行的生命周期中可能处于6种不同的状态,在给定的一个时刻, 线程只能处于其中的一个状态。
而且会随着代码的执行在不同的状态之间进行切换,Java线程状态 变迁如图示。
1.初始化状态:新建一个线程对象
2.可运行状态(就绪状态):其他线程调用了该线程对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权
3.运行状态:可运行状态的线程获得了cpu 时间片(timeslice),执行程序代码
4.等待状态:运行中的线程执行wait()方法,线程会进入等待队列中。等待notify()、notifyAll()或interrupt()对其唤醒或中断
5.阻塞:运行中的线程需要获取同步锁(注:只有synchronized这种方式的锁(monitor锁)才会让线程出现BLOCKED状态,等待ReentrantLock则不会)时,若该锁已被其他线程占用,线程则会进入锁池队列。等待获取到锁
ReentrantLock
为java.util.concurrent.locks
包中的显式锁,基于 AQS(AbstractQueuedSynchronizer) 实现,其线程等待行为与synchronized
不同:
阻塞机制:未获取锁的线程会进入 AQS 的同步队列,并通过
LockSupport.park()
挂起,线程状态变为WAITING
或TIMED_WAITING
。不涉及 BLOCKED 状态:线程等待显式锁时不会触发 BLOCKED 状态,而是通过 AQS 的队列管理和
park()
方法进入挂起状态。之后讲到锁的时候还会细讲
6.超时等待:运行的线程执行sleep()、join(),或触发了I/O请求,该线程被置为超时等待状态。当sleep()状态超时、join()等待线程终止或超时、I/O处理完成,线程会重新进入可运行状态。
7.终止状态:线程执行完或因异常退出run()方法,线程生命周期结束
等待/阻塞状态
等待队列和锁池都和wait()、notify()、synchronized有关,wait()和notify()又必须由对象调用且必须写在synchronized同步代码块内。
-
等待队列(等待被唤醒):对应等待状态。调用obj的wait()方法,则进入等待队列
-
锁池(等待抢锁):对应阻塞状态。
notify() 是随机唤醒一个等待队列的线程,notifyAll() 是唤醒所有
关于锁的抢占顺序,之后还会具体讲。
公平锁:严格按照线程请求锁的顺序分配锁(FIFO),保证公平性但性能较低。
非公平锁:允许新请求的线程与等待队列中的线程竞争锁,可能导致“线程饥饿”但吞吐量高。
synchronized
是 Java 内置的非公平锁,其竞争策略和行为特点如下:
抢占式竞争:
当锁释放时,新请求线程可能直接抢占锁,即使同步队列中已有等待线程。
示例场景:线程 A 释放锁后,线程 B(新请求)可能与同步队列中的线程 C 直接竞争,线程 B 可能胜出而非依次唤醒队列中的线程。
底层实现依赖 Monitor 锁:
- JVM 通过
monitorenter
和monitorexit
指令管理对象锁,由操作系统的互斥量(Mutex Lock)实现,未设计显式同步队列来维护线程顺序。
面试过程中的常见问题
-
Thread.sleep(long millis) 静态方法。当前线程调用此方法,使当前线程进入超时等待状态,但不释放任何锁资源,一定时间后线程自动进入runnable状态。 这是给其它线程执行机会的最佳方式
-
obj.wait()或obj.wait(long timeout) 当前线程调用某对象的wait()方法,当前线程释放对象锁(wait一定在synchronized代码块/方法中,故一定得到了锁,才能调用这个方法),进入等待队列。等待notify或wait设置的timeout到期,进入阻塞状态(锁池)。
-
t.join()或t.join(long millis) 非静态方法。当前线程A执行过程中,调用B线程的join方法,使当前线程进入超时等待状态,但不释放对象锁,等待B线程执行完后或一定时间millis后,A线程进入runnable状态。
-
Thread.yield() 静态方法。当前线程调用此方法,使线程由running态进入runnable态,放弃cpu使用权,让cpu再次选择要执行的线程。 注:实际过程中,yield仅仅是让其它具有同等优先级的runnable线程获取执行权,但并不能保证它们就一定能获得cpu执行权。因为做出让步的当前线程,可能会被cpu再次选中,进入running状态。yield()不会导致阻塞。
启动和终止线程
在前面的例子中通过调用线程的start()方法进行启动,等run()方法的执行完毕,线程也会终止,接下来我们详细看看线程的启动和终止。
构造线程
在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程的属性,比如线程所属的线程组、线程优先级、是否是Daemon线程等信息。看下源码 ,这段代码是摘自java.lang.Thread中对线程初始化的部分。
Thread.java:
private void init (ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) {if (name == null) {throw new NullPointerException("name cannot be null");}// 当前线程就是该线程的父线程Thread parent = currentThread();this.group = g;// 将daemon、priority属性设置为父线程的对应属性this.daemon = parent.isDaemon();this priority = parent.getPriority();this.name = name.toCharArray();this.target = target; // target 为传入的 Runnable 对象,后续通过 run() 方法调用其逻辑setPriority(priority);// 将父线程的InheritablerhreadLocal复制过来if (parent.inheritableThreadLocals != null)this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals) ;// 分配一个线程IDtid = nextThreadID() ;
}
- 父线程继承
父线程设置:新线程的父线程(
parent
)为 **当前调用该方法的线程**(currentThread()
)。父线程负责分配新线程的初始资源。属性继承:
daemon
属性:继承父线程的daemon
状态(后台线程标记)。
priority
属性:继承父线程的优先级(parent.getPriority()
),并通过setPriority(priority)
显式设定。
- 线程基本信息初始化
名称配置:
name
参数为必填项,转换为字符数组存储(name.toCharArray()
)。执行目标:
target
为传入的Runnable
对象,后续通过run()
方法调用其逻辑。
- 线程上下文传递
InheritableThreadLocal
复制:
若父线程的
inheritableThreadLocals
不为空,则通过ThreadLocal.createInheritedMap()
复制父线程的可继承线程本地变量到新线程。这一机制使得子线程可以继承父线程的线程本地上下文(例如用户身份信息、全局配置等)。
- 线程标识分配
- 线程 ID:通过
tid = nextThreadID()
分配**全局唯一的线程 ID**。nextThreadID()
方法内部使用原子操作保证 ID 唯一性。
- 完整初始化的意义
此
init
方法是 Java 线程对象构造的核心,其行为逻辑包括:
资源继承:确保新线程默认继承父线程的上下文属性(如守护状态、优先级、线程本地变量)。
独立性:通过唯一 ID 和独立栈空间,与父线程解耦,成为可调度的独立执行单元。
在上面过程中,一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent的属性,比如是否为Daemon、优先级(虽然在大部分情况下会被忽略)和ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待运行。
Daemon 线程是一种支持型线程,主要用于执行后台任务(如垃圾回收、日志记录等),不阻止 Java 虚拟机(JVM)退出。
JVM 退出条件:当 JVM 中仅剩 Daemon 线程运行时,虚拟机会直接退出,无论守护线程是否执行完毕。
终止行为:Daemon 线程的终止是强制性的,JVM 不会等待其完成剩余任务,其
finally
代码块也不保证执行,不可依赖finally
块进行资源清理。设置时机:必须在启动线程前通过
setDaemon(true)
方法设置,否则抛出IllegalThreadStateException
启动线程
线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义是:当前线程(即parent线程)告知Java虚拟机,只要线程规划器空闲,应立即启动调用 start()方法的线程。
注意: 启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程 或者进行问题排查时,就会给我们一些提示。
中断
本质
中断是线程的一种协作机制,通过标识位属性标记线程是否被其他线程请求终止。它不是强制终止线程,而是通知目标线程“需要终止”,由目标线程自行决定何时响应。
状态管理
-
设置中断:调用
interrupt()
方法向线程发送中断信号,设置其中断标识位。 -
检查中断:
-
isInterrupted()
:检查线程的中断标识位是否被设置(不修改状态)。 -
Thread.interrupted()
:检查并清除当前线程的中断状态(复位标识位)
-
如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。
看代码:
public class InterruptExample {public static void main(String[] args) throws Exception{MyThread myThread=new MyThread();myThread.start();Thread.sleep(1);myThread.interrupt();Thread.sleep(10);System.out.println("myThread线程是否存活:"+myThread.isAlive());}
}
class MyThread extends Thread {@Overridepublic void run(){int i = 0;while(true){System.out.println("i="+(i++));if(this.isInterrupted()){System.out.println("通过this.isInterrupted()检测到中断");System.out.println("第一个interrupted()"+Thread.interrupted()); //静态方法,当前线程!!!System.out.println("第二个interrupted()"+Thread.interrupted());break;}
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// Thread.currentThread().interrupt();
// }}}
}
结果:
注意:为啥要注释掉 run里面的sleep?
阻塞方法触发异常
调用以下阻塞方法(如 sleep()
、wait()
、join()
)时,若线程已被中断,则抛出InterruptedException
,此时中断标识位会被清除(即 this.isInterrupted()
返回false), 需要在捕获异常里执行 Thread.currentThread().interrupt();
将中断标识位复位。
从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。 就会一直在循环里出不来了。
了解完中断标识位,我们来看看怎么利用它安全地终止线程。
终止线程
我们可以用中断和boolean变量来安全地控制是否需要停止任务,终止线程。
看代码:
import java.util.concurrent.TimeUnit;public class Shutdown {public static void main(String[] args) throws Exception{Runner one = new Runner();Thread countThread = new Thread(one, "CountThread");countThread.start();TimeUnit.SECONDS.sleep(1);countThread.interrupt();//通过中断标志位终止线程Runner two = new Runner();countThread = new Thread(two, "CountThread");countThread.start();TimeUnit.SECONDS.sleep(1);two.cancel();//通过自定义标志位终止线程}private static class Runner implements Runnable{private long i = 0;private volatile boolean on = true;public void run() {while (on && !Thread.currentThread().isInterrupted()) { i++; }System.out.println("Count i: " + i);}public void cancel() {on = false;}}
}
代码里创建了一个线程CountThread,它不断地进行变量累加,而 主线程尝试对其进行中断操作和停止操作。
在执行过程中,main线程通过中断操作和cancel()方法均可使CountThread得以终止。 这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是暴力将线程停止,因此这种终止线程的做法显得更加安全和优雅。