线程基础

1. 关于本笔记

本文件介绍的是java线程基础的难点知识

2. 目录

多线程简介

Java 多线程是指在一个 Java 程序中同时运行多个线程,这些线程共享程序的内存空间(如全局变量、方法区等),但有各自的栈和程序计数器,能同时执行不同的任务,比如一个线程处理用户输入,另一个线程后台下载文件,提升程序效率。

为什么需要多线程?

众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:

  • CPU 增加了缓存,以均衡与内存的速度差异;// 导致可见性问题
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致原子性问题
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致有序性问题

使用 Java 多线程需要注意以下几点:

  • 首线程安全问题。多个线程同时操作共享数据时,可能出现错误。比如两个线程同时给一个变量加 1,原本该加 2,结果可能只加了 1,这是因为线程切换时没做好数据保护。需要用synchronized关键字、Lock锁等方式,保证同一时间只有一个线程操作共享数据;
  • 线程间通信与同步。线程需要协作时,比如一个线程生产数据,另一个线程消费数据,要通过wait()、notify()等方法控制,避免出现一方没准备好,另一方就操作的情况,否则可能导致数据错误或线程无限等待。通信过程是隐式的, 调用一个方法就能进行, 而同步操作是显示的, 体现为使用锁进行业务处理;
  • 线程的创建和销毁成本。频繁创建和销毁线程会消耗系统资源,影响性能。可以用线程池管理线程,提前创建好一定数量的线程,重复使用,减少资源消耗。

java里面的线程和操作系统的线程一样吗?

进程与线程

用户与内核

Java 底层会调用 pthread_create 来创建线程,所以本质上 java 程序创建的线程,就是和操作系统线程是一样的,是 1 对 1 的线程模型。

进程是资源分配的最小单位, 线程是任务调度的最小单位

操作系统层面的线程:

  • 用户级线程(ULT):

    • 管理位置:完全由用户态的线程库(runtime / 用户代码)来管理,操作系统内核并不知道它们的存在。

    • 调度者:线程库本身,比如早期的 Pthreads 用户态实现。

    • 切换开销:轻量,线程切换只涉及用户态寄存器/栈,不需要内核态切换。

    • 缺点:

      1. 如果一个线程进行系统调用(比如 read 阻塞 I/O),整个进程都会被阻塞,因为内核只看到进程,不知道里面还有别的线程。
      2. 无法真正利用多核 CPU,因为在内核看来依旧是单个进程。

    ➝ M:1 模型(多个用户线程映射到一个内核实体)。

  • 内核级线程(KLT):

    • 管理位置:由操作系统内核来感知和调度。

    • 调度者:OS 内核调度器(调度单位就是线程,而不是进程)。

    • 切换开销:线程切换需要进入内核态,开销比用户线程大。

    • 优点:

      1. 一个线程阻塞不会影响同进程的其他线程(内核能调度别的线程)。

      2. 能够真正利用多核 CPU 并行运行。

    ➝ 1:1 模型(每个用户线程就是一个内核线程)。

  • 混合模型(M:N):

    • 既有用户态线程库,又由内核提供支持,可以将多个用户线程映射到多个内核线程。

    • 灵活,但实现复杂。

    • 现代语言运行时(比如 Go 的 goroutine)就是这种思路。

Java 中的线程(JVM 视角):

Java 线程被实现为内核级线程的包装,即 Java 的 Thread 对象最终是映射到一个 OS 内核线程。

因此:

  • Java Thread.start() -> 内核新建线程。

  • Java Thread.sleep() / wait() / park() -> 底层依赖 OS 的调度和阻塞机制。

  • 多核 CPU 可以真正并行运行多个 Java 线程。

➝ 也就是说,现代 Java 线程是 1:1 KLT 模型。

Java 与操作系统线程模型的对应区别

Java 不再使用 ULT(除了极少数嵌入式/早期 VM),所以 Java 开发者不直接面对用户线程的局限。
但可以通过协程、虚拟线程(Project Loom)重新获得类似 ULT 的轻量调度体验。

Java 线程基本就是 KLT:每个 Thread 对象 <-> 一个内核线程。
因此,Java 线程状态(BLOCKED、WAITING 等)其实是 JVM 在 用户态抽象出来的管理 API,看起来像“进程控制块”,但底层依然靠 OS 线程调度。

并发问题

并发会影响线程的安全, 所以要注意并发三要素:

  • 可见性(CPU缓存引起):
    一个线程对共享变量的修改,另外一个线程能够立刻看到。
    在Java中使用了volatile保证,synchronized和Lock这两个关键字也能确保可见性;

  • 原子性(分时复用引起):
    即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
    在Java中使用了atomic包(这个包提供了一些支持原子操作的类,这些类可以在多线程环境下保证操作的原子性)和synchronized和Lock来确保原子性;

  • 有序性(重排序引起):
    即程序执行的顺序按照代码的先后顺序执行。
    由于指令重排序,该观察结果一般杂乱无序,在Java中通过volatile关键字来保证一定的“有序性”, 通过synchronized和Lock能够保证可见性, JMM内部通过happens-before原则来确保有序性。

保证数据的一致性:

  • 事务管理:使用数据库事务来确保一组数据库操作要么全部成功提交,要么全部失败回滚。通过ACID(原子性、一致性、隔离性、持久性)属性,数据库事务可以保证数据的一致性。
  • 锁机制:使用锁来实现对共享资源的互斥访问。在 Java 中,可以使用 synchronized 关键字、ReentrantLock 或其他锁机制来控制并发访问,从而避免并发操作导致数据不一致。
  • 版本控制:通过乐观锁的方式,在更新数据时记录数据的版本信息,从而避免同时对同一数据进行修改,进而保证数据的一致性。

解决并发问题

使用JMM(Java内存模型)(原文链接:../Jvm体系/java内存模型.md)

JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法
具体来说,这些方法包括:

  • volatile、synchronized 和 final 三个关键字
  • Happens-Before 规则

只要确保可见性,有序性,原子性的实现就行

对于关键字 volatile、synchronized 和 final, 线程关键字(原文链接:../并发/线程关键字.md)文章详细分析了这三个关键字

Happens-Before 规则

上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。

从 JDK5 开始,java 使用新的 JSR -133 内存模型(本文除非特别说明,针对的都是 JSR- 133 内存模型)。JSR-133 提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

happens-before 与 JMM 的关系

  1. 单一线程原则(Single Thread rule)

    在一个线程内,在程序前面的操作先行发生于后面的操作。确保即便JVM内部发生重排序也会保证视觉上的有序。

  2. 管程锁定规则(Monitor Lock Rule)

    一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。即线程A释放锁后线程B拿到锁能够立即看到线程A在锁内的修改。

  3. volatile 变量规则(Volatile Variable Rule)

    对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。确保被修饰的变量修改后立马生效。

  4. 线程启动规则(Thread Start Rule)

    Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。即start这个方法比内部run方法快。

  5. 线程加入规则(Thread Join Rule)

    Thread 对象的结束先行发生于 join() 方法返回。即run内部的方法肯定快于join方法后的那些动作。

  6. 线程中断规则(Thread Interruption Rule)

    对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生(线程 interrupt() 方法一定会被感知)。

  7. 对象终结规则(Finalizer Rule)

    一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始

  8. 传递性(Transitivity)

    如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。

除此之外还要注意以下锁的可重入性, 即同一个线程可以多次获取相同的锁

线程安全

一个类在可以被多个线程安全调用时就是线程安全的。

线程安全的关键点

线程安全不是一个非真即假的命题,可以将共享数据按照安全程度的强弱顺序分成以下五类: 不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

  1. 不可变

    不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满足线程安全。

    不可变的类型:

    • final 关键字修饰的基本数据类型
    • String
    • 枚举类型
    • Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。

    对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class ImmutableExample {
    public static void main(String[] args) {
    Map<String, Integer> map = new HashMap<>();
    Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
    unmodifiableMap.put("a", 1);
    }
    }
    ...
    Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
    at ImmutableExample.main(ImmutableExample.java:9)

    Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。

  2. 绝对线程安全

    不管运行时环境如何,调用者都不需要任何额外的同步措施。

  3. 相对线程安全

    相对线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施。但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。

  4. 线程兼容

    线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。

  5. 线程对立

    线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。

线程安全的实现方法

  1. 互斥同步

    synchronized 和 ReentrantLock。

  2. 非阻塞同步

    互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。

    (一)CAS

    随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略: 先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。

    乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是: 比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。

    (二)AtomicInteger

    J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。

    (三)ABA

    如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。

    J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。

  3. 无同步方案

    要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

    (一)栈封闭

    多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。

    (二)线程本地存储(Thread Local Storage)

    如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

    符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。

    可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。

    (三)可重入代码(Reentrant Code)

    这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。

    可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。

线程使用

线程状态转换

线程状态

新建(New)

创建后尚未启动。

可运行(Runnable)

可能正在运行,也可能正在等待 CPU 时间片。包含了操作系统线程状态中的 Running 和 Ready。

阻塞(Blocking)

等待获取一个排它锁,如果其线程释放了锁就会结束此状态。

无限期等待(Waiting)

等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
进入方法 退出方法
没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
LockSupport.park() 方法 -

限期等待(Timed Waiting)

无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。

调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。

调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。

阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
进入方法 退出方法
Thread.sleep() 方法 时间结束
设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos() 方法 -
LockSupport.parkUntil() 方法 -

死亡(Terminated)

可以是线程结束任务之后自己结束,或者产生了异常而结束。

线程创建

  1. 继承 Thread 类

需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。

当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

编写简单,如果需要访问当前线程,无需使用Thread.currentThread ()方法,直接使用this,即可获得当前线程

  1. 实现Runnable 接口

如果一个类已经继承了其他类,就不能再继承Thread类,此时可以实现java.lang.Runnable接口。实现Runnable接口需要重写run()方法,然后将此Runnable对象作为参数传递给Thread类的构造器,创建Thread对象后调用其start()方法启动线程。

编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

  1. 实现Callable 接口 与 FutureTask

java.util.concurrent.Callable接口类似于Runnable,但Callable的call()方法可以有返回值并且可以抛出异常。要执行Callable任务,需将它包装进一个FutureTask,因为Thread类的构造器只接受Runnable参数,而FutureTask实现了Runnable接口。

编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

  1. 使用线程池

从Java 5开始引入的java.util.concurrent.ExecutorService和相关类提供了线程池的支持,这是一种更高效的线程管理方式,避免了频繁创建和销毁线程的开销。可以通过Executors类的静态方法创建不同类型的线程池。

常见用法

ExecutorExecutor

管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。主要有三种 Executor:
CachedThreadPool: 一个任务创建一个线程;
FixedThreadPool: 所有任务只能使用固定大小的线程;
SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool。

Daemon

守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。

main() 属于非守护线程。使用 setDaemon() 方法将一个线程设置为守护线程。

sleep()

Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

yield()

对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

Thread.currentThread()

Thread.currentThread()方法返回获取当前正在执行这段代码的线程对象,能操作线程状态

interrupt()

通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

每个线程都一个与之关联的布尔属性来表示其中断状态,中断状态的初始值为false,当一个线程被其它线程调用Thread.interrupt()方法中断时,会根据实际情况做出响应。

  • 如果该线程正在执行低级别的可中断方法(如Thread.sleep()、Thread.join()或Object.wait()),则会解除阻塞并抛出InterruptedException异常。
  • 否则Thread.interrupt()仅设置线程的中断状态,在该被中断的线程中稍后可通过手动轮询中断状态来决定是否要停止当前正在执行的任务。

join()

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

wait() notify() notifyAll()

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

它们都属于 Object 的一部分,而不属于 Thread。只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。

使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

notify唤醒一个线程,其他线程依然处于wait的等待唤醒状态;notifyAl将所有线程退出wait的状态,开始竞争锁,但只有一个线程能抢到

notify在源码的注释中说到notify选择唤醒的线程是任意的,但是依赖于具体实现的jvm。notify在源码的注释中说到notify选择唤醒的线程是任意的,但是依赖于具体实现的jvm。

wait() 和 sleep() 的区别:

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
  • wait() 会释放锁,sleep() 不会。

await() signal() signalAll()

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。

相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。使用 Lock 来获取一个 Condition 对象。

  • await():使当前线程进入等待状态,直到被其他线程唤醒。
  • signal():唤醒一个等待在该 Condition 上的线程。
  • signalAll():唤醒所有等待在该 Condition 上的线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class AwaitSignalExample {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();

public void before() {
lock.lock();
try {
System.out.println("before");
condition.signalAll();
} finally {
lock.unlock();
}
}

public void after() {
lock.lock();
try {
condition.await();
System.out.println("after");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}

互斥同步

Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。

synchronized

可以直接修饰普通方法, 同步整个方法的执行, 作用的是当前对象(静态方法的话就是类)

可以作用于代码块, 参数可以是this/class, 作用的是当前对象/整个类

ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。lock()与unlock()方法用来加锁与释放锁

Lock 接口提供了比 synchronized 更灵活的锁机制,Condition 接口则配合 Lock 实现线程间的等待 / 通知机制。

比较

  1. 锁的实现: synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
  2. 性能: 新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
  3. 等待可中断: 当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock 可中断,而 synchronized 不行。
  4. 公平锁: 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
  5. 锁绑定多个条件: 一个 ReentrantLock 可以同时绑定多个 Condition 对象。

使用选择

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

线程机制

如何停止一个线程?

在 Java 中,停止线程的正确方式是 通过协作式的逻辑控制线程终止,而非强制暴力终止(如已废弃的 Thread.stop())。以下是实现安全停止线程的多种方法:

  • **第一种方式:**通过共享标志位主动终止。定义一个 可见的 共享状态变量,由主线程控制其值,工作线程循环检测该变量以决定是否退出。

  • **第二种方式:**使用线程中断机制。通过 Thread.interrupt() 触发线程中断状态,结合中断检测逻辑实现安全停止。

  • **第三种方式:**通过 Future 取消任务。使用线程池提交任务,并通过 Future.cancel() 停止线程,依赖中断机制。

  • **第四种方式:**处理不可中断的阻塞操作。某些 I/O 或同步操作(如 Socket.accept()、Lock.lock())无法通过中断直接响应。此时需结合资源关闭操作。比如,关闭 Socket 释放阻塞。

线程停止的正确实践,如下表格:

方法 适用场景 注意事项
循环检测标志位 简单无阻塞的逻辑 确保标志位使用 volatile 或通过锁保证可见性
中断机制 可中断的阻塞操作 正确处理 InterruptedException 并恢复中断标志
Future.cancel() 线程池管理任务 需要线程池任务支持中断处理机制
资源关闭 不可中断的阻塞操作(如Sockets) 显式关闭资源触发异常,结合中断状态判断回滚

避免使用以下已废弃方法:

  • Thread.stop():暴力终止,可能导致状态不一致。
  • Thread.suspend()/resume():易导致死锁。

blocked和waiting的区别

区别如下:

  • 触发条件:

    线程进入BLOCKED状态通常是因为试图获取一个对象的锁(monitor lock),但该锁已经被另一个线程持有。这通常发生在尝试进入synchronized块或方法时,如果锁已被占用,则线程将被阻塞直到锁可用。

    线程进入WAITING状态是因为它正在等待另一个线程执行某些操作,例如调用Object.wait()方法、Thread.join()方法或LockSupport.park()方法。在这种状态下,线程将不会消耗CPU资源,并且不会参与锁的竞争。

    锁竞争

  • 唤醒机制:

    当一个线程被阻塞等待锁时,一旦锁被释放,线程将有机会重新尝试获取锁。如果锁此时未被其他线程获取,那么线程可以从BLOCKED状态变为RUNNABLE状态。

    线程在WAITING状态中需要被显式唤醒。例如,如果线程调用了Object.wait(),那么它必须等待另一个线程调用同一对象上的Object.notify()或Object.notifyAll()方法才能被唤醒。

所以,BLOCKED和WAITING两个状态最大的区别有两个:

  • BLOCKED是锁竞争失败后被被动触发的状态,WAITING是人为的主动触发的状态
  • BLCKED的唤醒时自动触发的,而WAITING状态是必须要通过特定的方法来主动唤醒

总结

Java 线程基础可以归纳为一条主线:先理解并发为什么会出问题,再用正确的同步与协作手段控制风险。

  1. Java 线程本质上是对操作系统内核线程(1:1 模型)的封装,因此线程切换、阻塞与唤醒都与 OS 调度机制强相关。
  2. 并发的核心风险是可见性、原子性、有序性;工程上通常通过 volatilesynchronizedLock、原子类和 Happens-Before 规则来建立正确的内存语义。
  3. 线程安全不是“安全/不安全”的二元判断,而是从不可变、绝对线程安全、相对线程安全到线程兼容等不同层次,设计时应优先减少共享和可变状态。
  4. 线程状态与协作方法要配套理解:sleep/wait/join/park 决定进入哪类等待状态,notify/notifyAll/signal/signalAll 决定如何恢复执行。
  5. 锁选择以“够用优先”为原则:默认优先 synchronized,需要可中断、公平策略、多条件队列等高级能力时再使用 ReentrantLock
  6. 线程终止应坚持协作式停止(标志位、中断、Future 取消、资源关闭),避免使用 stop/suspend/resume 等破坏一致性的过时方式。

掌握以上原则后,再结合线程池与任务拆分实践,可以在保证正确性的前提下逐步优化并发性能与可维护性。