线程

线程是CPU资源调度的最小单位,主流操作系统都有成熟的线程模型,应用层经常提到的线程的概念大多是对应于内核线程的,所以不同的编程语言一旦引入了线程,那么基本上就是照搬了内核线程的概念。线程本身也不是编程语言实现的——线程调度需要由操作系统控制。

Runnable

Runnable不是线程,它只是一个接口定义了一个方法,Thread启动才是一个线程。

Thread

thread.start()方法

一个Thread的实例一旦调用start()方法,这个实例的started标记就标记为true,事实中不管这个线程后来有没有执行到底,只要调用了一次start()就再也没有机会运行了,这意味着:通过Thread实例的start(),一个Thread的实例只能产生一个线程。

thread.join()方法

Thread的join(方法可用于让当前线程阻塞)以等待特定线程的消亡,或者是说把生特定线程合并到当前线程。这是一种相对原始的线程间通信形式,但有时却有用。

join是可以响应中断的。

在主线程中调用子线程的join方法,主线程会阻塞等待子线程执行完成,主线程只是在阻塞等待目标线程结束,任务仍在子线程中执行,属于线程同步的基本机制。调用 thread.join() 时,当前线程会进入 WAITING 状态(通过 Object.wait() 实现),并释放 CPU 资源,直到目标线程终止(或超时)。这期间它不消耗 CPU,但逻辑上“卡住不动”——不是卡死,是主动让出、安静等待。

thread.toString()方法

thread.toString方法输出的线程信息分别是:线程名称、线程优先级、线程所在线程组名称。

每个Thread必然存在于一个ThreadGroup中,Thread不能独立于ThreadGroup存在。执行main()方法线程的名字是main,如果在new Thread时没有显式指定,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组。

线程挂起

不恰当的挂起时机包括:

  • 在线程锁定了共享资源时挂起线程,可能会发生死锁条件(deadlock condition)。
  • 在长时间计算期间,这一过程通常不应该半途而废,除非响应中断主动退出。

线程中断

待决中断

待决中断‌(Pending Interrupt)是指‌线程在调用阻塞方法(如 Thread.sleep()Object.wait() 或 Thread.join())之前已经被中断‌的情况。当线程在调用这些方法时,如果发现其中断状态为 true,就会立即抛出 InterruptedException 异常,而不会真正进入阻塞状态。

运行结果分析:
  • 由于主线程在调用 sleep() 之前就调用了 interrupt(),所以线程处于“待决中断”状态。
  • 当 sleep() 方法被调用时,它检测到中断标志为 true,于是立即抛出 InterruptedException
关键点总结:
  • 待决中断发生在调用阻塞方法前已经设置了中断标志。
  • 阻塞方法检测到中断后会立即抛出 InterruptedException
  • 这种机制允许线程在进入长时间等待前被中断。

中断响应

在耗时方法中,应加入检查线程是否被中断,如果被中断则中止计算,并抛出InterruptedExeption。

thread.isInterrupted()判断是否被中断。

Thread.interrupted()判断是否被中断并消除中断标志。

thread.interrupted()中断线程thread。

当响应中断后,如果线程不需要停止执行,则需要重新声明中断标记,以便如果存在多个可中断语句时,其它语句可继续响应中断。

线程中循环语句中应该加入判断退出标记,并捕获可中断语句,控制是退出执行还是继续执行。

线程同步

线程同步一般是要解决在“单对象多线程”的情况下,控制共享变量的访问,或是控制执行步骤顺序。
控制共享变量的策略:
<ul>
     <li>将“单对象多线程”修改成“多对象多线程”;</li>
     <li>将“全局变量”降级为“局部变量”;</li>
     <li>使用ThreadLocal机制,它用于解决线程间共享变量,使用ThreadLocal声明的变量,即使在线程中属于全局变量,针对每个线程来讲,这个变量也是独立的。</li>
     <li>用final域,有锁保护的域和volatile域可以避免非同步的问题。</li>
</ul>

控制执行步骤
执行步骤可以使用synchronized关键字来解决。
java的每个对象都有一个内置锁,当用synchronized关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则调用者就停在阻塞状态。

volatile 易变成员变量修饰符、特殊域修饰符

用volatile修饰的成员变量,会通知JVM它的值可能被多个线程同时修改和使用,线程中可以实时看到它被修改后的新值。如果没有标记为volatile,在while循环中将不会看到新值。

每次线程访问该volatile变量时,强迫它从共享内存中重读变量的值。而且,当变量发生变化时,强迫线程将变化值回写到共享内存。大部分人希望这种行为来自Java的VM。
Java语言规范表明,为了获得最佳速度,允许线程保存共享成员变量的工作拷贝,而且只是偶而用共享的原始值来校准。"偶而"是指"当线程进入或离开同步代码块时"。当多个线程同时与某个对象交互时,必须确保一个线程对共享成员的改变能让另一个线程知道。
volatile关键字用于告诉VM:线程不应当保存此变量的私有拷贝,而应当直接与共享拷贝交互

使用特殊域变量(volatile)实现线程同步

  • volatile关键字为域变量的访问提供了一种免锁机制
  • 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
  • 因此每次使用该域就要重新计算,而不是使用寄存器中的值
  • volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

关闭内置Just-In-Time(JIT)编译器,使用如下命令:java -Djava.compiler=NONE Volatile,如果关闭Just-In-Time编译功能,所有线程会直接读写共享内存,不会保存变量的私有拷贝,就像每个变量都声明为volatile一样,但这样性能会低。

volatile修饰符的存在要求VM总是访问变量的共享拷贝。与VM通过保存一份私有拷贝来执行优化相比,它的效率要低。只在必需时使用volatile,滥用 volatile会导致不必要地降低应用程序的执行速度。

Sun在VM中包含JIT后,每个线程进入或离开同步块,它都会让变量的私有拷贝与共享拷贝一致。synchronized代码块广泛分布在java.*类库中,因此,开发人员可能没有意识到私有拷贝已经被校准了。例如,System.out.println0就包含一个synchronized块,所以使用它打印volatile value,会保持私有变量为最新值,而volatile修饰符似乎不需要发挥作用,volatile没有关键性的区别。

技巧

在两个或更多线程访问的成员变量上使用volatile,除非所有的线程都访问synchronized代码块内的变量。如果成员变量构建后保持为常量(即只读),就没有必要声明为 volatile。

Java5中的concurrent工具包

JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
concurrent工具包中也提供了线程池,分为3类:ScheduledThreadPool、FixedThreadPool和CachedThreadPool。
有些情况下需要使用线程的返回值,可以使用Callable和CompletionService,前者返回单个线程的结果,后者返回一组线程的结果。
在concurrent工具包中,我们可以使用BlockingQueue来实现生产者-消费者模型。


使用信号量来控制线程:

JDK提供了Semaphore来实现“信号量”的功能,它提供了两个方法分别用于获取和释放信号量:acquire和release.
可以用synchronized关键字来控制单个线程中的执行步骤,要对线程池中的所有线程的执行步骤进行控制的,有两种方式,一种是使用CyclicBarrier,一种是使用CountDownLatch。
CyclicBarrier使用了类似于Object.wait的机制,它的构造函数中需要接收一个整型数字,用来说明它需要控制的线程数目,当在线程的run方法中调用它的await方法时,它会保证所有的线程都执行到这一步,才会继续执行后面的步骤。
CountDownLatch则是采取类似”倒计时计数器”的机制来控制线程池中的线程,它有CountDown和Await两个方法。


使用重入锁实现线程同步

ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
关于Lock对象和synchronized关键字的选择:
如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码。
如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。

局部变量实现线程同步


如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal与同步机制
ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
ThreadLocal采用以"空间换时间"的方法,同步机制采用以"时间换空间"的方式

Future

java1.5中Future是一个未来对象,里面保存线程处理结果,它像一个提货凭证,可以随时去提取结果。Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
在两种情况下离开Future较难处理:
<ul>
     <li>一种情况是拆分订单,比如应用收到一个批量订单,此时如果要求最快的处理订单,那么需要并发处理,并发的结果如何收集的问题如果自己编程非常繁琐,此时可以使用CompletionService解决这个问题。CompletionService将Future收集到一个队列里,可以按结果处理完成的先后顺序进队。</li>
     <li>另一种情况是如果需要并发查询一些东西(比如爬虫),并发查询只要有一个结果返回就认为查询到了,并且结束查询,这时也需要用CompletionService和Future来解决。</li>
</ul>

线程的取消

最简单的就是不断检查线程的中断标志来实现任务的取消响应。

JDK最初提供了停止线程的API,但它很快就被废弃 了,因为强行停止一个线程会导致该线程中持有的资源无法正常释 放,进而出现不安全的程序状态。

“挂起”“阻塞”和“等待”的区别

Java线程中的“阻塞”和“挂起”概念都有线程暂停执行、不再占有cpu资源,在这一点上是相同的。但在触发方式、恢复机制、安全性、使用场景等方面存在差异。现在标准Java线程中已没有严格称为挂起的状态。

核心区别

  • 触发方式不同

    • 阻塞(Blocked)‌:是‌线程在获取资源时被动‌发生的。例如线程试图获取一个已被其他线程持有的同步锁(synchronized),或等待I/O操作完成。
    • 挂起(Suspended)‌:是‌线程主动‌暂停执行。历史上通过 Thread.suspend() 实现,但该方法‌已被废弃‌,因其容易引发死锁。
    • 等待: 线程抢到了锁进入了同步代码块,某些条件下Object.wait()或join()了,就处于了等待状态。(此时线程已经进入了同步代码块)。是一种主动行为,不知道它什么时候被阻塞,也不清楚它什么时候会解除阻塞。
  • 恢复机制不同

    • 阻塞‌:当所等待的资源可用(如锁被释放、I/O完成)时线程会‌自动恢复‌到就绪状态。
    • 挂起‌:需‌主动调用 resume()‌ 恢复。但因 suspend() 和 resume() 已废弃,现代Java不推荐使用此方式。
  • 安全性与锁处理

    • 阻塞‌:线程在阻塞时‌不会释放已持有的锁‌。这是安全的,符合Java同步机制设计。
    • 挂起‌:被挂起的线程‌仍持有其占用的锁‌。若其他线程依赖这些锁,就可能造成‌死锁‌,因此 suspend() 被视为不安全操作。
  • 现代替代方案

    若需暂停线程,应使用以下‌安全机制‌:
    • Thread.sleep():定时暂停,不释放锁。
    • Object.wait() / notify():基于对象监视器的协作式等待通知线程间通信方式,会释放锁。
    • LockSupport.park() / unpark():底层轻量级线程调度工具,可安全挂起/恢复。

Java线程状态对应关系

操作/状态 Java Thread.State 说明
阻塞(等待锁) BLOCKED 等待进入 synchronized 块或方法 ‌19
等待(无超时) WAITING 调用 wait()join() 或 LockSupport.park()
超时等待 TIMED_WAITING 调用 sleep()wait(timeout)join(timeout)
挂起(历史) 无(suspend() 已废弃) 不再推荐使用,JVM中无对应标准状态

注意:现代Java中,“挂起”更多指代 park() 这类机制,而非旧版 suspend()park() 是安全的,常用于 java.util.concurrent 包的底层实现。


总结

  • 不是一回事‌:阻塞是‌被动等待资源‌,挂起(若指 suspend)是‌主动且危险的暂停‌。
  • 挂起已废弃‌:Thread.suspend() 和 resume() 在JDK早期就被标记为废弃,‌不应在生产代码中使用‌ 。
  • 推荐做法‌:使用 wait/notifyLockSupport 或并发工具类(如 CountDownLatchSemaphore)实现线程协调。

线程优先级

调用线程的start后,线程是否立即执行还会受到线程优先级的影响。

默认优先级:

  • main线程为5。
  • Finalizer线程为8,以帮助垃圾回收器得到更多相会运行并释放内存。
  • AWT-EventQueue-0事件处理线程为6。

线程优先级常量:

  • Thread.MIN_PRIORITY,值为1,此优先级运行机会少。
  • Thread.MAX_PRIORITY,值为10,此优先级会独占处理器。
  • Thread.NORM_PRIORITY,值为5,此优先级适中。

新建线程的优先级默认与其构建者线程同级,即线程优先级具有继承性。应当给频繁阻塞的线程分配较高优先级,给需要较多时间计算的线程分配中低等优先级,确保处理器不会被独占。

线程状态

VM实现使用操作系统的线程规划器。

  • 准备运行:
  • 正运行:
  • 休眠:阻塞可响应中断可设置时间,苏醒或中断后准备运行。
  • 等待:阻塞可响应中断可设置超时,接到通知或等待超时或中断后准备运行。
  • 阻塞于IO:阻塞不响应中断不可设置超时,有数据到达后准备运行。
  • 阻塞于同步:阻塞不响应中断不可设置超时,获得锁后准备运行。

yield 主动让出

当线程休眠或阻塞时会隐式放弃处理器,而Thread.yield()则可以让线程在方便的时间显示放弃处理器。频繁阻塞的线程没有必要调用yield,而执行长时间非阻塞运算的线程可以偶尔调用yield让出处理器资源。

线程调用静态方法yield后,纯种规划器把它交换出处理器,让和它的优化先相等或更大的线程来运行。

执行线程间上下文切换会导致系统额外开销,不要在1秒内调用yield超过5次。

yield() 方法

  • 作用‌:建议当前线程主动让出 CPU 执行权,从‌运行状态转为就绪状态‌,供其他线程调度。
  • 关键特性‌:
    • 非强制性‌:只是向调度器发出“建议”,操作系统可能立即重新调度该线程继续执行。
    • 不释放锁‌:与 sleep() 一样,不会释放任何已持有的锁。
    • 优先级影响‌:更可能让给‌同优先级或更高优先级‌的线程,但不保证。
    • 不确定性高‌:不适合用于生产环境中的精确线程协调。
  • 典型用途‌:调试、性能测试中减少单线程占用,‌不推荐用于生产逻辑

局部变量拷贝

未采用同步措施时,同一个对象的方法内可以同时有多个线程,每个线程都保存有自己的局部变量拷贝。

synchronized

当线程碰到 synchronized实例方法时,就会一直阻塞到可以排它性访问对象级别的互斥锁(mutex lock)为止。互斥(Mutex)是互相排斥(imutual exclusion)的缩写。互斥锁在某一时刻只能由一个线程持有。当释放该锁时,所有等待的线程均竞争排它性访问权限。只有个线程可以竞争成功,其他线程将重新回到阻塞状态,并再次等待锁的释放。如果对象上的一个synchronized方法调用同一个对象上的另一个synchronized方法,它不会阻塞来竞争对象级别的锁,因为它已经获得了排它性访问锁的权限。

同一个对象实例的所有synchronized方法都是锁定的同一个自身实例,所有synchronized方法中只能有一个方法同时被一个线程执行。类的每个实例都有自己的对象级别锁,不同实例的相同方法它们锁定的是不同的对象级别锁,所以它们不会线程访问互斥。

持有一个对象级别锁不会阻止线程被交换出来,即如果发生阻塞它一样会被线程交换。如果它被交换出来,它将继续持有对象级别锁。必须同时确保阻塞所有的读来小心数据不一致。

synchronized的原理和数据库中事务锁的原理类似。在使用过程中应该尽量缩减synchronized覆盖的范围,原因是被它覆盖的范围是串行的,效率低,并且容易产生死锁。当没有必要同步整个方法时,可使用synchronized代码块同步关键代码。

同步语句块

synchronized块可用于减少持有对象级别锁的时间。只限制到关键的需要访问成员变量的工作,来缩短持有锁的时间。同步语句块可以引用VM中的任意对象mutex,不仅仅是this,例如还可以同步类级别锁。

synchronized(ClassA.class){

}

静态同步方法

对于类的每个实例,除了存在对象级别锁,还存在类级别锁,类级别锁被类的所有实例共享。VM装载的每个类只有一个类级别锁。如果方法既是静态又是同步则线程在进入方法前,必须获得排斥性访问类级别锁的权限,类级别锁可用于控制static成员变量的并发访问。

线程组

在Java中,所有的线程都属于某个TheadGroup实例。线程组拥有一个名字以及与它相关的一些属性,可以用于作为一个组来管理其中的线程。线程组能够组织VM的线程,可以提供一些组间安全性。一个ThreadGroup内可以包含另外的ThreadGroup。与Thread不一样,线程组必须指定一个名字,无选项自动生成名字。默认新Thread对象与构建它的线程位于同一个线程组。

如果线程不再存活,getThreadGroup返回null,而不是返回一个ThreadGroup。

ThreadGroup的interrupt方法可用于通知组及其子组中所有线程的中断。

为了防止主线程退main0方法,该线程等待一个任意对象得到永远不会到达的通知。

守护线程

守护线程的本质是 ‌“随主而生,随主而亡”‌ 的辅助角色,适用于‌非关键、可中断、低优先级‌的后台任务。合理使用可提升程序响应性和退出效率,但滥用可能导致数据丢失或资源未释放等问题。

典型应用场景

  • 垃圾回收(GC)‌:JVM 自带的垃圾回收线程就是典型的守护线程。
  • 定时任务‌:如定期清理临时文件、发送心跳包。
  • 日志记录‌:异步写入日志到磁盘或远程服务器。
  • 监控与健康检查‌:微服务中向注册中心(如 Nacos、Eureka)上报心跳。
  • 连接池维护‌:清理空闲数据库连接或超时 Socket。

线程模型

线程模型主要分为 ‌KLT(内核级线程)模型‌ 与 ‌ULT(用户级线程)模型‌。

KLT 模型(内核级线程)

  • 定义‌:由操作系统内核直接支持和管理,每个线程是内核中的一个调度实体。
  • 特点‌:
    • 内核感知线程存在,可独立调度。
    • 线程阻塞不会导致整个进程阻塞。
    • 可充分利用多核 CPU 并行执行。
    • 上下文切换需陷入内核态,开销较大。
  • 典型实现‌:Linux 的 pthread、Java 线程(JVM 使用 1:1 映射)‌‌。

ULT 模型(用户级线程)

  • 定义‌:完全在用户空间实现,内核对线程无感知,线程管理由用户态线程库(如 pthread 用户层)完成。
  • 特点‌:
    • 创建、切换、销毁开销小,无需系统调用。
    • 调度可自定义,灵活性高。
    • 致命缺陷‌:任一线程阻塞(如 I/O)会导致整个进程阻塞;无法利用多核并行‌‌。
  • 典型实现‌:早期 Java 绿色线程、Go 的 goroutine(早期版本)、Python 的 greenlet‌‌。

补充说明:混合模型

现代系统(包括 Java)普遍采用 ‌混合模型‌(如 M:N 映射),结合两者优势:

  • 用户线程由运行时管理(低开销)。
  • 映射到少量内核线程,支持并行与避免进程阻塞‌‌。
  • Java 虚拟线程‌(JDK 21+)即基于此模型,实现高并发低开销‌‌。

当前主流 Java 线程仍为 ‌KLT 模型‌(1:1 映射),但虚拟线程正在推动向混合模型演进‌‌。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐