面试记录

JVM线程属于用户态还是内核态

当进程运行在ring3级别时为用户态,ring0级别时为内核态

有些操作需要有内核权限才能进行,那么有三种由用户态切换到内核态的情况:

  1. 系统调用:操作系统封装内核指令,统一管理硬件资源,然后向用户程序提供系统服务,用户程序进行系统调用,操作系统进行检查确保安全然后再进行相应的资源访问操作。比如malloc(),print()调用write()系统输出字符串
  2. 异常事件:当cpu正在运行用户态程序,发生不可预知的异常事件,就会转用户态,比如缺页中断。
  3. 外围设备的中断:当外围设备完成请求就会向CPU发出中断信号,此时cpu暂停下一条要执行的指令,去执行中断信号所对应的程序

相同点和不同点:都是中断,但是系统调用是主动,其他都是被动

用户态和内核态线程的映射关系

一对一(内核线程实现)

程序使用轻量级进程和内核线程产生映射

缺点:轻量级进程的数量有限制,执行效率低

多对一(用户线程实现)

优点:用户线程数量几乎无限制,执行效率高

缺点:一个用户线程阻塞,其他线程也会阻塞

多对多

UT 用户态线程 LWP 轻量级线程 KLT 内核态线程

1656470393008

优点:

  1. 一个用户线程的阻塞不会导致所有线程的阻塞,因为此时还有别的内核线程被调度来执行。
  2. 多对多模型对用户线程的数量没有限制。
  3. 在多处理器的操作系统中,多对多模型的线程也能得到一定的性能提升,但提升的幅度不如一对一模型的高。在现在流行的操作系统中,大都采用多对多的模型。

用户态线程:切换代价小,高并发但是容易阻塞

内核态线程:处理能力高,切换代价大

Java线程的实现

虚拟机规范中并没有限定java线程需要使用哪种线程模型,要根据不同的平台来说,但是无论使用哪种线程模型,java程序的编码和运行都是没有差异的

Java线程调度

线程调度有两种:协同调度和抢占调度

协同:自己分配时间,自己切换

抢占:系统分配时间,系统决定线程的切换

java线程采用抢占调度

java线程的6种状态

新建———-运行———无限期等待——–限期等待———阻塞——结束

操作系统线程的几种状态

新建———就绪————等待————运行———–结束

java和操作系统的线程对应关系

1.2之前(绿色线程 1:N),程序员为jvm开发了一个线程调度内核,映射到操作系统层面就是用户态线程;

1.2(1:1)之后,jvmU型安泽了操作系统原生线程模型,映射到操作系统层面就是内核态线程,通过系统调用,将程序的线程交给了操作系统内核进行调度。

通过创建过程来理解

1656474569799

Java的Thread对象:仅仅是一个Java对象

JVM的JavaThread对象:连接着java的Thread对象与OS对象

JVM的OSThread对象:一个工具类,对OS线程API进行了功能性封装

流程图:

1656474707894

JVM_StartThread核心做了两件事情:

1.创建JavaThread对象

​ (1) 设置jvm执行run方法的跳板

​ (2) 调用os::create_thread创建OSThread对象及操作系统线程完成三者的关联

​ os::create_thread做了一下这些:

​ 创建OSThread对象,将JavaThread对象与OSThread对象进行关联

线程库

为开发人员提供创建和管理线程的一套API

三个主要的线程库:

1)POSIX Pthreads:可以作为用户或内核库提供,作为 POSIX 标准的扩展

2)Win32 线程:用于 Window 操作系统的内核级线程库

3)Java 线程:Java 线程 API 通常采用宿主系统的线程库来实现,也就是说在 Win 系统上,Java 线程 API 通常采用 Win API 来实现,在 UNIX 类系统上,采用 Pthread 来实现。

操作系统对于锁的实现

在硬件层面,CPU提供了原子操作、关中断(可解决单核情况下两个线程同时获得锁)、锁内存总线的机制(解决多核情况下两个线程同时获得锁);OS基于这几个CPU硬件机制,就能够实现锁;再基于锁,就能够实现各种各样的同步机制(信号量、消息、Barrier等等等等)

synchronized的底层实现

是通过对象内部的一个监视器锁(monitor)实现的,监视器锁有时通过操作系统的互斥锁来实现的,而且现在主流的java虚拟机实现中,java的线程是映射到操作系统原生的内核线程中的,那么线程的阻塞或唤醒,就涉及到用户态和内核态的转换中,所以是重量级锁。

是一种块结构的同步语法,经过javac反编译之后,会在同步块的前后分别生成monitorenter和monitorexit两个字节码指令,这两个指令都需要一个reference类型的参数来指明加锁解锁的对象,如果synchronized明确了对象参数。就以这个对象的引用作为reference,如果没有指定,那就根据修饰的方法类型,来决定是区代码所在的对象实例还是相应的class对象。

反射的性能优化

两个地方导致性能差:getMethod和invoke,反射是一个解释操作,临时告诉jvm应该做什么

优化思路1:缓存Method,不重复调用getMethod
优化思路2:借助ASM框架,使用reflectAsm,让invoke变成直接调用

借反射的getDeclaredMethods获取目标类的所有方法,然后动态生成一个继承于MethodAccess 的子类SimpleBeanMethodAccess,动态生成一个Class文件并load到JVM中。

SimpleBeanMethodAccess中所有方法名建立index索引,index跟方法名是映射的,根据方法名获得index,SimpleBeanMethodAccess内部建立的switch直接分发执行相应的代码,这样methodAccess.invoke的时候,实际上是直接调用。

hashmap在jdk1.7的死循环

多线程头插法造成的,线程2已经完成扩容散列,链表变成了倒序,线程1再进行扩容,将倒序链表变成了一个正序链表,从而形成环形链表,在使用get方法时造成死循环。

常用集合

ArrayList

首先有三种构造方法(有参,无参,指定集合参数组成的列表)

主要就是三个方法:

1.ensureCapacityInternal得到最小扩容量,并进行扩容

2.ensureExplicitCapacity//判断是否需要扩容,如果最小扩容量大于数组现在的长度就调用grow方法

3.grow 进行位运算,扩容1.5倍,并进行判断是否超出数组的最大容量。

始化数据量为0,add时变为10,当 要 add 进第 1 个元素时,minCapacity 为 1,在 ensureCapacityInternal的Math.max()方法比较后,minCapacity 为 10 ,

最好在 add 大量元素之前用 ensureCapacity 方法(因为是public修饰),以减少增量重新分配的次数

Set Map

posted @ 2022-07-17 18:40  山野村夫01  阅读(79)  评论(0编辑  收藏  举报