并发编程基础


1.什么是进程?什么是线程?

进程是操作系统资源分配的基本单位。在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main函数所在的线程就是这个进程中的⼀个线程,也称主线程。

线程是进程中的⼀个执⾏单元。⼀个进程可以包含多个线程,这些线程共享进程的内存空间和系统资源。线程是操作系统调度的最⼩单位,它负责执⾏进程中的任务,但是线程的并发执⾏也可能导致⼀些问题,如竞态条件、死锁等。

通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。

从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、虚拟机栈** 和 本地方法栈。

1.1程序计数器为什么是私有的?

程序计数器主要有下面两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

1.2虚拟机栈和本地方法栈为什么是私有的?

  • 虚拟机栈: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。方法调用对应着一个栈帧在 Java 虚拟机栈中入栈,方法执行结束对应着出栈。
  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

1.3一句话了解堆和方法区

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。


2.如何创建线程?

一般来说,创建线程有很多种方式,例如继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等。

不过,这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。

严格来说,Java 就只有一种方式可以创建线程,那就是通过new Thread().start()创建。不管是哪种方式,最终还是依赖于new Thread().start()。


3.线程的生命周期和状态

线程由生到死的完整过程(生命周期):当线程被创建并启动以后,既不是一启动就进入了执行状态,也不是一直处于执行状态,在 API 中 java.lang.Thread.State 这个枚举中给出了六种线程状态:

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征
Runnable(可运行) 线程可以在 Java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法)
Blocked(阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒
Timed Waiting (限期等待) 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait
Teminated(结束) run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡

由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

在操作系统层面,线程有 READY 和 RUNNING 状态,而在 JVM 层面,只能看到 RUNNABLE 状态。因为现在的时分(time-sharing)多任务(multi-task)操作系统通常都是用时间分片方式抢占式轮转调度。这个时间分片很小, 一个线程在CPU上运行比如10-20ms(running),时间片用完就被换下等待调度(redy)。线程切换的如此之快,区分这两种状态就没什么意义了


4.如何预防和避免线程死锁?

产生死锁有4个必要条件:

  • 互斥条件:该资源任意时刻只能有一个线程占有
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:线程已获得的资源在使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
  • 循环等待条件:即存在一个等待循环队列:p1 要 p2 的资源,p2 要 p1 的资源,形成了一个等待环路

要预防死锁,我们破坏死锁产生的必要条件即可:

  • 破坏请求与保持条件:一次性申请所有的资源。
  • 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

5.sleep()方法和wait()方法对比

共同点:两者都可以暂停线程的执行。

区别:

  • sleep()方法没有释放锁,而wait()方法释放了锁。

  • sleep()通常被用于暂停执行,而wait通常被用于线程间交互/通信

  • sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。

  • sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。为什么这样设计呢?下一个问题就会聊到。


文章作者: Fuchanglai
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Fuchanglai !
赏
  目录