Java线程知识(二)
一,终断一个线程
1)终断方法
- 使用标志位终止线程
下例running作为一个自定义标志位
标记位设计时应设为外部类的变量(设计到了l变量捕获问题)
public class SafeStopThread extends Thread {private volatile boolean running = true;@Overridepublic void run() {while (running) {try {// 模拟工作System.out.println("Working...");Thread.sleep(1000);} catch (InterruptedException e) {System.out.println("Thread interrupted, stopping...");running = false; // 在捕获中断异常时也停止线程//也可以使用breakbreak;}}System.out.println("Thread stopped safely");}public void stopThread() {running = false;}public static void main(String[] args) throws InterruptedException {SafeStopThread thread = new SafeStopThread();thread.start();// 运行3秒后停止线程Thread.sleep(3000);thread.stopThread();// 也可以选择中断线程// thread.interrupt();}
}
- 使用interrupt() 方法终止线程
public class InterruptThread extends Thread {@Overridepublic void run() {while (!isInterrupted()) {try {System.out.println("Working...");Thread.sleep(1000);} catch (InterruptedException e) {System.out.println("Thread interrupted, exiting");interrupt(); // 重新设置中断状态//或者breakbreak;}}}public static void main(String[] args) throws InterruptedException {InterruptThread thread = new InterruptThread();thread.start();// 运行3秒后中断线程Thread.sleep(3000);thread.interrupt();}
}
2)注意事项
- 在 Java 中,当一个线程被中断时(通过调用 interrupt() 方法),如果线程正处于阻塞状态(如 sleep()、wait()、join() 等),它会立即抛出 InterruptedException 异常,并且中断状态会被清除(将中断状态设为 false)
为了确保循环条件 !isInterrupted() 能够正确检测到中断,我们需要在捕获异常后重新设置中断状态,或者重新设置标记位
public class InterruptExample extends Thread {@Overridepublic void run() {// 使用中断状态作为循环条件while (!isInterrupted()) {try {System.out.println("Working...");Thread.sleep(1000); // 可能在这里被中断} catch (InterruptedException e) {System.out.println("线程被中断,但中断状态已被清除: " + isInterrupted());// 这里没有重新设置中断状态!}}System.out.println("线程退出");}public static void main(String[] args) throws InterruptedException {InterruptExample thread = new InterruptExample();thread.start();// 给线程一点时间开始运行Thread.sleep(100);// 中断线程thread.interrupt();System.out.println("主线程请求中断工作线程");// 等待工作线程结束thread.join(3000);if (thread.isAlive()) {System.out.println("工作线程仍在运行 - 可能出现问题!");} else {System.out.println("工作线程已正常退出");}}/*运行结果
Working...
主线程请求中断工作线程
线程被中断,但中断状态已被清除: false
Working...
Working...
Working...
工作线程仍在运行 - 可能出现问题!
*/
}
使用标志位也会出现上述问题
解决方法:
- 直接在捕获InterruptedException 异常后进行break
- 重新设置标记为或者重新设置中断状态
具体演示在终断方法的代码中
2.使用break解决InterruptedException 异常时,可以结合写一些其他要需要的代码
补充:catch方法的使用一般分为三个方向
- 对问题尝试自动回复(问题不严重易恢复的情况)
- 把错误信息记录于日志 (问题不严重,不着急处理的情况)
- 发出警报(严重问题,要及时处理的情况)
二,获取线程引用
在 Java 中获取当前线程的引用非常简单,可以使用 Thread.currentThread() 方法。下面我将详细解释这个方法并提供示例代码
// 获取当前线程的引用
Thread currentThread = Thread.currentThread();// 可以获取线程的各种信息
String threadName = currentThread.getName();
long threadId = currentThread.getId();
int priority = currentThread.getPriority();
Thread.State state = currentThread.getState();
三,线程休眠
线程休眠是 Java 多线程编程中的一个重要概念,它允许线程暂停执行一段时间。在 Java 中,线程休眠主要通过 Thread.sleep() 方法实现
两种方法,区别在于带nanos参数的精度更高,但一般不需要用到该方法
public static void sleep(long millis) throws InterruptedException
public static void sleep(long millis, int nanos) throws InterruptedException
四,线程安全问题
1,核心定义:
- 当多个线程同时访问一个对象或资源时,无论运行时环境如何调度线程,或者这些线程如何交替执行,这个类(对象)都能表现出正确的行为,那么这个类就是线程安全的。
- 反之,如果一个类在多线程环境下不能表现出正确的行为,我们就说它存在线程安全问题,或者它是非线程安全的
2.线程安全问题发生原因:
1)多线程环境
存在多个线程同时操作,并且Java中线程是抢占式执行,随机调度的,并且这些线程操作的是同一个资源(共享数据、共享变量、共享文件等)
2)非原子性操作
对共享资源的操作是“可分割”的
以下为例子
public class UnsafeCounter {private int count = 0;public void increment() {count++; // 这并非一个原子操作}public int getCount() {return count;}
}
注意:
- 抢占式执行:这是操作系统层面的行为。它描述了CPU如何管理多个线程:“我可以随时打断任何一个线程,并且先执行哪个、后执行哪个是不确定的。” 它只是一个规则,本身不产生正确或错误的结果
- 竞态条件:这是程序逻辑层面的bug。它描述了由于不恰当的代码设计,导致程序结果依赖于不可控的线程执行顺序,竞态条件是代码中的bug,错误之处
- 二者关系,抢占式执行(和随机调度)是竞态条件发生的 催化剂 和 必要条件,但不是其 根本原因(根本原因是代码结构的缺陷)
3)内存可见性问题
在一个线程中修改变量的值后,其他线程可能无法立即(甚至永远)看到这个更新后的值,这听起来违背直觉,但在现代多核计算机架构下,这是出于性能考虑而设计的,但如果不采取正确的同步措施,就会导致程序行为不可预测
public class VisibilityProblem {// 共享变量private static boolean flag = false;public static void main(String[] args) throws InterruptedException {Thread writerThread = new Thread(() -> {try {Thread.sleep(1000); // 模拟一些准备工作} catch (InterruptedException e) {e.printStackTrace();}flag = true; // 1秒后,将flag设置为trueSystem.out.println("WriterThread: Flag is now TRUE");});Thread readerThread = new Thread(() -> {while (!flag) { // 空循环,等待flag变为true}System.out.println("I see the flag is TRUE! Exiting.");});writerThread.start();readerThread.start();writerThread.join();readerThread.join();}
}
-
出现这种问题的原因
并且线程修改flag值时并不是立即写回主存(最终会写回主存),并且写回主存时其余缓存(工作内存)无法立即获取,但是总的来说时间还是很短 -
而如果最终会写回主存又为何最终无法得到正确结果?
原因在于现代编译器和JIT(Just-In-Time)编译器的激进优化
while (!flag) { // 空循环,等待flag变为true
}//在多次读取flag发现没有变化时可能被优化为
if (!flag) { // 只检查一次!while (true) { // 然后进入无限循环// 空循环体}
}
4)内存重排序问题
出现内存重排序的原因是极致的性能优化
体现性能优化的例子:
存在内存重排序问题的简单例子:注意该例子未能真正优化性能,仅作举例
// 共享变量
int x = 0, y = 0;
int a = 0, b = 0;// 线程A执行
public void threadA() {a = 1; // 称为操作 A1x = b; // 称为操作 A2
}// 线程B执行
public void threadB() {b = 2; // 称为操作 B1y = a; // 称为操作 B2
}
此时如果将ab声明为volatile
// 共享变量
int x = 0, y = 0;
volatile int a = 0; // 添加 volatile
volatile int b = 0; // 添加 volatile// 线程A执行
public void threadA() {a = 1; // 操作 A1 - volatile 写x = b; // 操作 A2 - volatile 读
}// 线程B执行
public void threadB() {b = 2; // 操作 B1 - volatile 写y = a; // 操作 B2 - volatile 读
}
5)JMM的浅了解
JMM 规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory)
- 主内存: 对应于物理硬件的部分内存,是共享区域。所有线程创建的实例对象都存放在主内存中,无论该实例是成员变量还是局部变量(当然,局部变量是线程私有的,但实例本身在堆中是共享的)
- 工作内存: 这是一个抽象概念,并不真实存在。它涵盖了 CPU 的缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。每个线程都有自己的工作内存,其中存储了该线程需要使用到的变量的主内存副本
3.线程安全问题的解决方案
监视器锁(解决原子性问题)
1)监视器锁的定义与语法规则
监视器锁(Monitor Lock),这是 Java 中最基本、最常用的锁机制。它通过 synchronized 关键字实现,可以用于修饰方法或代码块
语法规则:
synchronized(一个对象){
加锁内容
}
public static void main(String[] args) {//1.自建锁对象Object locker = new Object();Thread t = new Thread(() -> {synchronized (locker) {System.out.println("hello");}//2.锁对象使用类对象// synchronized (this) {// System.out.println("hello");//}});t.start();}
2)可重入锁
-
可重入锁,也叫做递归锁,指的是同一个线程可以多次获取同一把锁,而不会产生死锁。
-
核心机制:锁内部维护了一个持有计数器(Hold Count) 和一个持有线程标识。
-
当线程第一次获取锁时,计数器变为 1,记录持有线程
每当这个线程再次获取同一把锁时,计数器就递增
每当线程释放锁时,计数器就递减
只有当计数器减到 0 时,锁才被真正释放,其他线程才能获取 -
synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题;
-
死锁:当线程尝试重新获取一个它已经持有的不可重入锁时,它会陷入永久的等待状态,因为它在等待自己释放锁,而释放锁的代码又在等待获取锁的操作完成
例:
public static void main(String[] args) {Object locker = new Object();Thread t = new Thread(() -> {synchronized (locker) {//如果是不可重入锁此时将进入死锁状态synchronized (locker) {System.out.println("hello");}}});t.start();}
3)死锁
- 定义:死锁是指两个或两个以上的进程(或线程)在执行过程中,由于竞争资源或彼此通信而造成的一种相互等待的现象。若无外力干涉,这些进程都将无法向前推进(简单来说就是由于锁导致进程或线程一直处于阻塞状态无法运行)
- 死锁产生的四个必要条件:
1)互斥:一个资源每次只能被一个进程使用(巧克力和小花生酱只能一个人拿着,不能同时两个人一起拿)
2)占有并等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放(小明拿着巧克力,但还在等花生酱。小红拿着花生酱,但还在等巧克力)
public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(() -> {synchronized (A) {// sleep 一下, 是给 t2 时间, 让 t2 也能尝试拿到 Btry {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 尝试获取 B, 并没有释放 Asynchronized (B) {System.out.println("t1 拿到了两把锁!");}}});Thread t2 = new Thread(() -> {synchronized (B) {// sleep 一下, 是给 t1 时间, 让 t1 能尝试拿到 Atry {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 尝试获取 A, 并没有释放 Bsynchronized (A) {System.out.println("t2 拿到了两把锁!");}}});t1.start();t2.start();}
3)不可剥夺:进程已获得的资源,在未使用完之前,不能被其他进程强行剥夺,只能由该进程主动释放(你不能从小明手里把巧克力抢过来,只能等他主动放下)
4)循环等待:存在一种进程资源的循环等待链,链中的每一个进程都在等待下一个进程所占有的资源(明在等小红的东西,小红又在等小明的东西。形成了一个“循环”,两个人互相等)
4)哲学家就餐问题(并由此引入死锁的解决方案 )
由于除了循环等待以外的必要条件都是锁本身的特性,因此解决死锁的常用方法是破坏循环等待
以下方法称为资源排序法
注意:至于是谁先吃饭不一定,这是由于随机调度与抢占式执行决定的,资源排序法的核心是通过统一拿筷子的顺序来避免循环等待,而不是保证特定哲学家先吃饭。谁先吃饭取决于运行时条件,但死锁一定被避免,一定有一个哲学家能正常吃饭
5)注意
- 锁对象可以通过自建也可以通过this使用类对象(例如上述代码如果是出于Test类中获取的即Test的类对象 )
- 确保两个线程间的安全要使用同一锁对象,简单来说即synchronized()括号中对象相同
例:下方代码已经失去了多线程提高效率的意义(和串行执行几乎没有差异),仅作为便于理解的例子
public class SynchronizedCounter {private int count = 0;public void increment() {count++;}}public class SynchronizedExample {public static void main(String[] args) throws InterruptedException {SynchronizedCounter counter = new SynchronizedCounter();// 创建两个线程,每个线程增加计数器1000次Thread thread1 = new Thread(() -> {synchronized(this) {for (int i = 0; i < 1000; i++) {counter.increment();}}});Thread thread2 = new Thread(() -> {synchronized(this) {for (int i = 0; i < 1000; i++) {counter.increment();}}});// 启动线程thread1.start();thread2.start();}
}
由于两个线程++操作都进行了加锁,一个线程进行++操作时另一个线程将进入阻塞状态(BLOCKER)
避免了线程安全问题
3. 有时synchronized会在方法定义时直接使用
public synchronized void increment() {//...}
//等价于
synchronized(this) {public void increment() {//.... }
}
volatite关键字
volatile 是一个轻量级的同步机制
它主要解决了两大问题:
- 内存可见性
- 禁止指令重排序
1)对于内存可见性的解决
当一个变量被声明为 volatile 后:
写操作:当写一个 volatile 变量时,JMM(Java 内存模型)会立即将该线程工作内存中的新值强制刷新到主内存中。
读操作:当读一个 volatile 变量时,JMM 会使该线程工作内存中的缓存失效,从而强制它直接从主内存中重新读取该变量的最新值。
简单来说,volatile 保证了每次读变量都直接从主内存读,每次写变量都立即写到主内存。 这就保证了不同线程对这个变量操作的可见性。
2)对于指令重排序的解决
volatile 通过插入 内存屏障 来禁止 JVM 和处理器对 volatile 变量相关的指令进行重排序
内存屏障的作用:
写屏障:确保在屏障之前的、对 volatile 变量和普通变量的所有写操作,都不会被重排序到屏障之后。在写 volatile 变量后,会插入一个写屏障,强制将所有写缓冲区的数据刷新到主内存。
读屏障:确保在屏障之后的、对 volatile 变量和普通变量的所有读操作,都不会被重排序到屏障之前。在读 volatile 变量前,会插入一个读屏障,使工作内存中的缓存失效,从主内存加载数据。
4.Java标准库中的线程安全类
- StringBuilder,ArrayList,LinkedList,HashMap,HashSet,TreeMap,TreeSet都是线程不安全的
- Vector(不推荐使用),HashTable(不推荐使用),ConcurrentHashMap,StringBuffer,String是线程安全的,都自带锁(不代表完全没有可能出问题)
五,线程状态
- 线程对象通过 new Thread() 创建后,在调用 start() 方法之前,就处于这个状态
- 调用了 start() 方法后,线程就进入了 RUNNABLE 状态
- 有且仅有一种情况 会让线程进入 BLOCKED 状态:线程正在等待获取一个由 synchronized 关键字保护的临界区的监视器锁(Monitor Lock)
重要区别:BLOCKED 状态针对的是 synchronized 锁。对于 java.util.concurrent.locks.Lock 接口下的锁(如 ReentrantLock),线程等待锁时的状态是 WAITING 或 TIMED_WAITING(因为底层用的是 LockSupport.park()) - 线程进入 WAITING 状态后,除非被其他线程显式地唤醒,否则会无限期地等待下去
进入此状态的场景(调用以下方法且不带超时参数)
1)Object.wait(): 当前线程在对象上等待,直到其他线程调用该对象的 notify() 或 notifyAll() 方法
2)Thread.join(): 等待目标线程执行终止。例如在主线程中调用 t.join(),主线程会进入 WAITING 状态,直到线程 t 执行完毕
3)LockSupport.park(): JUC(java.util.concurrent)包中锁的底层实现 - TIMED_WAITING (超时等待)是 WAITING 状态的一个超时版本。线程将等待一段指定的时间,时间到了会自动唤醒,或者在等待期间被其他线程唤醒
与wait的差别就是调用带参数的,会引起waiting的方法