并发基础

92 阅读7分钟

什么是线程和进程 ?

进程是系统进行资源分配和调度和系统运行程序的基本单位,系统运行一个程序即是一个进程从创建,运行到消亡的过程

在 Java 中,启动 main 函数就是启动了一个 JVM,而main函数所在的线程就是这个进程主线程。

线程:程序执行的最小单位。一个进程在其执行的过程中可以产生多个线程。

请简要描述线程与进程的关系,区别及优缺点 ?

各进程是独立的,而同一进程中的线程极有可能会相互影响。

线程执行开销小,但不利于资源的管理和保护;而进程正相反。

多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。

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

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

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。

  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

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

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

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

每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

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

堆和方法区

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

线程的生命周期和状态

  • 线程创建之后它将处于NEW(新建)状态,调用start()方法后开始运行,线程这时候处于 READY(可运行) 状态。
  • 可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
  • 当线程执行 wait()方法之后,线程进入WAITING(等待)状态,进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态
  • 而 TIME_WAITING(超时等待)状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。
  • 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。
  • 线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。
  1. NEW状态 ——start()——> READY状态 ——获取CPU时间片——> RUNNING状态 ——的run()——> TERMINATED状态
  2. NEW状态 ——start()——> READY状态 ——获取CPU时间片——> RUNNING状态 ——wait()——> WAITING状态
  3. NEW状态 ——start()——> READY状态 ——获取CPU时间片——> RUNNING状态 ——sleep()——> TIMED WAITING状态
  4. NEW状态 ——start()——> READY状态 ——获取CPU时间片——> RUNNING状态 ——调用同步方法时而没有获取到锁——> BLOCKED状态

什么是上下文切换 ?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

什么是线程死锁?如何避免死锁?

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

死锁条件

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁条件

  1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  2. 破坏请求与保持条件 :一次性申请所有的资源。
  3. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

说说 sleep() 方法和 wait() 方法区别和共同点?

两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。

两者都可以暂停线程的执行。 Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。

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

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new 一个 Thread,线程进入了新建状态;调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。

start() 会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作

而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。