1.在项目经历里挑一个详细介绍一下 项目的应用场景
2.项目里用到多线程是怎么用的?回答:线程池
用通过 ThreadPoolExecutor
构造函数的方式创建的线程池
3.线程池有哪些重要参数?回答:核心线程数、最大线程数、阻塞队列类型、抛出策略还有线程池类型。
线程池的核心线程数、最大线程数,当线程数大于核心线程数时多余的线程存活的时间,任务队列,拒绝策略。
4.线程池类型有哪些?回答:有固定一个线程的还有按照参数指定线程数的,还有按时执行任务的。
基于ThreadPoolExecutor
共有四种类型线程池
FixedThreadPool
:固定数量线程池
SingleThreadExecutor
:单线程线程池
CachedThreadPool
:缓存线程池(是一种大小可变的线程池,线程数量根据任务的数量自动调整。)
ScheduledThreadPoolExecutor
:调度线程池(周期性地执行任务,或者在指定的时间执行任务。)
一般不建议采用Excutors类创建上述线程池,而是建议大家使用构造函数去创建线程池,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
(1)FixedThreadPool
和 SingleThreadExecutor
:使用的是无界阻塞队列,任务队列最大长度为 Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。
(2)CachedThreadPool
:使用的是同步队列,允许创建的线程数量为 Integer.MAX_VALUE
,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。
(3)ScheduledThreadPool
和 SingleThreadScheduledExecutor
: 使用的无界的延迟阻塞队列DelayedWorkQueue
,任务队列最大长度为 Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。
- 容量为
Integer.MAX_VALUE
的LinkedBlockingQueue
(无界队列):FixedThreadPool
和SingleThreadExector
。由于队列永远不会被放满,因此FixedThreadPool
最多只能创建核心线程数的线程。 SynchronousQueue
(同步队列):CachedThreadPool
。SynchronousQueue
没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,CachedThreadPool
的最大线程数是Integer.MAX_VALUE
,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。DelayedWorkQueue
(延迟阻塞队列):ScheduledThreadPool
和SingleThreadScheduledExecutor
。DelayedWorkQueue
的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue
添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达Integer.MAX_VALUE
,所以最多只能创建核心线程数的线程。
5.实际工作中参数怎么选择呢?比如核心线程数?回答:核心线程数和cpu个数有关,一般是2n+1个
这是固定的?不是固定的,要看任务是主要使用cpu的还是占用cpu比较少的。
如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的,CPU 根本没有得到充分利用。
如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
假设机器有N个CPU,那么对于计算密集型的任务(压缩、解压缩、加密、解密、科学计算等),应该设置线程数为N+1;对于IO密集型的任务(MySQL 数据库、文件的读写、网络通信),应该设置线程数为2N;对于同时有计算工作和IO工作的任务,应该考虑使用两个线程池,一个处理计算任务,一个处理IO任务,分别对两个线程池按照计算密集型和IO密集型来设置线程数。
最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间))
,其中 WT(线程等待时间)=线程运行总时间 - ST(线程计算时间),WT/ST可以通过VisualVM 来查看。
综合来看,我们可以根据自己的业务场景,从“N+1”和“2N”两个公式中选出一个适合的,计算出一个大概的线程数量,之后通过实际压测,逐渐往“增大线程数量”和“减小线程数量”这两个方向调整,然后观察整体的处理时间变化,最终确定一个具体的线程数量。
6.多线程会带来一些并发问题,一般用锁来处理。乐观锁和悲观锁你了解吗都是什么概念?应用于什么场景
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。常见的悲观锁有java的synchronized、ReentrantLock
等独占锁。悲观锁通常用于写比较多的情况,可以避免频繁失败和重试影响性能,开销是固定的。
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。JUC包下的原子变量类,比如AtomicInteger、LongAdder就是使用了乐观锁的一种实现方式CAS实现的。乐观锁通常用于写比较少的情况,比如多读场景,竞争比较少,可以避免频繁加锁影响性能。不过乐观锁主要针对的对象是单个共享变量。
从 JDK 1.5 开始,提供了AtomicReference
类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference
类把多个共享变量合并成一个共享变量来操作。
7.你用过的锁有哪些,sychronized锁升级的流程
sychronized锁升级过程
无锁:没有开启偏向锁的时候的状态
偏向锁状态:如果还没有一个线程拿到这个锁的话,当一个线程拿到偏向锁的时会将对象MarkWord当中存储的偏向线程ID改为自己,下次想要竞争只需要比较此ID
轻量级锁:如果线程1没有退出同步代码块,而后续线程通过CAS进行修改是不能成功的,那么这个时候后续的线程就将synchronized升级为轻量级锁,不断自旋进行CAS操作
重量级锁:多个线程竞争轻量级锁则锁升级为重量级锁。
reentrantlock(可重入锁):对于同一个线程,如果连续两次对同一把锁进行lock,对于一般的锁线程就会卡在这里。而在可重入锁中,同一个线程可以对同一把锁,在不释放的前提下,反复加锁,而不会导致线程卡死。
JUC包
同步器
ReentrantLock
ReentrantLock 和 synchronized 的区别(他们两个全是可重入锁):
- synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
- synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放.
- synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
- ReentrantLock 可以实现公平锁
- 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
如何选择使用哪个锁?
- 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
- 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
- 如果需要使用公平锁, 使用 ReentrantLock.
Semaphore
允许n个任务同时访问某个资源,可以将信号量看做是在向外分发使用资源的许可证,只有成功获取许可证,才能使用资源。acquire获取令牌,release归还令牌。
CountDownLatch(基于AQS实现)
计数器,同时等待 N 个任务执行结束。好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。
CyclicBarrier(基于ReentrantLock实现,也是基于AQS实现)
循环屏障,它可以协同多个线程,让多个线程在这个屏障前等到,直到所有线程都到达了这个屏障时,再一起执行后面的操作。假如每个线程各有一个await,任何一个线程运行到await方法时就阻塞,直到最后一个线程运行到await时才同时返回。
原子变量
Java——聊聊JUC中的原子变量类_宋子浩的博客-CSDN博客Java——聊聊JUC中的原子变量类https://blog.csdn.net/weixin_43823808/article/details/128992228Atomic包中原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。
有4种类型的原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新属性,Atomic包里的类基本都是使用Unsafe实现的包装类.
原子更新基本类型
(1)AtomicBoolean:原子更新布尔类型
(2)AtomicInteger:原子更新整型
(3)AtomicLong:原子更新长整型
采用volatile加CAS来保证并发安全,都是基于CAS的,细节没看
LongAdder(用于多个线程同时对同一个变量进行加法操作)
LongAdder的底层实现原理主要包括两个重要概念:“Cell"和"Base”。
“Cell”(单元)
LongAdder内部维护了一个或多个"cell",每个"cell"都是一个独立的变量,用于存储部分加法操作的结果。在初始化时,默认情况下会创建一个"cell"。
“Base”(基准)
除了"cell",LongAdder还维护了一个名为"base"的变量,用于存储没有被分配到"cell"的加法操作的结果。"base"是一个普通的long类型变量。
当一个线程进行加法操作时,LongAdder会采取以下策略:
如果当前线程是第一个进行加法操作的线程,那么直接将操作的结果累加到"base"上。
如果当前线程不是第一个进行加法操作的线程,那么LongAdder会将操作的结果累加到当前线程所分配的"cell"上。
当多个线程同时进行加法操作时,每个线程都会被分配到一个独立的"cell",从而减少了竞争条件。当需要获取最终的加法结果时,LongAdder会将"base"和所有"cell"中的值进行求和,得到最终的结果。
原子更新字段类
如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新
(1)AtomicIntegerUpdater:原子更新整型的字段的更新器
(2)AtomicLongFieldUpdater:原子更新长整型字段的更新器
(3)AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能发现的ABA问题。
使用demo如下:
class BankAccount {
public String bankName = "CCB";
public volatile int money = 0;
AtomicIntegerFieldUpdater<BankAccount> fieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "money");
public void addMoney(BankAccount bankAccount) {
fieldUpdater.getAndIncrement(bankAccount);
}
}
public class AtomicIntegerFieldUpdateDemo {
public static void main(String[] args) throws InterruptedException {
BankAccount bankAccount = new BankAccount();
CountDownLatch cdl = new CountDownLatch(10); //计数器
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 1000; j++) {
bankAccount.addMoney(bankAccount); //10个线程,每个线程对money执行1000次自增操作
}
} finally {
cdl.countDown(); //每执行完一个线程,计数器减一
}
}, String.valueOf(i)).start();
}
cdl.await(); //阻塞等待,直到10个线程全部执行完,计数器清零,程序继续向下执行
System.out.println(Thread.currentThread().getName() + " result: " + bankAccount.money);
}
}
AQS
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
8.看你的简历有一个oom的排查和一个循环依赖的排查,能详细讲讲吗(这里讲的不好,再深入一下)
日志显示的OOM类型是堆空间不足 OutOfMemoryError: Java heap space
查看dump文件,默认存储在启动该jar的用户根目录下,用MemoryAnalyzer对dump文件进行分析,发现占用内存异常的是一个map,定位到DistributedFileSystem,以此为关键字在项目里排查。
定位到代码中发现是之前为解决“两个HDFS集群namespace同名时会造成两个集群读取到同一套文件系统”的问题,由调用get(hadoopConfig)方法换为调用newInstance方法新建实例。但是该方法中会一直往一个静态的cache的map里放值,在定时多次调用的情况下,map一直不会被回收但是内部一直在添加新值造成OOM。后续解决方法是将FileSystem保存到map缓存里,FileSystem是通过集群名和集群配置生成的,如果map缓存中存在该实例就从缓存中读取,不存在才新增。
FileSystem是根据集群名和配置生成的,底层有http连接等状态信息,之前的get方法只区分配置。
怎么排查OOM?
线上如遇到 JVM 内存溢出,可以分以下几步排查
-
jmap -heap
查看是否内存分配过小
在 Java 虚拟机(JVM)中,-Xms 和 -Xmx 都是用来设置 JVM 堆内存大小的参数。其中,-Xms 用于设置 JVM 启动时分配的初始堆内存大小,而 -Xmx 用于设置 JVM 堆内存的最大可用空间。默认情况下,-Xms参数的值为物理内存的1/64,-Xmx参数的值为物理内存的1/4。
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2,Eden : from : to = 8 : 1 : 1
-
jmap -histo
查看是否有明显的对象分配过多且没有释放情况 -
jmap -dump
导出 JVM 当前内存快照,使用 JDK 自带或 MAT 等工具分析快照
如果上面还不能定位问题,那么需要排查应用是否在不断创建资源,比如网络连接或者线程,都可能会导致系统资源耗尽。
9.你项目哪里用到了redis 怎么用的(这里因为没问过也没深入下去,接下来把redis加入到项目里)
10.项目主要用mysql,处理过海量数据吗 有哪些优化手段?一般对哪些字段建索引,索引的个数为什么不是越多越好?
1.哪些情况需要创建索引:
在数据库数据量比较大的情况下,如果有一些高频查询的字段、连表查询的一些字段、有排序需求、有分组需求这些场景都可以考虑建立索引来优化。
(1)单表索引优化:频繁作为查询条件的字段需要建立索引,在where语句用到的字段加索引,能建联合索引时建立联合索引,联合索引时将高区分度的字段放到前面。建立索引的字段尽量选择区分度高的 。频繁更新的字段不适合创建索引。更新字段的过程中,需要维护B+树结构,会频繁更新索引文件,降低SQL性能。过长的字段不适合创建索引(占用空间多)。有序的字段比无序字段更适合加索引。
不要创建过多的索引,会占用更多存储空间,另外也会严重影响SQL性能,每次更删改查SQL都需要更新大量索引文件。一般一张表索引不超过5个。
实用场景:以项目中运行流水线场景为例,有pipeline_history表、pipeline_stage_history表(里面包含stage所属于的pipeline_history_id、site(stage次序))、pipeline_chain_history表(里面包含chain所属于的stage_id、site(chain次序))、pipeline_job_history表(里面包含job所属于的chain_id、site(job次序)),当某个job执行完成时需要获取和他同属于一个chain的下一个job,就需要在pipeline_job_history表里找chain_id与自己相同、次序为自己下一个的记录。在pipeline_job_history表内数据400w的情况下直接查询需要4s时间,而为(chain_id,site)建立一个联合索引以后查询只需要0.001s了。同样的道理也为pipeline_chain_history表里(stage_id,site)、pipeline_stage_history表的(pipeline_history_id,site)建表,这样能大幅度提升查询速度。
(2)连表查询优化:保证被驱动表上的连接字段建了索引。左外连接给右边大表连接字段建立索引,右外连接的话给左边大表连接字段建立索引,内连接时mysql会自动选择小的做驱动表,这时候需要给大表加索引。
连接时有一张表会全表扫描保留,称为驱动表,另外一个连接到驱动表的称为被驱动表。
select * from t1 left join t2 on t1.card=t2.card
一般把数据量小的放在左边,当做驱动表,t2就是被驱动表,可以在t2的card字段建索引。
(3)排序优化:尽量避免出现using FileSort,order by语句满足最左匹配或者是where与order by的部分子句满足最左匹配都可以命中索引。where子句中如果出现了范围查询会导致后面order by索引失效。
以下都可以使用到联合索引 (a,b,c):
where a=常量 order by b,c
where a=常量 b=常量 order by c
where a=常量 b>常量 order by c
order by a order by a,b order by a,b,c
不能使用的:
order by a ASC,b DESC,c DESC(排序不一致)
order by b,c(不满足最左匹配)
where a in (...) order by b,c(a使用了范围查询,只能用到a)
(4)分组优化:和排序的类似,where和group by组合起来要满足最左匹配原则。
2.慢查询日志:
会把超过查询阈值的sql记录到慢查询日志中。
默认在\data文件夹下,一般非调优场景下不建议启动改参数。
3.索引失效情况:
复合索引不匹配最左匹配、不能在索引上使用计算函数,使用不等于、is not null、like查询以%开头都会全表扫描,中间出现范围查询只能使用部分索引。字符串不加单引号,会发生隐形类型转换也会导致索引失效,使用or连接时索引也会失效。
4.mysql主从复制
为什么要主从复制?
(1)在业务复杂的系统中,有一句sql语句需要锁表,就会导致表不可读,可能会影响运行中的业务,使用主从复制可以让主库负责写,从库负责读,这样即使主库锁表,从库也可以保证业务正常运行。
(2)做数据备份
(3)业务量大,I/O访问效率过高,单机无法满足,需要多库存储提升单个机器的I/O性能。
主从复制怎么实现?
主库会将数据库中数据的变化写入到binlog,从库连接主库,从库创建一个I/O线程向主库请求更新的binlog,从库I/O线程将接收的binlog写入到relay log中,从库的SQL线程读取relay log同步数据到本地。
主从复制的三种模式:
1)异步复制(默认):主库在执行完客户端提交的事务之后会立即将结果返回给客户端,并不关心从库是否已经接收并处理。主库将事务Binlog事件写入到Binlog文件中,此时主库只是通知Dump线程发送这些新的Binlog,然后主库就会继续处理提交操作,并不保证这些Binlog传到任何一个从库节点上,在主库故障情况下可能丢失。
2)同步复制:主库提交事务之后,所有从库节点必须收到,APPLY并提交这些事务之后主库线程才能继续做后续操作,影响性能。
3)半同步复制:主库在执行完客户端提交的事务后不是立刻返回客户端,而是等待至少一个从库接收到并写到relay log中后才返回客户端。属于上述两种方法的折中。
复制延迟(主从同步延迟)怎么解决?
延迟是一定存在的,只能尽量减少。从数据库层面可以修改从库的一些设置,比如将同步binlog时间间隔设置为0或者是关闭binlog,可以关闭节省一部分I/O时间防止造成大量延迟。主库和从库要在同一个网络交换机下,减少网络延迟。
从编码角度:(1)对于一些必须获取最新数据的读请求,将他强制路由到主库处理。可以使用Sharding-JDBC的分片键值管理器强制使用主库。
从业务流程设计角度:设置延迟读取,比如支付成功以后跳转到支付成功页面,需要等待几秒钟才能返回自己的账户。
5.覆盖索引
11.看简历写到过设计模式,你熟悉哪些设计模式? 回答了建造者、简单工厂。
了解代理模式吗?哪里用过代理模式
静态代理实现步骤:
- 定义一个接口及其实现类;
- 创建一个代理类同样实现这个接口
- 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
MethodInterceptor
中的 intercept
方法,用enhancer创建的代理类。
是通过生成一个被代理类的子类,并重写父类方法。
12.hashmap结构
13.怎么判断能获取到的mysql锁是表级锁还是行级锁?
14.select for update 语句什么情况下会上锁,会上哪种锁
select for update行锁or表锁,20个场景分析,还真得看情况_for update锁表_竹林幽深的博客-CSDN博客
无论哪个版本的MySQL,查询条件用到索引(主键、唯一索引、普通索引)的情况下为行锁。
查询条件用到索引,事务隔离级别为可重复读时,MySQL还会添加一个间隙锁,条件内的插入、更新会被阻塞。
事务隔离级别为可重复读时,查询条件无索引,为表锁。
事件隔离级别为读已提交时,查询条件无索引也是行锁。
一些非技术相关提问:
1.过去开发时候 产品 测试 前端这些都有吧 介绍一下大概多少人 都有哪些角色参与生产
提问:组里的人员构成和架构?组里主要做什么 信贷平台的底层平台 做一些支付或者用户权限的认证 十个人 每两三个一组。初中高级比例大概怎么样?一比一
2.如果已经开发要完成了,产品提出加需求怎么办
3.今天面试我很多地方深入的了解都不够,你比较看中求职者哪些方面技术能力。比如redis这种我没有实际没有应用的,但是自己也看了一下,但没有实际经验面试官也不接着问下去了,比如这种知识我怎么准备呢
这还是每个面试官有不同的侧重点,从我的角度我更看重对你用过的技术的考察,如果工作三到四年没用过redis就不正常,但是你经验一年可以理解,但是目前大多数面试的十个人里面只有一两个没用过,所以还是建议多一些实际使用的经验。
总结:
1.多线程、mysql调优这些用过的但是不深入的东西需要重点学习!
2.redis看完基础教程以后加入到项目里,不要让他成为短板。
3.OOM和循环依赖那个的介绍要再熟悉深入一下。
4.leetcode还是要继续刷。
Mysql调优:
一般mysql单表存储数据量小于1千万左右的时候,它的读写性能是最好的,这个程度的数据范围就可以使用索引进行优化解决。如果数据量达到千万级以上就要采用分库分表或者是读写分离的手段进行优化了。
读写分离:一般选择一主多从,也就是一台主数据库负责写,其他从数据库负责读。主库和从库之间要进行数据同步