java学习之路

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

2022-07-18  本文已影响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并发编程艺术

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

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

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

什么是上下文切换?

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

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

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

认识线程死锁

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

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


死锁

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

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

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

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

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

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

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

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

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

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

上一篇 下一篇

猜你喜欢

热点阅读