Java线程知识拾遗

知识回顾

进程与线程是常常被提到的两个概念。进程拥有独立的代码段、数据空间,线程共享代码段和数据空间,但有独立的栈空间。线程是操作系统调度的最小单位,通常一个进程会包含一个或多个线程。多线程和多进程都可以实现并发处理,如 nginx 使用多进程方式、tomcat 使用多线程方式、Apache 支持混合使用。在 C/C++ 等语言中可以同时使用多进程和多线程,而在 Java 中只能使用多线程。

在 Java 中,创建线程的唯一方式是创建 Thread 类的实例,调用实例的 start() 方法启动线程。

Java 线程实现

在 JDK 1.2 之前,Java 使用用户线程实现 Java 线程,在 JDK 1.2 及之后,Java 基于操作系统原生的线程模型实现 Java 线程。

使用用户线程( User Thread, UT ) 实现,是指线程建立在用户态空间,线程的建立、同步、调度与销毁都在用户态完成,进程与用户线程之间是1 : N 的对应关系。这种情况下,内核无法知道有多少个用户线程,实现较为复杂。

使用内核线程实现,是指基于轻量级进程( Light Weight Process, LWP ) 来实现线程。每个轻量级进程都有一个内核线程( Kernel-Level Thread, KLT ) 支持,与内核线程之间是 1 : 1 的对应关系。这种情况下,调度线程时可能需要在内核态和用户态之间进行切换。由于轻量级进程需要消耗内核资源,能够支持的线程数量是有限的。

如在 Windows 和 Linux 系统中,操作系统原生的线程模型是 1 : 1 的对应关系,对于 Sun JDK 来说,一个 Java 线程就对应着一个轻量级进程。

线程调度与状态

在 Java中线程的调度方式是抢占式调度,即由系统来负责各个线程的时间分配,并在线程使用完分配的时间后调度下一个线程。任何一个线程都不能独占 CPU 。Java 语言一共设置了 10 个线程优先级,当两个线程同时等待执行时,优先级高的先被调度。线程的优先级会被映射到操作系统原生线程上去,但各个操作系统的优先级划分不完全一样,因此两个优先级不同的Java 线程在操作系统中执行时也可能处于相同的优先级。

Java 定义了 5 种线程状态,分别是新建 ( New )、运行 ( Running )、等待 ( Waiting )、限期等待 (Timed Waiting )、阻塞 ( Blocked ) 和结束 ( Terminated )。任一时刻,线程都处于 5 种状态中的一种,并在各个状态之间切换,如图所示。

其中,各个状态含义如下:

新建:创建后未启动;

运行:对于 Java 来说,线程已经运行,但对于操作系统来说,可能在运行或等待;

等待:线程等待被其他线程唤醒,如调用了 wait、join 且没有指定超时时间;

限期等待:线程等待一段时间后被系统唤醒,如调用了 sleep、wait、join 并设置了超时时间;

阻塞:线程进入同步区域需要与其他线程协调同步,如需要进入 synchronized 区域但其他线程尚未退出此区域;

结束:run 方法执行完成后,线程结束。

虚拟机栈

在 Java 内存模型中,每个虚拟机线程都有自己私有的虚拟机栈。栈与线程同时创建,其中存储的是线程的栈帧 ( Stack Frame )。每个方法的调用,都对应着一个栈帧的入栈和出栈。在栈帧中,存储着局部变量表 ( Local Variable Table )、操作栈 ( Operand Stack )、动态连接 ( Dynamic Linking )、返回地址 ( Return Address ) 和其他附加信息。

线程的工作内存

在内存模型中,Java 要求所有的变量都必须存储在主内存中,每个线程拥有自己的工作内存。工作内存中保存了线程需要读写的变量的主内存的副本。线程对变量的读写操作都在工作内存中直接进行,并不会去操作主内存中的内容,主内存与工作内存的同步由虚拟机完成。不同线程不能访问彼此的工作内存,变量值的传递需要经过主内存才能完成。

Volatile 修饰的变量可以保证变量对所有线程可见,即某个线程修改变量后,其他线程总能立刻读到新值。即便如此,多线程并发时,对 volatile 变量进行自增自减操作也不能保证线程安全。

总结

线程在 Java 中只能通过创建 Thread 类的实例来创建。在 JDK 1.2 之后,Java 中的线程基于操作系统原生的线程模型来实现线程。线程的调度方式是抢占式调度,即由系统来负责各个线程的时间分配,并在线程使用完分配的时间后调度下一个线程。Java 定义了 5 种线程状态:新建、运行、等待、限期等待、阻塞和结束。

每个虚拟机线程都有自己私有的虚拟机栈。栈与线程同时创建,其中存储的是线程的栈帧。每个方法的调用,都对应着一个栈帧的入栈和出栈。每个线程拥有自己的工作内存,工作内存中保存了线程需要读写的变量的主内存的副本。线程对变量的读写操作都在工作内存中直接进行,并不会去操作主内存中的内容,主内存与工作内存的同步由虚拟机完成。

每周 3 篇学习笔记或技术总结,面向有一定基础的 Java 程序员,内容涉及 Java 进阶、虚拟机、MySQL、NoSQL、分布式计算、开源框架等多个领域。关注作者或微信公众号 backend-develop 第一时间获取最新内容。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,384评论 11 349
  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,751评论 0 11
  • 下面是我自己收集整理的Java线程相关的面试题,可以用它来好好准备面试。 参考文档:-《Java核心技术 卷一》-...
    阿呆变Geek阅读 14,914评论 14 507
  • Java SE 基础: 封装、继承、多态 封装: 概念:就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽...
    Jayden_Cao阅读 2,156评论 0 8
  • 人生就像一场戏,因为有缘才相聚。 相扶到老不容易,是否更该去珍惜。 为了小事发脾气,回头想想又何必。 别人生气我不...
    李青塬阅读 630评论 0 0