Day 21 线程知识补充
-
线程中断机制
其他线程通过调用某个正在执行线程A的**interrupt()**方法对其进行安全中断操作,调用后不代表A会立即停止自己的工作,它也可以拒绝中断请求,通过检车自身的中断标志位是否被设置为true来进行响应。
线程通过isInterrupted()方法或者Thread.interrupted()判断是否被中断,后者会同时将中断标识位改为false
如果线程处于阻塞状态(sleep(),join(),obj.wait()),在线程检查发现中断标识为true时,会抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,重新设置为false。(死锁线程无法被中断)
-
Java线程模型
-
线程调度机制
-
协同式线程调度Cooperative Threads-Scheduling: 线程执行由线程本身控制,线程把自己的工作执行完成之后,主动通知系统切换到另一个线程上。 好处:实现简单,没有线程同步问题 坏处:如果某一个线程出了问题,其他线程就没法执行,会一直阻塞
-
抢占式线程调度Preemptive Threads-Scheduling: 由操作系统控制线程中断,按策略分配CPU时间片,线程无法独占。(Java使用) 好处:线程执行可控,单个线程出问题不会导致整个系统瘫痪,高优先级任务可以及时抢占CPU
坏处:上下文切换开销大,执行顺序不确定,实现复杂
-
-
线程的实现
-
内核线程(1:1)实现
内核线程是直接由操作系统支持的线程,由内核控制线程切换,通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
由于内核线程的支持,每个线程都是一个独立的调度单元,即使某个线程被阻塞,也不影响整个进程工作,后续相关的调度操作系统也会处理好
局限性:由于是基于内核线程实现,所以各种线程操作都需要经过操作系统,在用户态和内核态之间来回切换代价较高;
-
用户线程(1:N)实现
严格意义上的用户线程是完全建立在用户空间的线程库上,系统内核感知不到用户线程的存在及实现,创建、同步、销毁和调度都不需要内核参与。
用户线程的优势在于不需要系统内核参与,消耗低,操作快速;但劣势也在此,所有线程操作都需要用户程序自己处理。
-
混合(N:M)实现
即存在用户线程,也存在内核线程,集两者之所长
Java在JDK2之前是用户线程实现,3之后改成了内核线程实现,全权交给了系统进行调度,JVM无法干涉,所以有时候Java设置的线程优先级无法准备的和操作系统中的线程优先级一一对应
-
-
虚拟线程(协程)
JDK21推出的革命性技术,是JVM管理的轻量级线程,旨在解决传统线程内存开销大、上下文切换慢、受限于内存的并发瓶颈等问题,提高并发能力的同时无需消耗更多资源。虚拟线程在 sleep()、read()、accept() 等阻塞操作时会自动挂起,不占用 OS 线程,当 I/O 完成后,JVM 自动将其调度回某个 Carrier Thread 继续执行。
▼java复制代码//Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread private static void createThread() { for (int i = 0; i < 10_000; i++) { new Thread(() -> { // 同样的 I/O 操作 try { Thread.sleep(1000); } catch (Exception e) { } System.out.println("Done"); }).start(); } } private static void createVirtualThread() { // Java 21+ try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 10_000; i++) { executor.submit(() -> { // 同样的 I/O 操作 try { Thread.sleep(1000); } catch (Exception e) { } System.out.println("Done"); return null; }); } } // 自动等待所有任务完成 }-
使用场景
适合I/O密集型任务 如Web服务器、数据库查询、外部调用、文件读写
不适合CPU密集型任务 视频编码、科学计算等
-
使用注意事项
- 不需要池化技术,用完即弃,创建成本低
- 避免使用synchronized锁,会导致Carrier Thread阻塞,需改用reentrantLock或无锁设计
- 慎用ThreadLocal,虚拟线程数量多,容易导致内存泄露
-
-
-
线程通信
- volatile 轻量通信,加上volatile关键字,保证不同的线程对这个变量操作时的可见性,但无法保证线程安全
- 等待/通知机制
-
Object.wait() 调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放对象的锁
-
Object.notify() 通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。
-
Objecct.notifyAll() 通知所有等待在该对象上的线程。尽可能用notifyAll(),谨慎使用notify(),因为notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。
▼java复制代码public class WaitDemo { public static void main(String[] args) throws InterruptedException { Object locker = new Object(); Thread t1 = new Thread(() -> { try { System.out.println("wait开始"); synchronized (locker) { locker.wait(); } System.out.println("wait结束"); } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); //保证t1先启动,wait()先执行 Thread.sleep(1000); Thread t2 = new Thread(() -> { synchronized (locker) { System.out.println("notify开始"); locker.notifyAll(); System.out.println("notify结束"); } }); t2.start(); } } -
LoclSupport 是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”。
▼java复制代码public class LockSupportDemo { public static void main(String[] args) throws InterruptedException { Thread parkThread = new Thread(new Runnable() { @Override public void run() { System.out.println("ParkThread开始执行"); // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行 LockSupport.park(); System.out.println("ParkThread执行完成"); } }); parkThread.start(); Thread.sleep(1000); System.out.println("唤醒parkThread"); // 给线程 parkThread 发放『许可』(多次连续调用 unpark 只会发放一个『许可』) LockSupport.unpark(parkThread); } }
-
-
Callable&Future&FutureTask
-
背景:直接继承Thread或者实现Runnable接口都可以创建线程,但是这两种方法都没有返回值,也就不能获取执行完的结果。因此java1.5提供了Callable接口来实现这一场景,而Future和FutureTask就可以和Callable接口配合起来使用。
▼java复制代码@FunctionalInterface public interface Runnable { public abstract void run(); } @FunctionalInterface public interface Callable<V> { V call() throws Exception; }- Runnable 的缺陷:
- 不能返回一个返回值
- 不能抛出 checked Exception
Callable的call方法可以有返回值,可以声明抛出异常。和 Callable 配合的有一个 Future 类,通过 Future 可以了解任务执行情况,或者取消任务的执行,还可获取任务执行的结果。
▼java复制代码new Thread(new Runnable() { @Override public void run() { System.out.println("通过Runnable方式执行任务"); } }).start(); FutureTask task = new FutureTask(new Callable() { @Override public Object call() throws Exception { System.out.println("通过Callable方式执行任务"); Thread.sleep(3000); return "返回任务结果"; } }); new Thread(task).start(); System.out.println(task.get()); - Runnable 的缺陷:
-
Future 的API
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
- boolean cancel (boolean mayInterruptIfRunning) 取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束
- boolean isCancelled () 任务是否已经取消,任务正常完成前将其取消,则返回 true
- boolean isDone () 任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true
- V get () throws InterruptedException, ExecutionException 等待任务执行结束,然后获得V类型的结果。InterruptedException 线程被中断异常, ExecutionException任务执行异常,如果任务被取消,还会抛出CancellationException
- V get (long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException 同上面的get功能一样,多了设置超时时间。参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计算超时,将抛出TimeoutException
-
FutureTask
FutureTask是Future和Runnable的实现,该对象相当于是消费者和生产者的桥梁,消费者通过 FutureTask 存储任务的处理结果,更新任务的状态:未开始、正在处理、已完成等。而生产者拿到的 FutureTask 被转型为 Future 接口,可以阻塞式获取任务的处理结果,非阻塞式获取任务处理状态
▼java复制代码public class FutureTaskDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { Task task = new Task(); //构建futureTask FutureTask<Integer> futureTask = new FutureTask<>(task); //作为Runnable入参 new Thread(futureTask).start(); System.out.println("task运行结果:"+futureTask.get()); } static class Task implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("子线程正在计算"); int sum = 0; for (int i = 0; i < 100; i++) { sum += i; } return sum; } } } -
Future的局限性
- 并发执行多任务时,只能用get()方法获取结果,并且是阻塞的
- 无法组合多个任务进行链式调用
- 没有异常处理能力,每个get()调用都要手动catch
-
