并发编程中的一些基础知识
进程和线程
进程
进程是程序运行的基本单位。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
在 Java 中,我们启动 main
函数时其实就是启动了一个 JVM 的线程,而 main
函数所在的线程就是这个进程中的一个线程,也称主线程。
线程
线程是比进程更小的程序执行单位,一个进程在其执行过程中可以产生多个线程。
同一个进程中的多个线程共享一个 堆 和 方法区,但每个线程有自己 独立 的 程序计数器(PC)、虚拟机栈 和 本地方法栈。
二者的区别和联系
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于**各进程基本上是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。**线程执行开销小,但不利于资源的管理和保护;而进程正相反。
线程之间的切换所产生的负担比进程之间的切换要小得多,原因如下:
共享资源:同一进程内的线程共享同一个地址空间、文件描述符和其他资源,不需要像进程切换那样重新加载内存映射和管理数据结构。
上下文保存量少:线程切换只需要保存少量寄存器状态(例如程序计数器、栈指针等),而进程切换需要保存整个进程的上下文,并且往往还需要刷新TLB和切换虚拟内存环境。
Java 线程与操作系统线程的区别和联系
用户线程和内核线程
用户线程创建和切换成本低,但无法利用多核,运行在用户空间。
内核线程创建和切换成本高,但可以利用多核,运行在内核空间。
Java Thread
JDK 1.2 以前,Java的多线程实质上是JVM模拟的多线程运行,并不依赖于操作系统。这种模拟的多线程使得模拟的多线程只能够在一个内核线程上运行。这是一种用户线程。
JDK 1.2 及以后,Java线程改为基于原生线程(Naive Thread)实现,也就是JVM直接调用操作系统的原生线程来实现Java线程,由操作系统内核进行线程的调度及管理。这是一种内核线程。
线程的生命周期
当使用 new 关键字新建一个 Thread,这个 Thread 就进入了新建状态。
当调用这个 Thread 的 start()
方法时,系统就会启动另一个线程并使其进入就绪状态。
当分配到时间片后,这个 Thread 就会开始运行。系统会自动调用该线程 Runnable
接口的 start()
方法。
注意:如果我们直接调用 Thread 的 run()
方法,那么系统就会将其当成一个 main 线程下的普通方法去执行,并不会在某个其他的线程中执行,所以这并不是多线程工作。
Object.wait()
和 Thread.sleep()
的区别
方法 | Thread.sleep() |
Object.wait() |
---|---|---|
类 | Thread 类 |
Object 类 |
锁的影响 | 不释放锁 | 释放锁并进入等待队列 |
线程间通信 | 不支持线程间通信 | 用于线程间通信,通过 notify() 或 notifyAll() 唤醒 |
异常 | 会抛出 InterruptedException |
会抛出 InterruptedException |
使用场景 | 使线程暂停执行一段时间,通常用于延迟 | 用于线程间同步和协作,线程进入等待直到被唤醒 |
区别并发和并行
并发 Concurrent:两个及以上线程在同一时间段执行 (注意是同一时间段,并不是同一时刻)
并行 Parallel:两个及以上线程在同一时刻执行 (只有在多核 CPU 中才存在并行的概念)
我们常说的多线程,其实是指在一台机器上并发、串行地执行任务。
多线程执行可能带来的问题
线程不安全
线程不安全指的是多个线程并发访问共享数据时,程序没有采取适当的同步措施,导致某些数据在访问过程中被破坏,产生不一致的结果。
(补充:线程安全 指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的 正确性 和 一致性。)
内存泄漏(和内存溢出要区分开)
内存泄漏是指程序在运行过程中动态分配了内存,但没有正确释放这些内存,导致内存无法回收,占用更多系统资源,使系统性能下降甚至崩溃。
死锁
死锁是指两个或多个线程在执行过程中,由于 每个线程都在等待某个资源被释放,导致程序进入无法继续执行的状态。
死锁产生的四个必要条件 :互斥操作、不可剥夺、循环等待、请求和保持
预防死锁:
- 破坏请求与保持:一次性申请所有资源
- 破坏不可剥夺:当一个占有资源的线程申请其他资源不成功时,令其主动释放其拥有的资源
- 破坏循环等待:按一定顺序申请资源
避免死锁:事先评估(银行家算法 安全路径)