Java大厂面试题集
一、volatile
1、基本介绍
volatile 是Java虚拟机提供到轻量级的同步机制 只要主内存修改了,会通知工作内存,数据同步。
2、基本特性
2.1 保证可见性
主内存修改来,其它线程的都会收到通知,拷贝到自己的工作内存
2.2 不保证原子性
不能保证原子性,会导致多线程中数据写入内存与通知其它线程数据同步是出现不一致,不能保正线程安全。
2.3 禁止指令重排
指令重排,在单线程中是不会受到影响的,而在多线程中指令重排会导致数据出问题,这样可以通过关键字volatile 禁止指令重排。
二、JMM
1、基本介绍
JMM(java内存模型Java Memory Model ,简称JMM )本身是一种抽象的概念并不真实存在
,它描述的是一组规范或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
JMM关于同步的规定:
1、线程解锁前,必须把共享变量的值刷新回主内存。
2、线程加锁前,必须读取主内存的最新值到自己的工作内存。
3、加锁解锁是同一把锁。
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存
,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后将变量写回主内存,
不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
2、基本特性
1.1 可见性
主内存修改来,其它线程的都会收到通知,拷贝到自己的工作内存
1.2 原子性
不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整要么成功,要么同时失败。
1.3 volatile
volatile 具体内容请回看大纲一
1.4 有序性
计算机在在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分为以下3种:
--源代码-->编译器优化对重排-->指令并行对重排-->内存系统对重排-->最终执行对指令
单线程环境里面确保程序最终执行结果和代码顺序执行对结果一致。
处理器在进行重排序时必须要考虑指令之间对数据依赖性
多线程环境中线程交替执行,由于编译器优化重排对存在,两个线程中使用对变量能否保证一致性是无法确定对,结果无法预测。
三、问题解决
1、怎么解决原子性
通过JUC 下 AtomicInteger 类,此类是带原子性的包装类
通过 getAndIncrement() 方法 调用原子性的增加1 并返回值(先获得再增加)
2、凭什么AtomicInteger 类能保证原子性?
CAS 自旋锁来保证原子一致性。
四、CAS
1、比较并交换。
public class CASDemo{
public static void main(String[] a)
}
2、CAS底层原理,淡淡UnSafe的理解。
3、CAS缺点。
五、 DCL 单例模式双端解锁
(双端解锁)机制不一定线程安全,原因是有指令重排序单存在,加入volatile可以禁止指令重排
原因在于某个线程执行到第一检测,读取到到instance不为null时,instance的引用对象可能没有完成初始化。
如果数据不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变。在多线程中就会存在问题了。
六、多线程面试
1、java字符串常量池
JDK Version版本号类中已经加入了java 值
2、字节跳动两数求和
public static void main(String[] agrs){
Integer[] arrays= new Integer[]{2,7,8,10};
Integer tg=9;
//求得数组中两数之和等于9
sum1(arrays,tg);
}
//暴力解法
public static Integer[] sum1(Integer[] arrs,Integer tg){
for(int i=0;i<arrs.length;i++){
for(int j=i+1;j<arrs.length;j++){
if(tg-arrs[i]=arrs[j]){
return new Integer[]{i,j};
}
}
return null;
}
}
//hash解法
public static Integer[] sum2(Integer[] arrs,Integer tg){
Map<Integer,Integer> map= new HashMap<>();
for(int i=0;i<arrs.length;i++){
Integer part= tg-arrs[i];
if(map.containsKey(part)){
return new Integer[map.get(part),i];
}
map.put(arrs[i],i);
}
return null;
}
3、JUC 线程锁ReEnterLock
//可重入锁 分为三层:外 ,中,内
//同步代码块
public class ReEnterLock {
static Object objectLockA=new Object();
public static void m1(){
new Therad(()->{
synchronized(objectLockA){
Systen.out.println(Thread.currentThread().getName()+"\t"+"-----外层调用");
synchronized(objectLockA){
Systen.out.println(Thread.currentThread().getName()+"\t"+"-----中层调用");
synchronized(objectLockA){
Systen.out.println(Thread.currentThread().getName()+"\t"+"-----内层调用");
}
}
}
},"t1").start();
}
public static void main(String[] args){
m1();
}
}
//同步代码方法
public class ReEnterLock {
public synchronized void m1(){
Systen.out.println(Thread.currentThread().getName()+"\t"+"-----外层调用");
m2();
}
public synchronized void m2(){
Systen.out.println(Thread.currentThread().getName()+"\t"+"-----中层调用");
m3();
}
public synchronized void m3(){
Systen.out.println(Thread.currentThread().getName()+"\t"+"-----内层调用");
}
public static void main(String[] args){
new ReEnterLock().m1();
}
}
sybchronized的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向有该持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置当前线程,并且将其计数器加1.
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitirexit时,Java虚拟机则需将锁对象的计数器减1.计数器为零代表锁已被释放。
知识点:LockSupport
static Object objectLock=new Object();
static Lock lock =new ReentrantLock();
static Condition condition=lock.newCondition();
public static void main(String[] args){
new Thread(()->{
lock.lock();
try{
Systen.out.println(Thread.currentThread().getName()+"\t"+"-----come in");
try{
condition.await();
}catch(InterruptedExeption e){
e.printStackTrace();
}
}finally{
lock.nulock();
}
}
).start();
}
Park与unPark累计一次,不能重入。
LockSupport 提供Pack和nuPack方法实现阻塞需线程和解除线程的过程
lockSupport 和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0
调用一次unpark就加1变成1
调用一次park会消费permit,也就是将1变成0,同时park立即返回。
如果再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1.
每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也会积累凭证。
形象的理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
当调用park方法时
*如果有凭证,则会直接消耗掉这个凭证然后正常退出;
*如果无凭证,就必须阻塞等待凭证可用。
而unpark则相反
它会增加一个凭证,但凭证最多只能有1个,累加无效。
3、AQS抽象队列同步器
// AbstractQueuedSynchronizer 简称为:AQS
// extends AbstractOwnableSynchronizer
// 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO(先进先出)队列
// 来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。
//
大致模型如图所示:
CLH: Craig 、Landin and Hagersten 队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO
1、ReentrantLock
Syns extends AbstractQueuedSynchronizer 类
2、CountDownLatch
3、ReentrantReadWriteLock
4、Semaphore
4、 公平锁与非公平锁区别
1、公平锁 tryAcquire
protected final boolean tryAcquire( int acquirces){
final Tread current =Thread.currentThread();
int c=getState(); //判断状态
if(c=0){
// hasQueuedPredecessors 判断等待队列中是否存在有效节点
if(!hasQueuedPredecessors() && compareAndSetState(0,acquires)){
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()){
int nextc =c + acquires;
if(nextc <0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
2、非公平锁 nonfairTryAcquire
protected final boolean tryAcquire( int acquirces){
final Tread current =Thread.currentThread();
int c=getState(); //判断状态
if(c=0){
// 尝试抢占锁
if(compareAndSetState(0,acquires)){
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()){
int nextc =c + acquires;
if(nextc <0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
3、两种锁的区别
从上面代码可以得知,公平锁与非公平锁的唯一区别是在公平锁的基础之上多加了一个条件:
hasQueuedPredecessors()
方法
hasQueuedPredecessors
是公平锁加锁时判断等待队列中是否存在有效节点的方法
5、Spring 切面编程AOP
# 正常执行顺序
// spring版本:4.3.13.RELEASE springboot 版本: 1.5.9.RELEASE
// 环绕通知 -- start
// @Before 前置通知执行
// 执行目标方法
// 环绕通知 -- end
// @After 后置通知执行
// @AferReturning 返回后通知。
# 异常执行顺序
// spring版本:4.3.13.RELEASE springboot 版本: 1.5.9.RELEASE
// 环绕通知 -- start
// @Before 前置通知执行
// 执行目标方法--产生异常了
// @After 后置通知执行
// @AfterThrowing 异常通知执行
# 版本变化,顺序改变
# 正常执行顺序
// spring版本:5.2.8.RELEASE springboot版本:2.2.3.RELEASE
// 环绕通知 -- start
// @Before 前置通知执行
// 执行目标方法
/**----变化如下-----
* @AferReturning 返回后通知。
* @After 后置通知执行
* 环绕通知 -- end
*/
# 异常执行顺序
// spring版本:4.3.13.RELEASE springboot 版本: 1.5.9.RELEASE
// 环绕通知 -- start
// @Before 前置通知执行
// 执行目标方法--产生异常了
/**----变化如下-----
* @AfterThrowing 异常通知执行
* @After 后置通知执行
*/
具体业务代码
@Aspect
@Component
public class MyAspect{
/**
* 前置通知处理方法
*/
@Before("切入点表达式")
public void beforeNotify(){
System.out.println("---> @Before 前置通知执行");
}
/**
* 后置通知处理方法
*/
@After("切入点表达式")
public void afterNotify(){
System.out.println("---> @After 后置通知执行");
}
/**
* 返回时触发的方法
*/
@AfterReturning("切入点表达式")
public void afterReturningNotify(){
System.out.println("---> @AfterReturning 返回通知执行");
}
/**
* 异常通知处理方法
*/
@AfterThrowing("切入点表达式")
public void afterThrowing(){
System.out.println("---> @AfterThrowing 异常通知执行");
}
/**
* 环绕通知处理方法
* Proceedingjoinpoint 继承了 JoinPoint
* JointPoint是程序运行过程中可识别的点,这个点可以用来作为AOP切入点。
* JointPoint对象则包含了和切入相关的很多信息。比如切入点的对象,方法,属性等。
* 我们可以通过反射的方式获取这些点的状态和信息,用于追踪tracing和记录logging应用信息。
* proceed 执行目标方法,指代切入点的实际方法
*/
@Around("切入点表达式")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
Object retValue=null;
System.out.println("---> 环绕通知开始");
//执行目标方法
retValue = joinPoint.proceed();
System.out.println("---> 环绕通知结束");
return retValue;
}
}
切入点表达式
1)execution:用于匹配子表达式。
//匹配com.cjm.model包及其子包中所有类中的所有方法,返回类型任意,方法参数任意
@Pointcut("execution(* com.cjm.model..*.*(..))")
public void before(){}
2)within:用于匹配连接点所在的Java类或者包。
//匹配Person类中的所有方法
@Pointcut("within(com.cjm.model.Person)")
public void before(){}
//匹配com.cjm包及其子包中所有类中的所有方法
@Pointcut("within(com.cjm..*)")
public void before(){}
3) this:用于向通知方法中传入代理对象的引用。
@Before("before() && this(proxy)")
public void beforeAdvide(JoinPoint point, Object proxy){
//处理逻辑
}
4)target:用于向通知方法中传入目标对象的引用。
@Before("before() && target(target)
public void beforeAdvide(JoinPoint point, Object proxy){
//处理逻辑
}
5)args:用于将参数传入到通知方法中。
@Before("before() && args(age,username)")
public void beforeAdvide(JoinPoint point, int age, String username){
//处理逻辑
}
6)@within :用于匹配在类一级使用了参数确定的注解的类,其所有方法都将被匹配。
//所有被@AdviceAnnotation标注的类都将匹配
@Pointcut("@within(com.cjm.annotation.AdviceAnnotation)")
public void before(){}
7)@target :和@within的功能类似,但必须要指定注解接口的保留策略为RUNTIME。
@Pointcut("@target(com.cjm.annotation.AdviceAnnotation)")
public void before(){}
8)@args :传入连接点的对象对应的Java类必须被@args指定的Annotation注解标注。
@Before("@args(com.cjm.annotation.AdviceAnnotation)")
public void beforeAdvide(JoinPoint point){
//处理逻辑
}
9)@annotation :匹配连接点被它参数指定的Annotation注解的方法。也就是说,所有被指定注解标注的方法都将匹配。
@Pointcut("@annotation(com.cjm.annotation.AdviceAnnotation)")
public void before(){}
10)bean:通过受管Bean的名字来限定连接点所在的Bean。该关键词是Spring2.5新增的。
@Pointcut("bean(person)")
public void before(){}
5、Spring 之循环依赖
多个bean之间相互依赖,形成了一个闭环。比如:A依赖于B、B依赖于C、C依赖于A
通常来说,如果问Spring容器内部如何解决循环依赖,因为Spring中默认的单列Bean中,属性互相引用的场景。
依赖注入的方式有2种
**第一种:**通过构造方式进行依赖注入
- 结论 :构造器循环依赖是无法解决循环依赖问题。
**第二种:**通过以set方式注入依赖
- 结论:通过set方法可以解决循环依赖。
案例:XML配置
- 默认的单列模式( singleton )的场景是支持循环依赖。
- 改成原型(Prototype)的场景是不支持循环依赖
解决方案
- spring内部通3级缓存来解决循环依赖
DefaultSingletonBeanRegistry 对象来解决 其中有三个Map
- ConcurrentHashMap 一级缓存
/** * 单例对象的缓存:bean 名称-bean实例,即:所谓的单例池 * 表示已经经历了完整的生命周期的Bean对象 */ /** Cache of singleton objects :bean name to bean instance */ private final Map<String,Object> singletonObjects=new ConcurrentHashMap<>(256);
- HashMap<String,ObjectFactory<?>> 三级缓存
/** * 单例工厂的高速缓存: bean 名称 - ObjectFactory * 表示存放生成 bean 的工厂 */ /** Cache of singleton factories: bean name to ObjectFactory*/ private final Map<String,ObjectFactory<?>> singletonFactories =new HashMap<>(16);
- HashMap<String,Object> 二级缓存
/** * 早期的单例对象的高速缓存:bean 名称--bean 实例 * 表示Bean 的生命周期还没有走完(Bean 的属性还未填充)就把这个Bean存入该缓存中 * 也就是实例化但未初始化的bean 放入该缓存里 */ /**Cache of early singleton objects :bean name to bean instance*/ private final Map<String,Object> earlySingletonObjects=new HashMap<>(16);
- LinkedHashSet
/** set of registered singletons,containing the bean names isn registration order*/ private final Set<String> registeredSingletons=new LinkedHashSet<>(256);
Bean 对象生命周期
经历过程
- getSingleton
- doCreateBean
- populateBean
- addSingleton
三级缓存迁移过程
1 A 创建过程中需要B ,于是A将自己放到三级缓存里面,去实例化 B
2 B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A
3 B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态)
然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。
总结:
## 三种缓存作用
* 一级缓存:(也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象
* 二级缓存:early SingletonObjects,存放早期暴露出来的bean对象,Bean的生命周期未结束(属性还未填完)
* 三级缓存:Map<String,ObjectFactory<?>> sinletonFactories,存放可以生成Bean的工厂。
`只有单例的Bean会通过三级缓存 提前暴露来解决循环依赖问题,而并非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。`
6、Redis非关系型数据库
1、基本存储数据类型8种
- String(字符类型)
- list (列表类型)
- hash (散列类型)
- set (集合类型)
- zset(sortred set 有序集合类型)
- Bitmap (位图)
- HyperLogLog (统计)
- GEO (地理)
2、注意事项
- 命令不区分大小写,key区分大小写
3、Set命令
- 添加元素
SADD key member [member...]
- 删除元素
SREM key member [member ...]
- 获取集合中的所有元素
SMEMBERS key
- 判断元素是否在集合中
SISMEMBER key member
- 获取集合中的元素个数
SCARD key
- 从集合中随机弹出一个元素,元素不删除
SRANDMEMBER key [数字]
- 从集合中随机弹出一个元素,出一个删一个
SPOP key [数字]
4、集合运算
- 集合的差集运算A - B
属于A但不属于B的元素构成的集合 SDIFF key [key...]
- 集合的交集运算A n B
属于A同时也属于B的共同拥有的元素构成的集合 SINTER key [key...]
- 集合的并集运算A U B
属于A或者属于B的元素合并的集合 SUNION key [key...]
4、使用场景
- 微信抽奖小程序
- 朋友圈点赞功能
- 微博好友关注社交关系
- QQ内推可能认识的人
5、ZSet命令
- 向有序集合中加入一个元素和该元素的分数,添加元素
ZADD key score member [score member]
- 按照元素分数从小到大的顺序,返回索引从start到stop之间的所有元素
ZRANGE key start stop [WITHSCORES]
- 获取元素的分数
ZSCORE key member
- 删除元素 ZREM key member [member…]
- 获取指定分数范围的元素
ZRANGEBYSCORE key min max [WITHSCORE] [LIMIT offset count]
- 增加某个元素的分数
ZINCRBY key increment member
- 获取集合中元素的数量
ZCARD key
- 获得指定分数范围内的元素个数
ZCOUNT key min max
- 按照排名范围删除元素
ZREMRANGEBYRANK key start stop
- 获取元素的排名。从小到大:
ZRANK key member
从大到小:ZREVRANK key member
6、应用场景
- 根据商品销售对商品进行排序显示
- 抖音热搜
7、Redis分布式锁
实现方式
- MYSQL
- zookeeper
- Redis
常见的面试问题
- Redis除了拿来做缓存,你还见过基于Redis的什么用法?
- Redis做分布式锁的时候有需要注意的问题?
- 如果是Redis是单点部署的,会带来什么问题?
- 那你准备怎么解决单点问题呢?
- 集群模式下,比如主从模式,有没有什么问题呢?
- 那你简单的介绍一下Redlock吧?你简历上写Redisson,你淡淡?
- Redis分布式锁如何续期?看门狗知道吗?
解决方案
//设置分布式锁 Boolean flag =stringRedisTemlate.opsForValue().setIfAbsent(REDIS_LOCK,value); //设置分布式锁过期时间 stringRedisTemlate.expire(REDIS_LOCK,10L,TimeUnit.SECONDS); //以上这种方案存在 原子性问题 //解决方案如下 Boolean flag =stringRedisTemlate.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS); //虽然原子性问题解决了,但是存在超时删除问题,而导致误删锁问题 //解决方案 采用判断 if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){ stringRedisTemplate.delete(REDIS_LOCK); } //以上删除操作,还是存在原子性问题 //解决方案 官方推荐采用 Lua脚本操作, if redis.call("get",KEYS[1]==ARGV[1]) then return redis.call("del",KEYS[1]) else return 0 end //代码实现 Jedis jedis= RedisUtils.getJedis(); String script ="if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; try{ if(null!=jedis){ Object o=jedis.eval(script,Collections.singletonList(REDIS_LOCK),Collections.singletonList(value)); if("1".equals(o.toString())){ System.out.println("----del redis lock ok"); }else{ System.out.println("----del redis lock error") } }finally{ if(null!=jedis){ jedis.close(); } } } //如果不采用Lua解决 利用事务解决,以下其他解决方案 //思想就是基于乐观锁的重试机制。 while(true){ stringRedisTemplate.watch(REDIS_LOCK); if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){ stringRedisTemplate.mutil(); stringRedisTemplate.delete(REDIS_LOCK); List<Object> list=stringRedisTemplate.exec(); if(list==null){ continue; } } stringRedisTemplate.unwatch(); break; } // 超时问题,怎么添加时间(缓存延时问题) // AP 分区容错,高可用 // Redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。 // Zookeeper CP 一致性 // 解决方法 RedisLock Java实现 Redisson 落地实现 //redisConfig @Bean public Redisson redisson(){ Config config=new Config(); config.userSingleServer().setAddress("redis://ip:port").setDatabase(0); return (Redisson)Redisson.create(config); } //加锁代码 RLock redissonLock =redissson.getLock(REDIS_LOCK); redissonLock.lock(); try{ //业务逻辑代码 }finally{ //释放锁代码 redissonLock.unlock(); } //**注意细节** //解锁时出现一个异常,IllegalMonitorStateException:attempt to unlock lock,not locked by node id //在解锁的时候 加一个判断 finally{ if(redissonLock.isLocked()){ if(redissonLock.isHeldByCurrentThread()){ redissonLock.unlock(); } } }
Redis事务
事务介绍
- Redis的事物是通过
MULTI,EXEC,DISCARD和WATCH
这四个命令来完成。- Redis的单个命令都是
原子性
的,所以这里确保事务性的对象是命令集合
。- Redis将命令集合序列化并确保处于一事务的
命令集合连续且不被打断
的执行。- Redis
不支持回滚
的操作。相关命令
- MULTI
- 注:
用于标记事务块的开始
。- Redis会将后续的命令逐个放入队列中,然后使用
EXEC命令
原子化地执行这个命令序列。- 语法:
MULTI
- EXEC
- 在一个
事务中执行所有先前放入队列的命令
,然后恢复正常的连接状态。- DISCARD
- 清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
- 语法:
DISCARD
- WATCH
- 当某个
事务需要按条件执行
时 ,就要使用这个命令将给定的键设置为受监控
的状态
。- 语法:
WATCH [key ...]
- 注:该命令可以实现Redis的
乐观锁
- UNWATCH
- 清除所有先前为一个事务监控的键
- 语法:
UNWATCH