Java 线程
本文最后更新于:2024年3月18日 凌晨
Java 线程
线程与进程
- 进程(Process) :是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,在当代面向线程设计的计算机结构中,进程是线程的容器,程序是指令,数据及其组织形式的描述,进程是程序的实体。
- 线程(thread) :是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务,与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器,虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
进程与线程的区别
- 线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元,而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务,在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位,常用的Windows,Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。
- 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销,线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的,线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
- 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
- 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉,所以多进程要比多线程健壮。
- 执行过程:每个独立的进程有程序运行的入口,顺序执行序列和程序出口,但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
- Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下:
1 |
|
- 上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可):
1 |
|
- 从上面的输出内容可以看出一个 Java 程序的运行是 main 线程和多个其他线程同时运行
多进程与多线程
-
进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。
-
多进程:大多数操作系统允许创建多个进程,当一个程序因等待网络访问或用户输入而被堵塞时,另一个程序还可以运行,这样就增加了资源利用率,但是进程切换要占用较多的处理器时间和内存资源,也就是多进程开销大,而且进程间的通信也不太方便,大多数操作系统不允许进程访问其他进程的内存空间。
-
多线程:多线程则指的是在单个程序中可以同时运行多个不同的线程,执行不同的任务,因为线程只能在单个进程的作用域内活动,所以创建线程比创建进程要廉价得多,同一类线程共享代码和数据空间,每个线程有独立的运行栈,线程切换的开销小,因此多线程编程在现代软件设计中被大量采用。
-
具体采用哪种方式,要考虑到进程和线程的特点。
- 和多线程相比,多进程的缺点在于:
- 创建进程比创建线程开销大,尤其是在Windows系统上。
- 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
- 而多进程的优点在于:
- 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
- 和多线程相比,多进程的缺点在于:
线程的状态
-
Java中线程的状态分为6种。
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为"运行”
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法,该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready),就绪状态的线程在获得CPU时间片后变为运行中状态(running) - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
-
这6种状态定义在Thread类的State枚举中,可查看源码进行一一对应。
线程的状态图

状态详细说明
- 初始状态(NEW)
- 实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。
- 就绪状态(RUNNABLE之READY)
- 就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
- 调用线程的start()方法,此线程进入就绪状态。
- 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
- 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
- 锁池里的线程拿到对象锁后,进入就绪状态。
- 运行中状态(RUNNABLE之RUNNING)
- 线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态,这也是线程进入运行状态的唯一的一种方式。
- 阻塞状态(BLOCKED)
- 阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
- 等待(WAITING)
- 处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
- 超时等待(TIMED_WAITING)
- 处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
- 终止状态(TERMINATED)
- 当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了,这个线程对象也许是活的,但是它已经不是一个单独执行的线程,线程一旦终止了,就不能复生。
- 在一个终止的线程上调用start()方法,会抛出
java.lang.IllegalThreadStateException
异常。
同步队列,等待队列与就绪队列
- 就绪队列存储了将要获得锁的线程。
- 同步队列:进入Synchronized方法块(同步方法)时竞争锁的时候失败,则进入同步队列。
- 等待队列:比如线程调用了wait()方法,线程则进入等待队列,等待被唤醒再进入同步队列。
线程有关的方法
thread.sleep(long millis)
:一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态,作用:给其它线程执行机会的最佳方式。thread.yield()
:一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程,作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中,Thread.yield()不会导致阻塞,该方法与sleep()类似,只是不能由用户指定暂停多长时间。thread.join()/thread.join(long millis)
:当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁,线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)obj.wait()
:当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列,依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。obj.notify()
:唤醒在此对象监视器上等待的单个线程,选择是任意性的。obj.notifyAll()
:唤醒在此对象监视器上等待的所有线程。LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines)
:当前线程进入WAITING/TIMED_WAITING
状态,对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING
状态,需要通过LockSupport.unpark(Thread thread)
唤醒。
线程调度与优先级
- Java提供一个线程调度器来负责线程调度,Java采用抢占式调度策略,在程序中可以给每个程序分配一个线程优先级,优先级高的线程优先获得调度,对于优先级相同的线程,根据在等待队列的排列顺序按"先到先服务”原则调度,每个线程安排一个时间片,执行完时间片将轮到下一线程。
- 下面几种情况中,当前线程会放弃CPU:
- 当前时间片用完。
- 线程在执行时调用了
yield()
或sleep()
方法主动放弃。 - 进行I/O访问,等待用户输入,导致线程阻塞,或者为等待一个条件变量,线程调用
wait()
方法。 - 有高优先级的线程参与调度。
- 线程的优先级用数字来表示,范围从1~10,主线程的默认优先级为5,其他线程的优先级与创建它的父线程的优先级相同,为了方便,Thread类提供了如下几个常量来表示优先级:
Thread.MIN_PRIORITY=1
Thread.MAX_PRIORITY=10
Thread.NORM_PRIORITY=5
从 JVM 角度说进程和线程之间的关系
- 下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。

- 从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区(JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器,虚拟机栈和本地方法栈。
- 程序计数器为什么是私有的?
- 程序计数器主要有下面两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行,选择,循环,异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
- 所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
- 程序计数器主要有下面两个作用:
- 虚拟机栈和本地方法栈为什么是私有的?
- 虚拟机栈:每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表,操作数栈,常量池引用等信息,从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务,在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
- 所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
- 堆和方法区
- 堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象(所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!