美文网首页java学习之路
JavaGuide知识点整理——并发常见知识点

JavaGuide知识点整理——并发常见知识点

作者: 唯有努力不欺人丶 | 来源:发表于2022-07-18 23:02 被阅读0次

    什么是线程和进程?

    何为进程?

    进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行,到消亡的过程。
    在java中,当我们启动main函数的时候其实就是启动了一个jvm的进程,而main函数所在的线程就是这个进程中的一个线程,也称为主线程。
    如下图,我们在windows系统查看任务管理器,就可以很清楚的看到当前运行的进程:


    进程

    何为线程?

    线程与进程相似,但是线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区的资源。但是每个线程又有自己的程序计数器,虚拟机栈和本地方法栈。所以系统在产生一个线程,或者在各个线程之间切换工作的时候,负担比进程要小得多,也正因为如此,线程也被称为轻量级进程。
    java程序天生就是多线程程序,我们可以通过jmx来看一下一个普通的java程序有哪些线程,代码如下:


    获取java程序线程

    这几个线程的作用如下:
    [6] Monitor Ctrl-Break idea启动main方法特有的一个守护线程
    [5] Attach Listener 添加事件
    [4] Signal Dispatcher 分发处理给JVM信号的线程
    [3] Finalizer 调用对象的finalize方法的线程
    [2] Reference Handler 清除reference线程
    [1] main main线程,程序入口

    从上面能看出来java程序的运行是main线程和多个其他线程同时运行。

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

    图解进程和线程的关系

    java内存区域

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

    总结:线程是京城划分成的更小的运行单位,线程和进程最大的不同在于进程基本上是独立的,而线程则不是。因为同一进程中的线程会相互影响。线程执行开销小,但是不利于资源的管理和保护,而进程正相反。

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

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

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

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

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

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

    • 虚拟机栈:每个java方法在执行的同时会创建一个栈帧用于存储局部变量表,操作数栈,常量池引用等信息。从方法调用直至执行完毕的过程,就对应着一个栈帧在java虚拟机栈中入栈和出栈的过程。
    • 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机使用到的native方法服务。在hotSpot虚拟机中和java虚拟机栈合二为一。

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

    一句话简单了解堆和方法区

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

    为什么要使用多线程呢?

    先从总体上来说:

    • 从计算机底层来说:线程可以比作是轻量级的进程,是程序执行的最小单位,线程之间的切换和调度的成本远远小于进程,另外多核CPU时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
    • 从当代互联网发展趋势来说:现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正式开发高并发系统的基础。利用好多线程机制可以大大提高系统整体的并发能力以及性能。

    在深入到计算机底层来探讨:

    • 单核时代:在单核时代多线程主要是为了提高进程利用CPU和IO系统的效率。假设只运行了一个java进程的情况,当我们请求IO的时候,如果java进程中只存在一个线程,此线程被IO阻塞则整个进程被阻塞。
      CPU和IO设备只有一个在运行,那么可以简单的说系统整体效率只有百分之50,当使用多线程的时候,一个线程被IO阻塞,其他线程还可以继续使用CPU,从而提高了java进程利用系统资源的整体效率。
    • 多核时代:多核时代多线程主要是为了提高进程利用多核CPU的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话。不论系统有几个cpu核心,都只会有一个CPU核心被利用到。而创建多个线程,这些线程可以被映射到底层多个CPU上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著的提高。约等于单核执行时间/CPU核心数。

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

    并发编程的目的就是为了提高程序的执行效率,提高程序的运行速度。但是并发编程并不总是能提高运行速度的,而且并发编程可能会遇到很多问题,比如内存泄漏,死锁,线程不安全等。

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

    java线程在运行的生命周期中只会处于下面六种状态中的一个:

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

    线程在生命周期中并不是固定处于某一个状态,而是随着代码的执行在不同状态之间切换、下面是状态变迁图:


    图源于java并发编程艺术

    由上图可以看出,线程创建完成处于new状态,调用start犯法开始运行,处于ready状态。可运行状态的线程获得cpu时间片后处于running运行状态。

    为什么jvm没有区分READY和RUNNING而是统一成RUNNABLE状态呢?

    因为现在的时分多任务操作系统架构通常用所谓的时间分片方式进行抢占式轮转调度。这个时间分片通常很少,可能只有10-20ms,也就是只能执行0.01秒。然后被切换下来放入调度队列的末尾等待再次调用(也就是回到ready状态)。线程的切换如此快,区分这两种状态就没什么意义了。

    什么是上下文切换?

    线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到的程序计数器,栈信息等。当出现如下的情况时,线程会从占用CPU状态中退出:

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

    这其中前三种都会发生线程切换,线程切换意味着要保存当前线程的上下文。留待线程下次占用CPU的时候恢复现场。并加载下一个将要占用CPU的线程上下文。这就是所谓的上下文切换。
    上下文切换是现代操作系统的基本功能。因为每次需要保存信息恢复信息,这样会占用CPU,内存等系统资源进行处理。也意味着效率会有一定的损耗。如果频繁切换就会造成整体效率低下。

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

    认识线程死锁

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

    如下图:线程A持有资源2,线程B持有资源1,他们同时都想要对方的资源,所以这两个线程就会互相等待而进入死锁状态。


    死锁

    其实产生死锁是有四个必要条件的:

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

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

    如何预防死锁?破坏死锁产生的必要条件即可:

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

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

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

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

    这是一个非常经典的java面试题
    new 一个Thread,线程进入了new新建状态,调用start()方法后,会启动一个线程并使线程进入了就绪状态。当分配到时间片后就可以开始运行了。
    start()会执行线程的相应准备工作。然后自动执行run()方法的内容,这是真正的多线程工作。但是,直接执行run()方法,会把run()方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这不是多线程工作。

    总结:调用start()方法方可启动线程并使线程进入就绪状态。直接执行run方法的话不会以多线程的方式执行。

    本篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利,生活健健康康!

    相关文章

      网友评论

        本文标题:JavaGuide知识点整理——并发常见知识点

        本文链接:https://www.haomeiwen.com/subject/icqdirtx.html