Back
Featured image of post [并发编程]基础篇

[并发编程]基础篇

什么是进程?什么是线程?

进程是程序的一次执行过程,是系统运行程序的基本单位,进程是一个系统对一个程序从创建,运行到消亡的过程。

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

线程是程序的一个更小的执行单位,一个进程在执行过程中可以产生多个线程。不同在与,同类的多个线程可以共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,系统在线程之间切换的代价要比进程小得多。

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

从JVM的角度说明

一个进程中包括多个线程,线程可以共享进程的堆和方法区,但是每个线程都有自己的虚拟机栈,本地方法栈,程序计数器

线程是进程划分成的更小的运行单位,区别在与进程基本上是各自独立的,而同一进程的不同线程则可能会相互影响。线程开销更小,但是不利于资源的管理和保护;进程则相反。

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

程序计数器主要作用:

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

所以程序计数器私有主要是为了线程切换回来后,能从原来停止的位置正确恢复运行。

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

  • 虚拟机栈:每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、函数返回地址和参数、划定栈帧范围的ebp和esp指针等信息。一个方法从被调用到执行完成,就对应着一个栈帧在Java虚拟机中入栈和出栈的过程。
  • 本地方法栈:和虚拟机栈作用差不多,区别在与虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为Native方法用的。在HotSpot虚拟机中两者合二为一。

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

简单介绍下堆和方法区

堆和方法区都是线程共享的资源。

是进程中最大的一块内存,主要用于存放新创建的对象,几乎所有的对象都在这里分配内存。

方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

说说并发和并行的区别?

  • 并行:在单位时间内多个任务同时进程,一般在是多核CPU上出现,
  • 并发:在同一段时间内,多个任务由CPU切换着处理,在某一瞬间是不一定同时处理多个任务的。例外就是当我们CPU使用了因特尔的超线程技术,一个内核被虚拟成2个逻辑内核,当两个任务分别用到CPU的不同运算资源时,比如一个任务计算整数另一个计算浮点数,这个时候就又可能是真的同时进行的。

为什么要使用多线程?

从总体上说:

  • 从计算机底层的角度,线程是轻量级的进程,是程序执行的最小单位,线程切换的开销要远小于进程的切换,另外多核CPU时代意味着多个线程可以同时运行,再次减少了开销。
  • 从当代互联网的发展趋势角度:现在的系统基本上就动辄百万千万级的并发,多线程技术就是高并发系统的基础,可以大大提高系统整体的性能和并发能力。

从计算机底层来说:

  • 单核时代:如果我们只有一个线程,那请求进程IO就会阻塞我们整个进程,而CPU就会被闲置了,如果由多个线程,那我们可以在一个线程被IO阻塞的时候,用另一个线程继续使用CPU的运算能力,提高系统整体的资源利用效率。
  • 多核时代:如果有多个核心,那多个线程可以映射不同核心上并行执行,在没发生资源争抢的情况下执行效率就会显著提高。

使用多线程可能会带来什么问题?

  • 内存泄漏:ThreadLocal就可能会导致内存泄漏
  • 死锁:两个线程互相占用对方需要的资源并互相等待其释放
  • 线程不安全:多线程访问并修改临界资源

说说线程的生命周期和状态?

状态名称 说明
NEW 初始状态,线程被构建,但是还没有调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其它线程作出一些特定动作(通知或中断)
TIME_WAITING 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
TERMINATED 终止状态,表示当前线程已经执行完毕

线程随着代码的执行在不同状态之间切换,Java线程状态变迁如下所示:

(文字描述省略,要用自己的话能描述出下面这副图)

什么是上下文切换?

线程在执行过程中有自己的运行条件和状态(也成上下文),比如程序计数器,栈帧的一些信息,出现如下情况的时候,线程会从占用CPU状态中退出:

  • 调用了sleep(), wait(),主动让出CPU
  • 时间片用完 ,操作系统要防止一个线程或者进程长时间占用CPU导致其它线程饿死
  • 调用了阻塞类的系统中断,比如请求IO,线程被阻塞
  • 被终止或结束运行

前三种都会发生线程切换,这意味这要保存线程的上下文,留着线程下次占用CPU的时候恢复现场,并加载下一个要占用CPU的线程上下文。

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

认识线程死锁

死锁就是多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放,而资源却又同时被对方占用锁住,此时线程就会无限期等待,程序就无法正常终止。

死锁的代码例子(代码来源于《并发编程之美》):

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

代码中先让线程A获取资源1的锁,如何sleep1秒让系统切换运行线程B,线程B获取资源2的锁,然后线程A和线程B都在尝试获取被对方占用着的资源,陷入互相等待的状态,因此也造成了死锁。

造成死锁的四个必要条件

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

如何预防死锁?

破坏死锁产生的必要条件即可

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

如果避免死锁?

避免死锁就是在资源分配时,借助算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

安全状态指的是系统能够按照某种进程推进顺序(P1、P2、P3…..Pn)来为每个进程分配所需资源,直到满足每个进程对资源的最大需求,使每个进程都可顺利完成。称<P1、P2、P3…..Pn>序列为安全序列。 上述代码线程B的获取资源顺序改为资源1→资源2就可以解决死锁,因为线程A首先获取了资源1,线程B再去获取就得不到了,就阻塞在这里,然后资源2也不会被它获取,而是被线程A顺利获取。线程A使用完后释放资源1和2线程B也可以正常获取,这就破坏了循环等待的条件,避免了死锁。

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

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

两者的主要区别在于:

  • sleep()方法不会释放锁,而wait()方法释放了锁。
  • wait()方法通常用于线程间交互/通信,sleep()主要用于暂停线程的执行
  • wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成之后线程会自动苏醒。或者可以用wait(long timeout)超时后线程会自动苏醒.

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

new一个Thread进入新建状态,然后调用start() 方法就会启动这个线程进入就绪状态,当分配到时间片就可以开始执行了。start()会执行线程的一些准备工作,然后就会自动执行run()方法中的内容,这就是真正的多线程工作。

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

参考

Java 并发常见知识点&面试题总结(基础篇)

栈帧(Stack Frame) - 掘金

超线程技术在线程这个层面是否是真正的空间并行?

comments powered by Disqus
一辈子热爱技术
Built with Hugo
Theme Stack designed by Jimmy
gopher