大厂面试题

JVM:

1.jre、jdk、jvm的关系:

jdk是最小的开发环境,由jre++java工具组成。

jre是java运行的最小环境,由jvm+核心类库组成。

jvm是虚拟机,是java字节码运行的容器,如果只有jvm是无法运行java的,因为缺少了核心类库。

2.java的运行时数据区有哪些:

java运行时数据区分为5块:

线程共享的模块:方法区和堆空间

非线程共享的模块:程序计数器,虚拟机栈,本地方法栈。

 

方法区:存储已经被虚拟机加载的类信息,静态变量,常量,即时编译器编译后的代码等数据。主要可以分为两类:类信息(class文件信息,class内部的数据结构),运行时常量池(在1.7版本部分被迁移至堆空间了,其中有字符串常量池,静态变量。原因:永久代的空间评估很难预测,太少容易出现OOM,太大浪费空间,所以把占用空间最多的字符串常量池,静态变量迁移到了堆空间,可以有效的避免永久代出现OOM的情况。在1.8的时候方法区就全部迁移到了元数据,也就是no-heap,使用本地内存来存储,这样子就不需要分配空间,而是只会限制于本地内存的大小了,可以设置参数,如果没有设置则是动态的扩容)

 

堆空间:大部分对象存放的区域,包括数组

 

程序计数器:当前现成所执行的字节码的行号指示器:程序计数器中只存储当前线程执行程序的行号,一个类指针的数据结构。字节码解析器就是通过改变这个值,来确定下一个执行的字节码指令

虚拟机栈:java方法执行的内存模型,java执行的方法都是一个栈帧,记录了方法的局部变量表、操作数栈、动态连接、方法出口等,递归调用的存储情况(当一个方法需要调用另外一个方法时,会把当前的方法执行的情况保存起来,压入栈,弹出形参和方法返回地址,并重新创建一个新的方法的栈帧),其中局部变量表、操作数栈都是在编译期就能确定的。

 

本地方法栈:本地方法的调用栈

 

3.什么是运行时常量池:

运行常量池是方法区存放字面量和符合引用的地方,区别于class的信息常量池。一般是编译期就可以产生,但是并没有限制只能编译期产生,也可以在运行时产生,最多使用的就是String的intern方法。

4.jvm是如何给对象分配内存:

java给对象分配内存的方式有两种,一种是指针碰撞,这种方式主要是在于使用的内存空间是连续的,只要偏移指针就可以得到分配空间;另外一种是空间列表:这种适合不连续的内存空间,需要额外维护一张内存空闲列表,来确定那部分内存是空闲的。一般来说带有compact操作的垃圾回收算法会使用方式一,常见的就是serial,带有复制算法。而cms这种基于清除算法的,都是方式2。在分配过程中可能会出现并发问题,一个线程分配了A部分内存,另一个线程也是分配了这部分,那么就会被覆盖。jvm采用两种方式来避免,一个是分配的时候使用CAS的方式来保证操作的原子性;还有一种方式是给每个线程分配一个内存空间(TLAB),这样子就直接在自己的内存空间分配,如果TLAB不够,则在使用同步方式,重新分配

5.对象是如何创建的:

对象创建分为如下几步:

 

检查对象的类信息是否存在,如果不存在,则加载类信息

分配内存

设置对象的基本信息:比如类的元数据指向,哈希码,分代年龄等,这些信息会存放在对象头中

执行构造函数

 

其中a,b,c三步是对于jvm而言的,创建完成后,jvm就认为对象创建完成了,但是对于我们程序而言,才只是开始,d就是执行属于我们程序的逻辑。

6.对象的构成:

一个对象分为3个区域:对象头、实例数据、对齐填充

对象头:主要是包括两部分,1.存储自身的运行时数据比如hash码,分代年龄,锁标记等(但是不是绝对哦,锁状态如果是偏向锁,轻量级锁,是没有hash码的。。。是不固定的)2.指向类的元数据指针。还有可能存在第三部分,那就是数组类型,会多一块记录数组的长度(因为数组的长度是jvm判断不出来的,jvm只有元数据信息)

实例数据:会根据虚拟机分配策略来定,分配策略中,会把相同大小的类型放在一起,并按照定义顺序排列(父类的变量也会在哦)

对齐填充:这个意义不是很大,主要在虚拟机规范中对象必须是8字节的整数,所以当对象不满足这个情况时,就会用占位符填充

7.对象的访问定位:

java的对象访问定位分为两种方式:句柄和直接指针。

句柄方式主要是存储稳定的句柄地址,jvm会创建一个句柄池,一个reference会对应对象的句柄池地址(句柄池分为两个指针:对象实例指针,对象类型指针)

直接指针主要是快速,可以直接指向对象的地址(对象会分为两块,一个对象类型指针,和实例数据)

8.垃圾回收算法:

jvm常用的垃圾回收算法有:复制,标记-清除,标记-整理,分代回收。

复制算法:没有碎片化,但是需要把内存分为2个或以上部分,空余出一个区域,可以进行转移

标记-清除:会先把需要回收的对象进行标记,然后清除。这个算法主要的问题是会产生内存碎片。

标记-整理:先进行标记,再把存活的对象放到一边,清除边界外的数据

9.如果判断一个对象是否存活:

一般判断对象是否存活有两种算法,一种是引用计数,另外一种是可达性分析。在java中主要是第二种

10.java是根据什么来执行可达性分析的:

根据GC ROOTS。GC ROOTS可以的对象有:虚拟机栈中的引用对象,方法区的类变量的引用,方法区中的常量引用,本地方法栈中的对象引用。

11.什么是安全点?

安全点是用于辅助jvm进行GC ROOTS的枚举,以及GC过程中,作为线程停顿的特定位置。正常来说GC会需要STW的时候,都是需要线程进入一个暂停的状态,啥时候可以暂停?就是安全点了。一般来说让线程暂停可以分两种方式:抢占式中断和主动中断,抢占中断是由jvm控制,在一瞬间,暂停全部线程,如果发现线程不是安全点,则恢复线程(这玩意概率有点低啊);主动中断:则是需要线程自己进行,jvm在gc时候,设计一个标记,然后由线程到底安全点的时候去查询是否处于gc过程,如果是,则暂停。

12.那什么是安全区呢:

安全区指的是一段代码中并不会出现引用关系的变化,所以成为安全区,这个时候也是可以执行逻辑的,线程不会中断,当进入安全区的时候,线程会设置标示,说明自己是安全的,当要出临界点的时候,回去查询GC是否完成,如果还没有,则暂停线程。

13.GC ROOTS是怎么进行枚举的:

一般来说,有两种方式可以进行枚举:一种是遍历,另外一种是维护一个映射表。遍历的方式称为保守式,jvm没有记录一个类型是什么,所以它只能遍历需要检查应用的地方,判断这个指针是不是指向GC堆的指针(检查上下午,或者对齐填充),这种方式有个问题:无法判断疑似指针,就会被放过,而且也是因为疑似指针,它就不能被移动(除非使用句柄池的方式)。外部维护一个映射表方式比较简单,因为对象本身就是带有类型信息的(反射),所以只要有足够的信息,GC就能判断出这个对象那部分是引用(这个会在编译的时候确定,类加载的时候),jvm使用了OOPMap的方式来支持这个,只要递归遍历这些oopMap就可以枚举出来。

14.什么是OOPMAP:

OopMap 记录了栈上本地变量到堆上对象的引用关系,在编译后,只有变量在帧上的位置,OOPMap就是映射这个位置和对象的关系。JNI在编译的时候会在特定的位置安置oopMap,这样子线程执行到这个位置的时候,就可以更新这个关系(为什么?因为如果在每个指令后去更新,就太损耗性能了,所以在一些特定的点上做这个操作),这个点就是安全点。

15.安全点一般在什么位置:

一般会出现在循环的末尾,方法返回前,可能出现异常的地方。

16.内存是如何分代的:

java分为新生代和老年代,一般是1:2的存在。当对象新建的时候一般是在新生代中,除非是大对象,会直接达到老年代,老年代一般是一些比较稳定的对象,比如大对象,以及经历了多次gc的对象(默认15次),还有一种情况,当新生代中相同年龄的对象个数大于一半的时候,会把大于等于这个年龄的这部分也升级为老年代

17.什么时候会出现MGC和FGC:

MGC又称为YGC,主要是发生在新生代,当Eden区满的时候会被触发。FGC是全堆范围的GC,当堆可用空间只有20%的时候,则会被触发。

18.YGC只会出现在新生代嘛:

不一定的,因为新生代的对象有可能会持有老年代对象的引用,所以会把这部分一起给算进去,但是不会扫描整个老年代,而是由RememberedSet来记录不同年龄代的对象引用,直接把这部分弄出来就好了。

19.垃圾收集器有哪些:

新生代:serial收集器(单线程),parNew收集器(多线程,唯一可以和CMS连用),Parallel Scavenge收集器(吞吐量为目的);

老年代:serial old收集器,parallel old收集器 CMS收集器

不分区的G1收集器,以及最新的ZGC收集器

20.CMS收集器的工作原理:

CMS收集器可以分为4步或者6步:

 

初次标记(STW)

 

 

并发标记

 

 

重新标记(STW)

 

 

并发清除

 

其中,在b,c直接还会出现预清理和可中断预清理两个步骤,因为对于b步骤而言,已经标记了部分不可达的对象,而对象如果一旦被标记不可达,就再也不会变成可达了,所以这部分如果在内存不足的情况,可以在进行一次清理,而预清理主要是把上一阶段被标记的对象,重新有变更的进行标记。可中断预清理主要是处理 From 和 To 区的对象,标记可达的老年代对象。

CMS收集器清理算法主要是标记-清除,因为是并发清除的。。。所以不能动指针。

21.G1收集器的工作原理:

G1收集器的步骤分为:

 

初次标记(STW)

 

 

并发标记

 

 

最终标记(STW)

 

 

筛选回收(STW)

 

G1收集器和CMS不一样,G1收集器的时候已经不是物理隔离分代了,而是把内存分成一个个块。它能实现预测停顿,会跟踪每一个块里面的垃圾堆积和价值大小(回收空间大小和所需的时间),由于把内存分块了,所以只要回收这个块里面,不会进行全部的堆扫描。如果出现跨块的引用,G1收集器则基于rememberSet来进行管理。在对象进行reference类型进行写操作的时候,就会产生一个write barrier,暂时中断写入,去检查是否有跨块情况,如果有就记录rememberSet中。

22.知道G1里面的STAB嘛:

STAB是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。在并发标记的过程中,如果一个灰对象没有遍历完,删除了一个白对象到灰对象之间的直接引用或者间接引用,就会被漏标。如果是给黑对象一个白对象的引用,则也是会出现的。new 出来的对象是白对象,会通过使用TAMS来避免这个情况,Region中有两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,这是一种隐式的标记。但是第一种情况则是由STAB来破坏的,会由一个write barrier来记录指针的变更。

23.加载类的方式有哪些:

加载类的方式分为两种:隐式加载和显式加载。隐式加载一般是new指令,而显式加载是反射,如Class.forName还有classLoader.load。

24.类加载的过程:

类加载的过程分为加载->链接>初始化。而连接有分3个步骤:验证->准备->解析

加载:由类的全限定名来获取定义类的二进制流,把二进制流代表的静态存储结构转化为方法区的运行时的数据结构,生成一个Class对象,作为方法区的入口

验证:会校验字节流中包含的信息符合当前虚拟机的要求,有文件格式的校验:比如魔术,版本号等,元数据的校验:对数据类型的校验,比如继承呀,抽象等是否符合要求,字节码验证:主要是对类的方法体进行验证,符号引用的验证:类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,比如:符号引用的类全限名是否能找到类的实例等

准备:这阶段主要是把类的静态变量进行初始化,把对象变为默认值,分配空间。当然如果是final修饰的,则会直接赋值。

解析:把符号引用替换为直接引用.

初始化:会执行<clinit>指令,按照静态变量和静态块的顺序进行执行,这个方法是线程安全的,会加锁。这就是为什么静态块只能访问顺序在其前面的变量,后面的变量能赋值,但是不能访问的原因,因为后面的变量只有分配类空间和原始值,访问是错误的。这边还有个很特别的现象,那就是如果在static块中给后面的变量赋值,是会被后面变量覆盖的(除非后面变量没有赋值,只是声明)。

25.什么是双亲委派模型:

在类加载中,分为3大类加载器:bootstrap加载器,extension加载器,Application加载器,还有我们自定义的类加载器, 除了顶层的bootstrap加载器都要有一个父加载器,且每次加载都要优先由父加载器去加载,如果父加载器装载不了,则判断是否自身能装载。

26.双亲委派模型有啥作用:

保证java基础类在不同的环境还是同一个Class对象,避免出现了自定义类覆盖基础类的情况,导致出现安全问题。还可以避免类的重复加载。

27.如何打破双亲委派模型:

可以使用线程上下文,线程上下文默认会继承父类加载器,一般情况下是应用类加载器。

28.tomcat是如何打破双亲委派模型:

tomcat有着特殊性,它需要容纳多个应用,需要做到应用级别的隔离,而且需要减少重复性加载,所以划分为:/common 容器和应用共享的类信息,/server容器本身的类信息,/share应用通用的类信息,/WEB-INF/lib应用级别的类信息。整体可以分为:boostrapClassLoader->ExtensionClassLoader->ApplicationClassLoader->CommonClassLoader->CatalinaClassLoader(容器本身的加载器)/ShareClassLoader(共享的)->WebAppClassLoader。虽然第一眼是满足双亲委派模型的,但是不是的,因为双亲委派模型是要先提交给父类装载,而tomcat是优先判断是否是自己负责的文件位置,进行加载的。

 

 

 

并发编程:

1.什么是线程,进程?

进程是分配资源的最小单位,一个进程相当于是一个应用程序,一个进程至少有一个线程

线程是cpu执行的最小单位。可以提高进程的处理顺序,并发执行

2.线程的实现方式?

线程的实现方式一般有三种:内核线程,用户线程创建以及(用户线程和轻量级进程混合创建)。

内核线程:直接由操作系统内核支持的线程,由内核来完成线程的切换

用户线程:非内核线程,都是用户线程。

3.线程的调度方式有哪些:

线程的调度方式分为两类:协同式和抢占式。

协同式是有线程本身来控制的,等待自己运行完成则切换,

抢占式是由操作系统来进行调度,系统会给每个线程分配时间片,一旦时间片执行完,就需要让位。

4.线程的状态有哪些:

线程的状态有6个:新建,运行,阻塞,等待,超时等待,结束。

新建:线程刚刚创建的时候,但是并没有准备执行

运行:就绪和运行中两个状态

阻塞:等待锁的释放

等待:等待其他线程的通知,不会被分配cpu空间,可能无期限的等待下去 wait(),join()方法

超时等待:不会被分配cpu空间,但是不会无期限等待下去,等待一定时间,就会被重新唤醒。sleep(),有时间限制的wait或者join方法

结束:运行结束

5.JMM(java内存模型):

在java中线程->工作内存->save/load等同步指令->主内存。相对于操作系统的内存模型很相似,cpu->缓存1,缓存2->内存一致性协议->主内存。

6.为什么线程状态中没有中断状态:

线程中断是线程间的一个协程方式,并不是线程的一种状态。并不是真的中断了线程,而是设置了一个中断标示。有线程自己去处理,一般会有等待,或者异常的方式(那线程就真的结束了。。。)

 

可以用线程的interrupte()设置中断标示,isInterrrupted()判断是否中断(不会重制),interrupted()方法会检测是否中断状态,但是如果是中断,则会清除这个标记。

7.什么是死锁,活锁:

死锁是两个或两个及以上线程因为资源抢占而出现的永久性等待的情况。

活锁是线程因为某些条件没有满足,而一直重试,一直失败的情况。

8.产生死锁的原因有哪些,怎么避免死锁:

死锁产生的条件:

 

互斥

 

 

资源占用不释放

 

 

需要请求其他资源

 

 

循环等待

 

如果要避免死锁,则需要打破上面条件中的一个,一般来说比较好解决的是b和d,可以在请求资源的时候设置一个超时时间,如果一直获取不到,则释放自己占用的资源,重新尝试。或者就是在一开始就获取全部资源,比如reentrantlock就可以用tryLock的方式来获取资源,只要有一个失败,则释放全部资源。

9.什么是守护线程:

守护线程是一种服务线程,jvm在关闭的时候不会等待守护线程的关闭,只要其他的用户线程关闭后,就会关闭,而守护线程也会被失效(因为没有可以服务的线程了),最常见的就是垃圾回收线程。守护线程的设置需要在调用start方法前设置,否则无效

10.volatile关键词的作用是什么:

volatile关键词主要作用是保证变量线程间可见,以及避免指令重排序的情况。

11.volatile如何保证线程间可见和避免指令重排:

volatile可见性是有指令原子性保证的,在jmm中定义了8类原子性指令,比如write,store,read,load。而volatile就要求write-store,load-read成为一个原子性操作,这样子可以确保在读取的时候都是从主内存读入,写入的时候会同步到主内存中(准确来说也是内存屏障),

指令重排则是由内存屏障来保证的,由两个内存屏障,一个是编译器屏障:阻止编译器重排,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行。第二个是cpu屏障:sfence保证写入,lfence保证读取,lock类似于锁的方式。java多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个lock指令,就是增加一个完全的内存屏障指令。

12.锁的种类有哪些:

常用的锁有:悲观锁/乐观锁,自旋锁,重入锁/不可重入锁,公平锁/非公平锁等

13.你使用过哪些锁:

最多使用的是悲观锁,synchronized和reentrantlock,乐观锁,自旋锁使用的原子类工具。以及ReentrantReanWritelock。

14.说一下synchronized和reentrantlock的区别:

两种都是互斥锁,一般来说synchronized能使用的地方reentrantlock都能使用,synchronized是关键词,是有jvm来实现功能,reentrantlock是api包,是有开发人员自己的逻辑,故此可以实现公平锁和非公平锁等其他额外的功能,synchronized加锁,解锁是隐式的,添加关键词就好来,reentrantlock是显式的,需要手动的调用lock方法或者unlock方法来加锁/释放锁。

15.synchronized的实现原理:

synchronized有三种使用用法,实现方式都不相同:修饰代码块,会在指令上面增加monitorEnter和monitorExit来限制锁。如果修饰的是方法,则是加锁在当前实例上。如果修饰的是静态方法,获取的是对应的类实例。但是三种方式主要是基于minitor对象(基于操作系统的muntexLock来实现的)来实现的,而java对象天生是minitor(这个也就是为什么加锁是加在对象上面的了。。。),我们知道对象实例分为3个部分:对象头,实例数据,填充数据,在对象头中会存放一个指向ObjectMinitor的指针(偏向锁是没有的,轻量级锁也是没有的。。。),ObjectMinitor是有一个owner和一个计数器,以及其他的队列的,owner表示是否占用线程,指向了获取锁的线程,计数器是代表了重入的次数,队列中有个队列是用来存储等待锁的线程队列(特么和AQS差不多啊)

16.reentrantlock的实现原理:

reentrantlock的核心实现方式就是AQS,在AQS中会有一个state的值,对于reentrantlock来说就是可以重入的次数,利用cas操作来改变state的值,如果state不为0的时候,cas操作其实是失败的,那就是获取锁失败了,则会进去queue队列里面去等待锁的释放,如果获取锁成功,则会记录当前的线程。而锁对应的实现其实是Sync类,分为NofairSync和FairSync,公平锁和非公平锁的区别,就在于在cas前会判断队列是否是空的,当前线程是否是头节点的线程。

17.什么是公平锁和非公平锁:

公平锁意味着公平,所以线程获取锁的顺序是按照进队列的顺序的。而非公平锁不是,在唤醒头节点的时候,如果新来了一个线程,则会给新的线程,这样子可以减少一定的上下文切换。实现是cas前会判断队列是否是空的,当前线程是否是头节点的线程。

18.什么是悲观锁和乐观锁:

悲观锁认为,在线程执行中,其他的线程都会改变它的执行结果,所以需要把其他线程屏蔽(也就是互斥),不给他们执行。

乐观锁则是在并发量小的情况下,其他线程不会改变自己的结果,只要在执行完成后,去验证一下值是否被改变了,如果被改变了则失败。

19.cas是原子性操作嘛:

是的,cas的原子性是有操作系统保证的。

20.cas有什么问题:

cas会出现一个ABA的问题,如果一个线程改变了值,变成了B,而另外一个线程改回了A,则初始线程去判断值是否被改变的时候,会发现值是没有变的,则会认为没有修改值,但是准确来说运行结果已经被变更了。那么如何避免ABA问题,那就是需要增加一个版本号了,不光判断值,也要判断版本号是否一致, 常用的比如AtomicStampedReference。

21.读写锁了解嘛,知道读写锁的实现方式嘛:

常用的读写锁ReentrantReanWritelock,这个其实和reentrantLock相似,也是基于AQS的,但是这个是基于共享资源的,不是互斥,关键在于state的处理,读写锁把高16为记为读状态,低16位记为写状态,就分开了,读读情况其实就是读锁重入,读写/写读/写写都是互斥的,只要判断低16位就好了。

22.乐观锁的应用场景有哪些:

常用的乐观锁使用是在原子类中,原子类使用自旋锁的方式,循环调用cas,直到cas成功,所以也就是为什么是原子类的原因了。自旋锁的会有一个问题,也就是当并发量非常大的时候,cas只能允许一个线程执行成功,就会造成很多线程循环等待,循环执行失败,可能会导致活锁的情况。为了解决这种方式,我们可以采用分段的思想,比如LongAddr和ConcurrentHashMap

23.LongAddr是如果解决高并发的问题:

LongAddr对于值是由两部分组成,baseValue和Cell数组,它把一个值分成多个部分,每次线程竞争的时候,就可以竞争多个对象(也就是Cell),并把操作修改在上面,这样子把原本的单一线程执行成功,变成了可以多个线程执行成功。而且当线程获取cell失败,不是阻塞,而是尝试下一个。最终的vaue就是baseValue+Cell数组的和。

24.LongAddr的执行逻辑是什么样子:

在判断Cell数组是否为null,如果不为null,则选择一个Cell进行cas操作,如果为null,则对base进行cas操作,如果cas失败,说明并发量高,则需要扩容cell。扩容cell的时候会设置busy字段,也是用cas操作的。

25.LongAddr是如何选择对应的Cell的:

会调用getProbe方法来获取当前线程的一个hash值,并且和cell的长度进行&操作,计算出对应的下标。

26.锁膨胀的过程:

在java1.6前锁的性能是很差的,对锁进行了优化,引进了偏向锁,轻量级锁,自旋锁,自适应自旋锁等。其中对于一个线程的锁是有偏向锁-》轻量级锁-》重量级锁,进行膨胀的,而且不可回退。

线程加锁默认会是偏向锁,偏向锁会存储锁状态,和线程id,当开始的时候会先判断锁的状态,是否为可偏向锁,如果是,则判断当前线程id和偏向锁对应的id是否一致,如果一致,那就获取锁成功,如果不一致则尝试使用cas替换线程id。那么什么时候会被膨胀呢,当cas操作失败的时候,说明竞争激烈,则需要升级锁,还有一种情况,因为对象头里面存储的是锁状态,线程id,没有了对象的hash值,那如果要用这个值的时候,也会膨胀到轻量级锁。那么是如何膨胀到轻量级锁的呢:偏向锁是需要通过线程竞争来释放的(hash那个也是触发条件),当cas失败当时候,会等待,直到线程进入安全点,然后会暂停线程,校验占有了偏向锁的线程是否还活着,如果不活动了,则变为无锁状态,然后重新cas,如果线程是活跃的,那么就把对象头的信息指向栈的一个引用(不是monitor,是lock reocd,是对象的markword的一个引用(无锁状态的)),升级为轻量级锁,重新进行cas(这次是自旋锁了)

轻量级锁适用于一些竞争少的情况。所以当有锁竞争的时候,jvm会先生成栈上面的一个lock reocd,然后把对象头信息存放进去,用cas去替换,如果替换成功,则获取锁成功,如果替换失败,则自旋,如果自旋失败,则膨胀。

27.什么时候适合线程池:

1.任务的执行时间短

2.任务的数量比较多

28.Executors有哪些线程池,分别的应用场景有哪些:

固定线程数量的线程池fixed线程池,单例线程池,可缓存线程池,定时线程池。

fixed线程池比较常用,是固定线程数量的线程池,

单例线程池是只有一个线程的线程池。

可缓存线程池是如果有空余的线程,则会提交给空余的否则就是新建线程

定时线程池,一般是用于延时任务的。

29.ThreadPoolExecutor的参数有哪些:

核心线程数,最大线程数,空闲时间,空闲时间类型,线程工厂,队列,拒绝策略。

30.给线程池提交一个任务的流程是什么样子的:

先会判断核心线程数是否满了,如果没有满则新建一个线程,如果满了则丢给任务队列,如果任务队列也满了,则判断线程数是否超出了最大线程数,如果没有则新建线程,如果也满了,则执行拒绝策略。

31.线程池的拒绝策略有哪些:

直接拒绝,抛出异常,移除最先进入队列的任务,在 execute 方法的调用线程中运行被拒绝的任

32.如果线程池中的一个线程运行时出现了异常,会发生什么:

如果提交任务的时候使用了submit,则返回的feature里会存有异常信息,但是如果数execute则会打印出异常栈。但是不会给其他线程造成影响。之后线程池会删除该线程,会新增加一个worker。

33.线程池的状态有哪些:

线程池有5种状态:running,showdown,stop,Tidying,TERMINATED。

running:线程池处于运行状态,可以接受任务,执行任务,创建线程默认就是这个状态了

showdown:调用showdown()函数,不会接受新任务,但是会慢慢处理完堆积的任务。

stop:调用showdownnow()函数,不会接受新任务,不处理已有的任务,会中断现有的任务。

Tidying:当线程池状态为showdown或者stop,任务数量为0,就会变为tidying。这个时候会调用钩子函数terminated()。

TERMINATED:terminated()执行完成。

在线程池中,用了一个原子类来记录线程池的信息,用了int的高3位表示状态,后面的29位表示线程池中线程的个数。

 

34.我们如果在并发量很高的情况下使用随机数生成,会有什么问题:

随机数一般是一个伪随机数,它生产新的随机数是需要一个种子的,比如Random,一般我们会根据老的种子,重新生成一个新的种子,在根据种子生成一个新的随机数,但是呢,在高并发的情况下,随机数是有坑的,因为它没有加锁,所以当并发执行到生成新的种子的时候,有可能两个线程都生成了同一个新的种子(先读取了老种子,然后被切换了),为了避免这个问题,Random使用了自旋锁,那么就是自旋锁有的性能问题,如果在高并发的情况下,只能一个执行成功,容易出现活锁的情况。我们可以使用ThreadLocalRandom,看名字就知道和ThreadLocal有很大的关系了,ThreadLocalRandom会在线程内部保存了一个ThreadLocalRandwomSeed,相当于每一个线程都有一个种子的副本,每个线程都根据自己的副本生成随机数,那就没有并发的问题啦。

35.CountDownLatch和CyclicBarrier的区别是什么:

CountDownLatch是等待其他线程执行到某一个点的时候,在继续执行逻辑(子线程不会被阻塞,会继续执行),只能被使用一次。最常见的就是join形式,主线程等待子线程执行完任务,在用主线程去获取结果的方式(当然不一定),内部是用计数器相减实现的(没错,又特么是AQS),AQS的state承担了计数器的作用,初始化的时候,使用CAS赋值,主线程调用await()则被加入共享线程等待队列里面,子线程调用countDown的时候,使用自旋的方式,减1,知道为0,就触发唤醒。

CyclicBarrier回环屏障,主要是等待一组线程到底同一个状态的时候,放闸。CyclicBarrier还可以传递一个Runnable对象,可以到放闸的时候,执行这个任务。CyclicBarrier是可循环的,当调用await的时候如果count变成0了则会重置状态,如何重置呢,CyclicBarrier新增了一个字段parties,用来保存初始值,当count变为0的时候,就重新赋值。还有一个不同点,CyclicBarrier不是基于AQS的,而是基于RentrantLock实现的。存放的等待队列是用了条件变量的方式。

36.什么是信号量Semaphore:

信号量是一种固定资源的限制的一种并发工具包,基于AQS实现的,在构造的时候会设置一个值,代表着资源数量。信号量主要是应用于是用于多个共享资源的互斥使用,和用于并发线程数的控制(druid的数据库连接数,就是用这个实现的),信号量也分公平和非公平的情况,基本方式和reentrantLock差不多,在请求资源调用task时,会用自旋的方式减1,如果成功,则获取成功了,如果失败,导致资源数变为了0,就会加入队列里面去等待。调用release的时候会加一,补充资源,并唤醒等待队列。

37.并发包中的CopyOnWrite'ArrayList有用过嘛:

简单用过,这个适合于读多写少的情况,是读写分离的一种概念,当有写的需求时,会基于reentrantLock,锁主对象,然后copy一个数组,把原本的数组的值复制进去,并修改,然后替换原本的数组,在被修改的时候是不会影响读的,也就是并发修改的时候,读取的还是原来的快照。因为指针并没有切换。

38.ThreadLocal有使用过嘛,解释一下它的原理:

ThreadLocal是保证变量线程内可见。它其实存储的不是在ThreadLocal里面,而是存储在了线程的threadLocals里,是线程专属的。线程内部的threadLocals是一个ThreadLocalMap格式,可以理解为是一个简单的map,它把thread当成key来存放value。

39.如果我们要获取父线程的ThreadLocal值呢:

ThreadLocal是不具备继承性的,所以是无法获取到的,但是我们可以用InteritableThreadLocal来实现这个功能。InteritableThreadLocal继承来ThreadLocal,重写了createdMap方法,已经对应的get和set方法,不是在利用了threadLocals,而是interitableThreadLocals变量。这个变量会在线程初始化的时候(调用init方法),会判断父线程的interitableThreadLocals变量是否为空,如果不为空,则把放入子线程中,但是其实这玩意没啥鸟用,当父线程创建完子线程后,如果改变父线程内容是同步不到子线程的。。。同样,如果在子线程创建完后,再去赋值,也是没啥鸟用的

 

 

集合类:

1.java的集合框架有哪几种:

两种:collection和map,其中collection分为set和List。

2.List你使用过哪些:

ArrayList和linkedList使用的最多,也最具代表性。

3.你知道vector和ArrayList和linkedList的区别嘛:

ArrayList实现是一个数组,可变数组,默认初始化长度为10,也可以我们设置容量,但是没有设置的时候是默认的空数组,只有在第一步add的时候会进行扩容至10(重新创建了数组),后续扩容按照3/2的大小进行扩容,是线程不安全的,适用多读取,少插入的情况

linkedList是基于双向链表的实现,使用了尾插法的方式,内部维护了链表的长度,以及头节点和尾节点,所以获取长度不需要遍历。适合一些插入/删除频繁的情况。

Vector是线程安全的,实现方式和ArrayList相似,也是基于数组,但是方法上面都有synchronized关键词修饰。其扩容方式是原来的两倍。

4.hashMap和hashTable和ConcurrentHashMap的区别:

hashMap是map类型的一种最常用的数据结构,其底部实现是数组+链表(在1.8版本后变为了数组+链表/红黑树的方式),其key是可以为null的,默认hash值为0。扩容以2的幂等次(为什么。。。因为只有是2的幂等次的时候(n-1)&x==x%n,当然不一定只有一个原因)。是线程不安全的

hashTable的实现形式和hashMap差不多,它是线程安全的,是继承了Dictionary,也是key-value的模式,但是其key不能为null。

ConcurrentHashMap是JUC并发包的一种,在hashMap的基础上做了修改,因为hashmap其实是线程不安全的,那在并发情况下使用hashTable嘛,但是hashTable是全程加锁的,性能不好,所以采用分段的思想,把原本的一个数组分成默认16段,就可以最多容纳16个线程并发操作,16个段叫做Segment,是基于ReetrantLock来实现的

5.说说你了解的hashmap吧:

hashMap是Map的结构,内部用了数组+链表的方式,在1.8后,当链表长度达到8的时候,会变成红黑树,这样子就可以把查询的复杂度变成O(nlogn)了,默认负载因子是0.75,为什么是0.75呢?我们知道当负载因子太小,就很容易触发扩容,如果负载因子太大就容易出现碰撞。所以这个是空间和时间的一个均衡点,在1.8的hashmap介绍中,就有描述了,貌似是0.75的负载因子中,能让随机hash更加满足0.5的泊松分布。除此之外,1.7的时候是头插法,1.8后就变成了尾插法,主要是为了解决rehash出现的死循环问题,而且1.7的时候是先扩容后插入,1.8则是先插入后扩容(为什么?正常来说,如果先插入,就有可能节点变为树化,那么是不是多做一次树转化,比1.7要多损耗,个人猜测,因为读写问题,因为hashmap并不是线程安全的,如果说是先扩容,后写入,那么在扩容期间,是访问不到新放入的值的,是不是不太合适,所以会先放入值,这样子在扩容期间,那个值是在的)。1.7版本的时候用了9次扰动,5次异或,4次位移,减少hash冲突,但是1.8就只用了两次,觉得就足够了一次异或,一次位移。

6.concurrentHashMap呢:

concurrentHashMap是线程安全的map结构,它的核心思想是分段锁。在1.7版本的时候,内部维护了segment数组,默认是16个,segment中有一个table数组(相当于一个segmeng存放着一个hashmap。。。),segment继承了reentrantlock,使用了互斥锁,map的size其实就是segment数组的count和。而在1.8的时候做了一个大改版,废除了segment,采用了cas加synchronize方式来进行分段锁(还有自旋锁的保证),而且节点对象改用了Node不是之前的HashEntity。Node可以支持链表和红黑树的转化,比如TreeBin就是继承了Node,这样子可以直接用instanceof来区分。1.8的put就很复杂来,会先计算出hash值,然后根据hash值选出Node数组的下标(默认数组是空的,所以一开始put的时候会初始化,指定负载因子是0.75,不可变),判断是否为空,如果为空,则用cas的操作来赋值首节点,如果失败,则因为自旋,会进入非空节点的逻辑,这个时候会用synchronize加锁头节点(保证整条链路锁定)这个时候还会进行二次判断,是否是同一个首节点,在分首节点到底是链表还是树结构,进行遍历判断。

7.concurrentHashMap的扩容方式:

1.7版本的concurrentHashMap是基于了segment的,segment内部维护了HashEntity数组,所以扩容是在这个基础上的,类比hashmap的扩容,

1.8版本的concurrentHashMap扩容方式比较复杂,利用了ForwardingNode,先会根据机器内核数来分配每个线程能分到的busket数,(最小是16),这样子可以做到多线程协助迁移,提升速度。然后根据自己分配的busket数来进行节点转移,如果为空,就放置ForwardingNode,代表已经迁移完成,如果是非空节点(判断是不是ForwardingNode,是就结束了),加锁,链路循环,进行迁移。

8.hashMap的put方法的过程:

判断key是否是null,如果是null对应的hash值就是0,获得hash值过后则进行扰动,(1.7是9次,5次异或,4次位移,1.8是2次),获取到的新hash值找出所在的index,(n-1)&hash,根据下标找到对应的Node/entity,然后遍历链表/红黑树,如果遇到hash值相同且equals相同,则覆盖值,如果不是则新增。如果节点数大于8了,则进行树化(1.8)。完成后,判断当前的长度是否大于阀值,是就扩容(1.7是先扩容在put)。

9.为什么修改hashcode方法要修改equals:

都是map惹的祸,我们知道在map中判断是否是同一个对象的时候,会先判断hash值,在判断equals的,如果我们只是重写了hashcode,没有顺便修改equals,比如Intger,hashcode就是value值,如果我们不改写equals,而是用了Object的equals,那么就是判断两者指针是否一致了,那就会出现valueOf和new出来的对象会对于map而言是两个对象,那就是个问题了

10.TreeMap了解嘛:

TreeMap是Map中的一种很特殊的map,我们知道Map基本是无序的,但是TreeMap是会自动进行排序的,也就是一个有序Map(使用了红黑树来实现),如果设置了Comparator比较器,则会根据比较器来对比两者的大小,如果没有则key需要是Comparable的子类(代码中没有事先check,会直接抛出转化异常,有点坑啊)。

11.LinkedHashMap了解嘛:

LinkedHashMap是HashMap的一种特殊分支,是某种有序的hashMap,和TreeMap是不一样的概念,是用了HashMap+链表的方式来构造的,有两者有序模式:访问有序,插入顺序,插入顺序是一直存在的,因为是调用了hashMap的put方法,并没有重载,但是重载了newNode方法,在这个方法中,会把节点插入链表中,访问有序默认是关闭的,如果打开,则在每次get的时候都会把链表的节点移除掉,放到链表的最后面。这样子就是一个LRU的一种实现方式。

网络:

1.5层网络模型和7层模型:

7层网络模型:物理层-》数据链路层-》网络层-》传输层-》会化层-》表示层-》应用层:

物理层:比特流的转化,电信号转化为数字信号

数据链路层:负责物理层上面的互连,节点间的传输

网络层:将数据传输到目的地,根据链路寻找目标

传输层:可靠的传输作用

会话层:负责链接和断开通信链接

表示层:把应用层的信息转化为网络传输用的格式(也用于转化)

应用层:为应用程序提供服务并规定应用程序中通信的相关细节

5层网络模型:整合了传输层-》会化层-》表示层为传输层。

2.TCP/IP协议:

     TCP/IP其实是两个协议的结合体,是我们最常用的协议,其网络模型分为4层应用层-》传输层-》网络层-》网络传输层。其中的ip协议是网络层协议。

3.一次http请求是怎么样的:

http请求发生时,浏览器会先判断本地host,如果有本地host则直接定位到ip,如果没有,则查询路由表,如果路由表都没有,则请求dns服务器,获取ip。获取到ip后则可以进行tcp链接,连接成功后就可以发送http请求了,请求到服务器,等待服务器返回数据,浏览器根据返回结果渲染页面

4.dns解析到过程(补充上一题):

会先查询本地的host信息(我们可以定点打到某台机器就可以用这种方式),如果本地没有,则访问路由表(本地根服务器),本地根服务器会根据自己的缓存查询,如果有则返回,如果没有则请求发到根域名解析,根域名解析会返回一级域名解析服务器地址,请求一级域名服务器比如.com/.cn,一级域名会返回对应的ip/二级域名服务器,进行二级域名服务器请求,二级域名一般就会返回真正的ip信息了。一般解析地址的顺序:.->.com->taobao.com->www.taobao.com的顺序

5.TCP和UDP的区别是啥:

UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式,比如: QQ

TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的运输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接),这种造成了很多资源的消耗

6.TCP是怎么保证可靠性的:

应用数据被分割成 TCP 认为最适合发送的数据块(会出现沾包半包的现象)。

TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。

校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。

TCP 的接收端会丢弃重复的数据。

流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。

拥塞控制: 当网络拥塞时,减少数据的发送。

超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到 一个确认,将重发这个报文段

7.TCP的三次握手:

TCP的三次握手主要是为了保证接收方和发送方的收发功能正常,因为tcp是双向通道。在发送方准备发起tcp连接时,会发送一个SYN包(SYN=1,加一个随机序列号seq),接收方收到后,会回一个ACK包+SYN包(ack=1,ack是发送seq+1,SYN=1,同时生成一个随机序列号seq。发送方接受后,知道对方已经具备了发送和接受能力,但是对方还不知道我有这个能力,所以需要发送一个ACK包(ACK=1,ack=seq+1)。

8.TCP的四次挥手:

相对于连接,结束连接的时候需要的次数更加多一次,客户端请求断开会发送FIN包(FIN=1,随机的序列数seq),服务端接受到了FIN包,返回一个ACK包(ACK=1,ack=seq+1),这个时候服务端就会把自己没有完成的任务完成,任务完成后发送一个FIN+ACK(FIN=1,ACK=1,ack=之前的seq+1,seq=随机序列数),告诉客户端我准备好了断开,客户端接受到了后会返回一个ACK包(ACK=1,ack=seq+1)。为什么要这样呢?一个确保另一方准备好了断开,为嘛服务端在发送FIN的时候还要在发送一个ACK包,这个是因为第一次发送ACK包的时候并没有收到回复,可能会丢失了,所以要在发送一次确认,等到接收到对方的ack包后,就知道都准备好了,可以主动断开了。

 

Netty:

1.BIO、NIO、AIO的区别:

BIO:阻塞同步IO,本身是同步阻塞模式。 线程发起IO请求后,一直阻塞IO,直到缓冲区数据就绪后,再进入下一步操作。

NIO:同步非阻塞IO。线程发起io请求后,立即返回(非阻塞io)。同步指的是必须等待IO缓冲区内的数据就绪,而非阻塞指的是,用户线程不原地等待IO缓冲区,可以先做一些其他操作,但是要定时轮询检查IO缓冲区数据是否就绪,常见的就是Reactor模型

AIO:异步非阻塞IO,线程发起IO请求后,立即返回,也不需要等待IO缓冲区的数据,而是会有内核通知,可以进行io操作。是真正的非阻塞io,对应了uninx网络模型中的最后一个异步模型.

2.5类uninx网络模型:

1.同步阻塞io:用户线程发起io请求,会阻塞用户线程,内核会先查缓存区是否存在数据,如果没有则读取数据到缓存区。然后把内核数据复制到用户线程空间。用户线程开始处理数据

2.半非阻塞同步IO:用户线程发起io请求,内核检测缓存区数据后,如果没有会返回EWOULDBLOCK错误,由用户线程自己去轮询检查是否有数据了,等到内核缓冲区有数据的时候,同步等待内核数据复制到用户区。再由用户线程处理

3.IO复用模型:使用linux的select/poll模型,这样子可以阻塞在select操作上面,等待内核通知数据完成,内核数据准备完成后,阻塞等待内核数据复制到用户线程区域。因为select/poll模型有很严重的性能问题(高连接的情况)

4.信号驱动的IO模型:用户线程会先建立SIGIO信号处理程序,线程继续执行,等待信号的产生,内核复制数据到缓存区后,生成SIGIO信号,用户线程监听后,触发调用,阻塞用户线程从内核区复制到用户空间。

5.异步IO:用户线程通知内核处理数据,内核把数据复制到缓存区,且放入用户空间后,通知线程处理数据,线程接受到信号后,直接处理数据

4/5其实看似很像,都是基于信号到,但是5比4更加优秀,不会阻塞等待用户线程复制数据。而且内核通知的时间点也是不一样的,4是通知是数据准备好了,5是通知可以进行io操作了。

3.IO多路复用技术:

IO多路复用技术基于了uninx的第三种模型,用于服务器需要同时处理多个处于监听状态或者多个连接状态的嵌套词,还有多种网络协议的套接字。可以降低系统的开销,比原本的多线程多进程比。线程可以阻塞在select上,可以让系统在单线程的情况同时处理多个客户端请求

4.为什么不用select/poll,而是要用select/epoll:

poll有很多的限制,比如对一个进程打开对句柄数是有上限对,默认是1024个,这个数量不够的,但是epoll就没有上限的,是操作系统可以处理的最大文件句柄数。而且随着句柄数过多,select的遍历会成为瓶颈。而且使用了mmap来加速内核和用户空间传递。

5.NIO的核心组件:

Buffer:缓冲区,我们知道在javaIo是,字节流是一般不用缓存区的,只有字符流是使用缓冲区的,而字节流是直接放入stream里面的,这次使用流缓冲区,读取是从缓冲区读取,写入也是缓冲区。

Channel:双向管道,Stream是流,是单向的而channel是双工的,既可以读,也可以写,更加符合unix的底层操作系统。

Selector:多路复用器selector,会筛选出就绪状态的channel,可以做到单线程处理多个客户端的情况

6.NIO的通信过程:

服务端:

创建NIOServer,打开ServerSocketChannel,绑定监听的端口

创建Selector,启动线程

注册ServerSocketChannel到Selector

selector轮询就绪Key

ioHandler处理accept消息,设置新客户端的连接

注册Selector新建立的channel为OP_READ

channel读取请求信息到缓冲区

ioHandler处理信息,并写入SocketChannel

 

客户端:

创建NioClient,打开SockerChannel,设置参数,

异步连接服务端,

创建Selector,注册读事件

 

7.Netty的好处:

api简单,

功能强大,支持多种解码功能

定制能力强,可以用pipe进行扩展

稳定,成熟

8.netty是怎么解决拆包和沾包的:

netty处理沾包/拆包的方式有4种:

 

消息定长,固定报文大小,不够的利用填充

 

在报文结束的尾部添加换行符,用换行符进行分割

 

在报文结束的尾部添加特殊符号,用特殊符号来解析

 

将消息分为两部分,消息体和消息头,消息头记录长度

 

9.TCP沾包拆包出现的原因:

a. 应用程序write写入的字节大小大于套接口的发送缓冲区大小

b. 进行MSS大小的TCP分段

c. 以太网帧的payload大于MTU进行IP分片

 

10.Netty服务端流程:

a. 创建ServerBootstrap

b. 设置绑定Reactor线程池(服务端可以有两个EventLoopGroup组)

c. 设置channel类型,服务端一般是NioServerSocketChannel

d. 创建channelPipline

e. 绑定监听端口并启动

f. Selector轮询

 

11.EventLoopGroup作用:

EventLoopGroup是一个线程组,EventLoop是工作线程的包装,比如selector的轮询就是由其中的一个线程负担,还会处理网络io事件。除此之外,用户自定义的task和定时任务也是有EventLoop负责.

12.为什么NioServerSocketChannel创建的时候设置为0,后续才设置为OP_ACCEPT:

设置为0表示只注册,不监听任何网络操作,因为注册方法是多态的,既可以是读写,也可以是注册,所以初始化的时候不能写死。第二,可以根据执行链中预处理完后,在进行监听事件,可以用SelectorKey来修改

13.Netty客户端流程:

 

a. 创建BootStrap

b. 选择io线程组

c. 指定NIOSocketChannel

d. 创建Pipeline

e. 连接指定端口和地址

f. 注册到多路复用器

g. 处理连接结果

h. 发送连接成功到事件

14.netty的byteBuf:

byteBuf和jdk不一样,jdk是用position和limit来表示读取区域,而netty是用了readerIndex和writeIndex,写入使用writeIndex,读取使用readerIndex。0~readerIndex表示已读区域,是可以回收的。netty的buf可以分为两类,一部分是堆内存,一部分是直接内存,我们一般在处理io的时候使用直接内存。而后端业务消息处理使用堆内存。

15.byteBuf的扩容:

默认扩容的方式是倍数扩容,但是如果大于一定大小后变为步进(4MB)。

16.netty的内存池:

netty在开始的时候会申请一大块内存,这个内存由chunk组成。而每个chunk由page组成,使用二叉树的方式管理内存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值