2020-07-24—面试经历

2020-07-24 面试经历(全程1个多小时)

​ 这次面试,让我意识到了,关于各种并发上,自己的实战经验有很大的不足之处,自己以后,在学习的同时,也要多多注意代码的编写。以及各种问题的实践。下面把面试过程回忆一下,也是不错的巩固过程。

一、自我介绍

二、项目介绍,项目细节(业务,activiti工作流)

​ activiti主要问,act表的细节,中间问了是否改动过act的原表,由于自己没有改动的经验,所以当时说的没有。

三、从项目里延伸

docker 基本命令 和 dockerfile,在这复习一下吧:


FROM {base 镜像}
必须放在 DOckerfile 的第一行,表示从哪个 baseimage 开始构建

MAINTAINER
可选的,用来标识 image 作者的地方
RUN
RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。
第一层 RUN command1 的执行仅仅是当前进程,一个内存上的变化而已,其结果不会造成任何文件。
而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。 而如果需要将两条命令或者多条命令联合起来执行需要加上&&。
如:cd /usr/local/src && wget xxxxxxx

CMD
的作用是作为执行 container 时候的默认行为(容器默认的启动命令) 当运行 container 的时候声明了 command,则不再用 image 中的 CMD 默认所定义的命令一个 Dockerfile 中只能有一个有效的 CMD,当定义多个 CMD 的时候,只有最后一个才会起作用.

EXPOSE
EXPOSE:指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个
声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助
镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机
端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

entrypoint
entrypoint:的作用是,把整个 container 变成可执行的文件,且不能够通过替换 CMD 的方
法来改变创建 container 的方式。但是可以通过参数传递的方法影响到 container 内部每个Dockerfile 只能够包含一个 entrypoint,多个 entrypoint 只有最后一个有效当定义了 entrypoint 以后,CMD 只能够作为参数进行传递 .

ADD & COPY
把 host 上的文件或者目录复制到 image 中(能够进行自动解压压缩包)
ENV
用来设置环境变量,后续的 RUN 可以使用它所创建的环境变量
WORKDIR
用来指定当前工作目录(或者称为当前目录)
USER
运行 RUN 指令的用户
VOLUME
用来创建一个在 image 之外的 mount point

四、Java基础

1、集合

list的实现类 —- ArrayList、LinkedList 区别?ArrayList 添加数据的原理(说逻辑)?

Array(数组)是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的。
Array获取数据的时间复杂度是O(1),但是要删除数据却是开销很大,因为这需要重排数组中的所有数据, (因为删除数据以后, 需要把后面所有的数据前移)
缺点: 数组初始化必须指定初始化的长度, 否则报错
例如:
int[] a = new int[4];//推荐使用int[] 这种方式初始化
int c[] = {23,43,56,78};//长度:4,索引范围:[0,3]

List—是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式,它继承Collection。
List有两个重要的实现类:ArrayList和LinkedList
ArrayList: 可以看作是能够自动增长容量的数组
ArrayList的toArray方法返回一个数组
ArrayList的asList方法返回一个列表
ArrayList底层的实现是Array, 数组扩容实现
LinkList是一个双链表,在添加和删除元素时具有比ArrayList更好的性能.但在get与set方面弱于
ArrayList.当然,这些对比都是指数据量很大或者操作很频繁。

ArrayList 添加数据的原理(说逻辑)?

​ 这个问题我有点懵逼, 我以为要问的就是add()方法,可是不是,其实是add的逻辑。

实现逻辑就是:

调用add(int index, E element),然后先调用rangeCheckForAdd(index);来确保容量,容量不够抛出throw new IndexOutOfBoundsException异常,然后就是将数据填入数组,最后size++。

整体大致这样,详细实现还得看源码,我在也找到了大致的流程图:

https://www.cnblogs.com/Chsy/p/12593448.html

2、引用类型

​ 刚开始有点愣,其实指的就是 强软弱虚 ,

3、说一说Hashset

哈希表边存放的是哈希值。 
HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为 true , HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。
哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相
同的元素放在一个哈希桶中)。也就是哈希一样的存一列。 如图 1 表示 hashCode 值不相同的情
况; 图 2 表示 hashCode 值相同,但 equals 不相同的情况。

4、说一说HashMap(1.7和1.8),扩容机制,put操作的过程(答的不好)

1.7的put

简单地说就是把key值进行hash计算,然后判断hash表中是否存在,若存在

public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;
addEntry(hash, key, value, i);
return null;
}

五、ConcurrentHashMap

1.8 ConcurrentHashMap的结构?

​ Node + 链表 + 红黑树,使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结 点)(实现 Map.Entry<K,V>)。锁粒度降低了。

针对 ConcurrentHashMap锁机制具体分析(JDK 1.7 VS JDK 1.8)?

JDK 1.7 中,采用分段锁的机制,实现并发的更新操作,底层采用数组+链表

的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。

①、Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,每个

Segment 对象守护每个散列映射表的若干个桶;

②、HashEntry 用来封装映射表的键-值对;

③、每个桶是由若干个 HashEntry 对象链接起来的链表。

JDK 1.8 中,采用 Node + CAS + Synchronized 来保证并发安全。取消类

Segment,直接用 table 数组存储键值对;当 HashEntry 对象组成的链表长度超

过 TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 +

链表 + 红黑树。

然后接着问了 concurrentHashMap 中哪个地方用了Synchronized,哪个地方用了CAS?

​ 我一时间没想起来,面试官给我提示才想起来,

​ Synchronized锁的是一个Node节点,然后进行的put操作,里面有个putIfAbsent的实现,然后就是利用尾插法向链表尾部插入数据。

​ CAS调用的地方实在addCount( ) ; 中调用了硬件级别的U.compareAndSwap()方法。

六、线程问题

接下来问了些关于线程的问题,问了线程池的基本类型、线程池的参数、线程池的工作原理?

线程池的基本类型:

FixedThreadPool :
详解 创建使用固定线程数的 FixedThreadPool 的 API。适用于为了满足资源管理的 需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 都被设置为创建 FixedThreadPool 时指定的参数 nThreads。 当线程池中的线程数大于 corePoolSize 时,keepAliveTime 为多余的空闲线程 等待新任务的 最长时间,超过这个时间后多余的线程将被终止。这里把 keepAliveTime 设 置为 0L,意味着多余的空闲线程会被立即终止。 FixedThreadPool 使用有界队列 LinkedBlockingQueue 作为线程池的工作队列 (队列的容量为 Integer.MAX_VALUE)。

SingleThreadExecutor :
创建使用单个线程的 SingleThread-Executor 的 API,于需要保证顺序地执行 各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。 corePoolSize 和 maximumPoolSize 被设置为 1。其他参数与 FixedThreadPool 相同。SingleThreadExecutor 使用有界队列 LinkedBlockingQueue 作为线程池的工 作队列(队列的容量为 Integer.MAX_VALUE)。

CachedThreadPool :
创建一个会根据需要创建新线程的 CachedThreadPool 的 API。大小无界的线 程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。
corePoolSize 被设置为 0,即 corePool 为空;
maximumPoolSize 被设置为 Integer.MAX_VALUE。
这里把 keepAliveTime 设置为 60L,意味着 CachedThreadPool 中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。
FixedThreadPool 和 SingleThreadExecutor 使用有界队列 LinkedBlockingQueue 作为线程池的工作队列。CachedThreadPool 使用没有容量的 SynchronousQueue 作为线程池的工作队列,但 CachedThreadPool 的 maximumPool 是无界的。这意味着,如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时, CachedThreadPool 会不断创建新线程。极端情况下,CachedThreadPool 会因为创 建过多线程而耗尽 CPU 和内存资源。

WorkStealingPool :
利用所有运行的处理器数目来创建一个工作窃取的线程池,使用 forkjoin 实 现

ScheduledThreadPoolExecutor :
使用工厂类 Executors 来创建。Executors 可以创建 2 种类型的ScheduledThreadPoolExecutor,如下。
•ScheduledThreadPoolExecutor。包含若干个线程的 ScheduledThreadPoolExecutor。
•SingleThreadScheduledExecutor。只包含一个线程的 ScheduledThreadPoolExecutor。
ScheduledThreadPoolExecutor 适用于需要多个后台线程执行周期任务,同时 为了满足资源管理的需求而需要限制后台线程的数量的应用场景。
SingleThreadScheduledExecutor 适用于需要单个后台线程执行周期任务,同 时需要保证顺序地执行各个任务的应用场景。

线程池的参数:

corePoolSize :
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任 务,直到当前线程数等于 corePoolSize; 如果当前线程数为 corePoolSize,继续提交的任务被保存到阻塞队列中,等 待被执行; 如果执行了线程池的 prestartAllCoreThreads()方法,线程池会提前创建并启 动所有核心线程。
maximumPoolSize :
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则 创建新的线程执行任务,前提是当前线程数小于 maximumPoolSize

keepAliveTime :
线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认 情况下,该参数只在线程数大于 corePoolSize 时才有用

TimeUnit: keepAliveTime 的时间单位

workQueue :
workQueue 必须是 BlockingQueue 阻塞队列。当线程池中的线程数超过它的corePoolSize 的时候,线程会进入阻塞队列进行阻塞等待。通过 workQueue,线 程池实现了阻塞功能
workQueue,用于保存等待执行的任务的阻塞队列,一般来说,我们应该尽量使用有界队 列,因为使用无界队列作为工作队列会对线程池带来如下影响。
1)当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待, 因此线程池中的线程数不会超过 corePoolSize。
2)由于 1,使用无界队列时 maximumPoolSize 将是一个无效参数。
3)由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数。
4)更重要的,使用无界 queue 可能会耗尽系统资源,有界队列则有助于防 止资源耗尽,同时即使使用有界队列,也要尽量控制队列的大小在一个合适的范 围。所以我们一般会使用,ArrayBlockingQueue、LinkedBlockingQueue、 SynchronousQueue、PriorityBlockingQueue。

threadFactory :
创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具 有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有 的线程为守护线程。 静态工厂里默认的 threadFactory,线程的命名规则是“pool-数字 -thread-数字”。

RejectedExecutionHandler :
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提 交任务,必须采取一种策略处理该任务,线程池提供了 4 种策略:
(1)AbortPolicy:直接抛出异常,默认策略;
(2)CallerRunsPolicy:用调用者所在的线程来执行任务;
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务; 当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和 策略,如记录日志或持久化存储不能处理的任务。

线程池的工作原理:

1)如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务(注意, 执行这一步骤需要获取全局锁)。 
2)如果运行的线程等于或多于 corePoolSize,则将任务加入 BlockingQueue。
3)如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处 理任务。
4)如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被 拒绝,并调用 RejectedExecutionHandler.rejectedExecution()方法。

七、JVM

分别问了内存结构、判断对象存活、各种引用对GC的影响、垃圾回收算法、垃圾回收器。

内存结构

自己画了图:

https://www.processon.com/view/link/5f198920e401fd181ad55cf1

判断对象存活

JVM采用可达性分析算法,通过GCRoots向下搜索,会产生Reference Chain的链条,当一个对象不与GCRoot有任何关系时,就会判断为垃圾。

接着提问:GCRoots有哪些?
作为 GC Roots 的对象包括下面几种(重点是前面 4 种):
1、 虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
2、 方法区中类静态属性引用的对象;java 类的引用类型静态变量。
3、 方法区中常量引用的对象;比如:字符串常量池里的引用。
4、 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
5、 JVM 的内部引用(class 对象、异常对象 NullPointException、OutofMemoryError,系统类加载器)。(非重点)
6、 所有被同步锁(synchronized 关键)持有的对象。(非重点)
7、 JVM 内部的 JMXBean、JVMTI 中注册的回调、本地代码缓存等(非重点)
8、 JVM 实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收只回收部分代的对象,这个后续会细讲,先大致了解概念)(非重点)

各种引用对GC的影响

强引用 :
一般的 Object obj = new Object() ,就属于强引用。在任何情况下,只有有强引用关联(与根可达)还在,垃圾回收器就永远不会回收掉被引用的对象。

软引用 SoftReference :
一些有用但是并非必需,用软引用关联的对象,系统将要发生内存溢出(OuyOfMemory)之前,这些对象就会被回收(如果这次回收后还是没有足够的 空间,才会抛出内存溢出)

弱引用 WeakReference :
一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC 发生时,不管内存够不够,都会被回收。

虚引用 PhantomReference :
幽灵引用,最弱(随时会被回收掉) 垃圾回收的时候收到一个通知,就是为了监控垃圾回收器是否正常工作。

垃圾回收算法

复制算法(Copying):
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使 用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可, 实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。 但是要注意:内存移动是必须实打实的移动(复制),所以对应的引用(直接指针)需要调整。 复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。

标记-清除算法(Mark-Sweep):
算法分为“标记”和“清除”两个阶段:
首先扫描所有对象标记出需要回收的对象,在标记完成后扫描回收所有被标记的对象,所以需要扫描两遍。
回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低。
它的主要问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连 续内存而不得不提前触发另一次垃圾回收动作。 回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代。

标记-整理算法(Mark-Compact) :
首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端 边界以外的内存。
标记整理算法虽然没有内存碎片,但是效率偏低。 我们看到标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用 对象的地方都需要更新(直接指针需要调整)。 所以看到,老年代采用的标记整理算法与标记清除算法,各有优点,各有缺点。

垃圾回收器

自己花了图:https://www.processon.com/view/link/5f198920e401fd181ad55cf1

每个回收器的执行顺序,也参考了别人的图:

GC详解

JVM各种垃圾回收器

垃圾回收算法——复制算法

STW

CMS垃圾回收器

G1垃圾回收器

八、接着问了MySQL

B+ 树:

数据只存储在叶子节点上,非叶子节点只保存索引信息; 
◦ 非叶子节点(索引节点)存储的只是一个 Flag,不保存实际数据记录;
◦ 索引节点指示该节点的左子树比这个 Flag 小,而右子树大于等于这个 Flag
叶子节点本身按照数据的升序排序进行链接(串联起来);
◦ 叶子节点中的数据在 物理存储上是无序 的,仅仅是在 逻辑上有序 (通过指针串在一 起);
B+树的作用
 在块设备上,通过 B+树可以有效的存储数据;
 所有记录都存储在叶子节点上,非叶子(non-leaf)存储索引(keys)信息;
 B+树含有非常高的扇出(fanout),通常超过 100,在查找一个记录时,可以有效的减 少 IO 操作;
B+树的扇出
扇出 :是每个索引节点(Non-LeafPage)指向每个叶子节点(LeafPage)的指针
扇出数 = 索引节点(Non-LeafPage)可存储的最大关键字个数 + 1

MySQL锁:

表级锁:
开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发 度最低。
行级锁:
开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发 度也最高。
页面锁(gap 锁,间隙锁):
开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度 界于表锁和行锁之间,并发度一般。

MySQL基本数据类型:

#1. 数字:
整型:tinyinit int bigint
小数:
float :在位数比较短的情况下不精准
double :在位数比较长的情况下不精准
0.000001230123123123
存成:0.000001230000

decimal:(如果用小数,则用推荐使用decimal)
精准
内部原理是以字符串形式去存

#2. 字符串:
char(10):简单粗暴,浪费空间,存取速度快
root存成root000000
varchar:精准,节省空间,存取速度慢

sql优化:创建表时,定长的类型往前放,变长的往后放
比如性别 比如地址或描述信息

>255个字符,超了就把文件路径存放到数据库中。
比如图片,视频等找一个文件服务器,数据库中只存路径或url。

#3. 时间类型:
最常用:datetime

#4. 枚举类型与集合类型

九、最后给了个情景题

​ 有10万个用户同时登陆,如何设置黑名单,使性能最佳。

​ 我自己的答案是使用Redis进行控制,0是白名单,1是黑名单。但是设计时,key值怎么设计?这点我没想出来怎么合适,而且怎么保证效率性能最高?目前自己还在百度中。。。。。。

发布于

2020-07-24

更新于

2022-03-25

许可协议

评论

:D 一言句子获取中...

加载中,最新评论有1分钟缓存...