前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JUC面试点汇总

JUC面试点汇总

作者头像
秋落雨微凉
发布2022-12-07 15:31:53
2230
发布2022-12-07 15:31:53
举报

JUC面试点汇总

我们会在这里介绍我所涉及到的JUC相关的面试点内容,本篇内容持续更新

我们会介绍下述JUC的相关面试点:

  • 线程状态
  • 线程池
  • Wait和Sleep
  • Synchronized和Lock
  • Volatile线程安全
  • 悲观锁和乐观锁
  • Hashtable和ConcurrentHashMap
  • ThreadLocal

线程状态

下面我们来介绍我们面试中经常考察的两种线程状态分类

六种线程状态

Java虚拟机将线程状态划分为六种:

我们来简单介绍一下:

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后
  • 注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的可运行状态运行状态和阻塞状态
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对阻塞状态的细分
  • BLOCKED 表示被锁拦截下的阻塞
  • WAITING 表示自己进行wait等待时的阻塞
  • TIMED_WAITING 表示进行有时间限制的阻塞
  • TERMINATED 当线程代码运行结束

五种线程状态

操作系统将线程分为五种状态:

我们来简单介绍一下:

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态
    • 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

线程池

下面我们来介绍线程池中常考的一些知识点

线程池工作流程图

首先我们给出线程池的工作流程图以及相关参数:

我们对上面元素进行简单介绍:

  • submit(task):负责分配任务,将任务传入WorkQueue
  • WorkQueue:工作等待队列,用于存放未被执行的任务,通常具有任务个数限制,防止内存过满
  • 核心线程:一直处于运行状态的线程,不断从WorkQueue中取得任务并执行
  • 救济线程:只有当核心线程全部运行且WorkQueue装载满员并且有submit继续传入任务时开启,在一定时间没有任务接收后结束

线程池工作参数

我们给出线程池工作的基本参数:

代码语言:javascript
复制
/*corePoolSize核心线程数目*/

用于控制核心线程的个数
    
/*maximumPoolSize最大线程数目*/
    
表示核心线程和救急线程的最大个数,用该值减去corePoolSize核心线程数目就是救急线程个数
    
/*keepAliveTime生存时间*/
    
针对救急线程,当救急线程在该时间段没有接收新任务,就结束该线程
    
/*unit时间单位*/
    
配合keepAliveTime生存时间使用的时间单位
    
/*workQueue阻塞队列*/
    
用于存放处于阻塞状态的任务
    
/*threadFactory线程工厂*/
    
用于生成线程名称
    
/*handler拒绝策略*/
    
当线程均处于运行状态,workQueue满员,且有新任务进入时,handler负责处理新进入的线程
    
- AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
- CallerRunsPolicy 让调用者运行任务 
- DiscardPolicy 放弃本次任务 
- DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之 
- Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方 便定位问题 
- Netty 的实现,是创建一个新线程来执行任务 
- ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略 
- PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略

线程池构建代码

我们直接给出线程池构建代码,了解即可:

代码语言:javascript
复制
/*线程池构造:我之前有线程池专门的文章,可以深入了解*/

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

Wait和Sleep

我们来介绍一下wait和sleep的相关面试点

Wait和Sleep区别以及共同点

我们给出wait和sleep的区别以及共同点:

代码语言:javascript
复制
/*共同点*/

wait(),wait(long),sleep(long) 都会导致阻塞,将当前线程暂时放弃CPU使用权
    
/*不同点*/
    
// 方法归属类不同
wait 	属于	Object实体类
sleep 	属于	Thread线程类
    
// 醒来时机不同
wait():			只有当notify唤醒时才会醒来
wait(long): 	当时间结束,或者notify唤醒就会醒来
sleep(long):	当时间结束才会醒来
    
注意:都可以被打断唤醒!
    
// 锁性质不同
1.wait必须配合锁一同使用,且只有锁对象调用Lock.lock;sleep在任何时间段都可以使用
2.wait在lock时会解除当前锁的限制;sleep若在锁中不会解除当前锁的限制

Synchronized和Lock

我们来介绍一下Synchronized和Lock的相关面试点

Synchronized和Lock区别以及共同点

我们会从三个层面讲解两者的区别以及共同点:

代码语言:javascript
复制
/*语法层面*/

// 所属类
synchronized:关键字,属于JVM,用C++语言实现
Lock:接口,属于JDK,用Java语言实现
    
// 锁实现
synchronized:在结束同步代码块后自动释放锁
Lock:在结束代码块后,需要手动unlock释放锁
    
/*功能层面*/
    
// 相同点
均属于悲观锁,具备互斥,同步,锁重入功能
    
// 不同点
Lock提供了synchronized所不具备的功能:获得等待状态,公平锁,可打断,可超时,多条件变量
Lock提供了多场景Lock:ReentrantLock,ReentrantReadWriteLock
    
/*性能方面*/

// 性能差距
synchronized:在无竞争情况下,存在轻量级锁,偏向锁,性能较高
Lock:在竞争激烈的情况下,性能更好

相关知识点补充

我们来补充上述所讲述的部分知识点:

代码语言:javascript
复制
/*知识点补充*/

// Lock正常使用

owner:正在运行程序,存有status,表示几重锁,当锁重入时,status++;解除锁时,status--;当status==0,释放锁
blocked queue:阻塞队列
waiting queue:等待队列
    
// 公平锁和非公平锁
    
公平锁:所有任务在进入时均放于阻塞队列按顺序排序并执行
非公平锁:当owner释放锁后,新加入的任务可以和阻塞队列的任务处于同一级竞争锁
    
// 多条件变量
条件变量创建:Condition c1 = Lock.newCondition("c1");
条件变量使用:c1.await();
条件变量唤醒:c1.signal();
条件变量唤醒:c1.signalAll();

必须处于owner才能使用await,唤醒后放于Blocked Queue尾部等待

Volatile线程安全

我们来介绍一下Volatile的线程安全相关问题

Volatile线程安全

我们首先要知道线程安全主要从三方面解释:

代码语言:javascript
复制
/*可见性*/

当前线程对数值的修改是否对其他线程可见?
    
问题产生原因:
    CPU和内存之间还有一层缓存区,当使用数据较多时,会直接将内存的数据放入缓存区,然后直接从缓存区调入数据
    这时倘若另一个线程修改了内存中的数据,但是原线程仍旧从自己的缓存区读取数据,就会导致数据不可见
    
/*有序性*/
    
当前线程内的代码是否按照编写顺序执行?
    
问题产生原因:
    JVM存在自动编写机制,当出现同级别的代码时,JVM会自动优化代码顺序,加快速度,可能导致代码执行顺序错乱
    
/*原子性*/
    
当前线程内的代码是否为一次执行?
    
问题产生原因:
    线程是由CPU调度使用的,倘若线程调度到达指定时间片,可能就会导致线程内代码未完成执行而被其他线程使用的情况

然后我们需要知道Volatile对这三种特性的可控度:

代码语言:javascript
复制
/*可见性*/

Volatile可以保证数据的可见性

Volatile会使该属性的每次读取都默认从内存中读取(该属性不会被存放于缓冲区)
    
/*有序性*/
    
Volatile可以保证数据的有序性
    
Volatile存在写屏障和读屏障
    写屏障出现在属性输入之后,在写屏障之前的代码顺序不会变更
    读屏障出现在属性读取之前,在读屏障之后的代码顺序不会变更
    
/*原子性*/
    
Volatile无法保证数据的原子性!

悲观锁和乐观锁

我们来介绍一下悲观锁和乐观锁的面试点

悲观锁和乐观锁区别

我们来介绍一下悲观锁和乐观锁的区别:

代码语言:javascript
复制
/*悲观锁*/

代表:
    Synchronized
    Lock
    
特点:
    1.核心思想:当前线程占用锁后,才能操作共享数据,只有当当前线程结束操作后,其他线程才能竞争
    2.线程的运行与阻塞都会导致上下文切换,频繁的上下文切换会导致CPU速度降低
    3.悲观锁大部分都存在自旋现象,在获得锁时会多次尝试来减少上下文切换次数
    
/*乐观锁*/
    
代表:
    Atomic系列
    AtomicInteger
    
特点:
	1.核心思想:所有数据都可以操作共享数据,但只有一个线程可以修改数据,其他线程如果修改失败就会不断尝试
    2.由于线程一直运行,不需要阻塞,不涉及上下文切换
    3.由于线程一直运行,需要一个CPU保证其线程的活动,否则单CPU下乐观锁属于负增益,一般线程数不会超过CPU个数

悲观锁乐观锁代码比较

我们分别给出悲观锁和乐观锁的代码展示:

代码语言:javascript
复制
/*乐观锁底层实现*/

// 乐观锁底层其实是采用Unsafe类来完成的

// 1.获得该类的属性对于类的偏移量(第一个参数:类名称.class,第二个参数:类属性名称)
Long BALANCE = unsafe.objectFieldOffset(Account.class,"blance");

// 2.采用unsafe的原子比较赋值方法(第一个参数:类对象,第二个参数:属性偏移量,第三个参数:修改前数值,第四个参数:修改后数值)
// 如果再次检测时,该值和oldInt相同,就将其修改为newInt,否则不作为
unsafe.compareAndSetInt(account,BALANCE,oldInt,newInt);

/*乐观锁代码*/

Thread t1 = new Thread(() -> {
    // 乐观锁需要不断尝试
    while(true){
        // 每次获得当前值
        int oldInt = account.getBalance;
        // 我们假设做++操作
        int newInt = oldInt + 1;
        // 然后启动unsafe的比较赋值(compareAndSetInt会返回一个布尔值表示是否成功),若成功退出循环
        if(unsafe.compareAndSetInt(account,BALANCE,oldInt,newInt)){
            break;
        }
    }
}).start;

/*悲观锁代码*/

Thread t2 = new Thread(() -> {
    // 悲观锁就是直接采用锁处理即可
    synchronized(Account.class){
        int oldInt = account.getBalance;
        int newInt = oldInt + 1;
        account.setBalance(newInt);
    }
})

Hashtable和ConcurrentHashmap

我们来介绍一下Hashtable和ConcurrentHashmap的面试点

Hashtable和ConcurrentHashmap区别

我们来介绍一下Hashtable和ConcurrentHashmap区别:

代码语言:javascript
复制
/*线程安全?*/

Hashtable和ConcurrentHashMap均属于线程安全类的Map集合
    
/*并发度*/
    
Hashtable:只存在一个锁,所有索引点的操作均在一个锁上,并发度低
    
1.7ConcurrentHashMap:底层由数组+Segment+链表结构,每个Segment对应一把锁,不同Segment不会造成锁冲突
    
1.8ConcurrentHashMap:底层由数组+链表结构,每个链表头对应一把锁,相当于每个索引点对应一把锁,只有同一条链表会产生锁冲突

Hashtable

我们来介绍一下Hashtable的基本面试点:

代码语言:javascript
复制
/*基本问题*/

初始capacity:11
    
扩容:超过0.75
    
索引:hashcode即可,(因为以质数为主,分散性较好,不需要二次hash)

1.7ConcurrentHashMap

我们来介绍一下JDK1.7版本的ConcurrentHashMap:

代码语言:javascript
复制
/*基本组成*/

capacity:总共的索引头
    
factor:超过0.75
    
clevel:并发度,也就是Segment的个数
    
每个Segment算是一个大桶,然后大桶中会根据capacity/clevel算出小桶
    
举例:
    capacity:32
    factor:0.75
    clevel:8
    
    这时存在8个Segment,每个Segment中存有四个初始小桶
    
    不同Segment拥有不同锁,不同Segment独自占有并发性
    
    基本形式如下:
    ---- ---- ---- ---- ---- ---- ---- ----
    
/*put操作*/
    
1.hashCode
    
2.hash
    
3.根据hash值二进制的前(clevel二进制为2的n次方)n位的数值来判断放在哪个大桶
    
4.根据hash值二进制的后(小桶大小二进制为2的n次方)n为的数值来判断放哪个大桶
    
举例:
    capacity:32
    factor:0.75
    clevel:8
    
    首先clevel的为2的3次方,小桶大小为2的2次方
    
    假设我们的hash值二进制为 110 1101 0110

    这时我们的大桶取前三位:110 -> 6 -> Segment[6]
    
    这时我们的小桶取后两位: 10 -> 2 -> Segment[6][2]
    
    也就是第七个桶的第三个位置
    
/*扩容*/
    
扩容仅针对每个Segment单独扩容,最开始的桶大小为capacity/clevel,当超过factor,就会自动扩大一倍,单独计算
    
Segment[0]的扩容不会影响到其他Segment的桶大小

/*Segment[0]*/
    
Segment[0]会自动初始化小桶,其他Segment只有在put第一个数时初始化
    
因为Segment[0]类似于一个初始模板,其他Segment会根据Segment[0]的大小来构造,节省空间(懒汉式构建)

1.8ConcurrentHashMap

我们来介绍一下JDK1.8版本的ConcurrentHashMap:

代码语言:javascript
复制
/*基本知识点*/

1.8版本的ConcurrentHashMap只包含 数组 + 链表 结构
    
属于懒汉式初始化,我们new一个ConcurrentHashMap并不会产生数组,只有开始put时才会初始化
    
capacity:16
	注意:这里的capacity并不是初始桶大小,而是我们需要插入的数的数量,系统会根据我们书写的capacity更换桶大小
    例如我们写15,系统会为我们分配一个大小为32的桶
    
factor:达到0.75

/*并发依据*/
    
1.8ConcurrentHashMap根据链表头分配不同的锁,也就是如果不是在同一索引下,均可以正常运行
    
/*扩容操作*/
    
扩容是ConcurrentHashMap的考点之一
    
ConcurrenthashMap扩容是从后往前移动数据,每次移动完成该索引点数据,就为其标记为ForwardingNode用来表示已移动
    
ConcurrentHashMap扩容不再是将原数据next更换,而是直接在新数组上创建新数据,将数据拷贝过去,防止并发操作时出现问题
    
/*扩容细节*/
    
当一个线程t1正在进行扩容,另一个线程t2参与该HashMap各项操作:
    
1.get操作:
	I.如果查询的该索引属于ForwardingNode,就去新的数组中查找
    II.如果查询的索引不属于ForwardingNode,就直接查找
    
2.put操作:
    I.如果该索引不属于ForwardingNode,就直接插入即可
    II.如果该索引正在迁移,堵塞
    III.如果该索引已经属于ForwardingNode,帮助线程t1完成扩容后,再进行修改

ThreadLocal

我们来介绍一下ThreadLocal的面试点

对ThreadLocal的理解

我们来讲解一下对对ThreadLocal的理解:

代码语言:javascript
复制
/*线程安全性*/

ThreadLocal可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题
    
ThreadLocal同时实现了线程内资源共享
    
/*ThreadLocal使用*/
    
每个线程中有一个ThreadLocalMap类型的成员变量,用于存储资源对象
    
1.创建一个ThreadLocal(该ThreadLocal实际上就是ThreadLocalMap的key)
    ThreadLocal<class?> t1 = new ThreadLocal<>();

2.可以调用set方法存储数据(ThreadLocal就是key,资源对象作为value,放入当前线程的ThreadLocalMap集合中)
	t1.set(new String("123"));

3.可以调用get方法查找数据(根据ThreadLocal为key,查找相关数据)
    t1.get();

4.可以调用remove方法删除该数据(根据ThreadLocal为key,删除相关数据)
    
注意点:
    I.当使用set时才会构造map对象
    II.不同的ThreadLocal使用不同的数据结构
    III.扩容时阈值为0.75,每次扩容一倍;存储时采用开放寻址法
    
/*key*/
    
这里的key采用的是弱引用:
    1.Thread可能需要长时间运行(如线程池的线程),如果key不再被使用,可以被JVM的GC所释放
    2.GC仅使key释放,但是value不会释放:
    	I.获得key时,会删除value
  		II.setkey时,会将附近的value删除
    	III.手动remove删除

结束语

目前关于JUC的面试点就总结到这里,该篇文章后续会持续更新~

附录

参考资料:

  1. 黑马Java八股文面试题视频教程:并发篇-01-线程状态_java中的线程状态_哔哩哔哩_bilibili
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-12-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JUC面试点汇总
    • 线程状态
      • 六种线程状态
      • 五种线程状态
    • 线程池
      • 线程池工作流程图
      • 线程池工作参数
      • 线程池构建代码
    • Wait和Sleep
      • Wait和Sleep区别以及共同点
    • Synchronized和Lock
      • Synchronized和Lock区别以及共同点
      • 相关知识点补充
    • Volatile线程安全
      • Volatile线程安全
    • 悲观锁和乐观锁
      • 悲观锁和乐观锁区别
      • 悲观锁乐观锁代码比较
    • Hashtable和ConcurrentHashmap
      • Hashtable和ConcurrentHashmap区别
      • Hashtable
      • 1.7ConcurrentHashMap
      • 1.8ConcurrentHashMap
    • ThreadLocal
      • 对ThreadLocal的理解
  • 结束语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档