高级会员
- 威望
- 371
- 贡献
- 478
- 热心值
- 0
- 金币
- 19
- 注册时间
- 2020-3-29
|
1. 线程池如果任务数超过的核心线程数,会发生什么?
当任务数超过核心线程数时,会把到达的任务放入缓冲队列当中,如果队列满了,会判断线程池中当前线程数是否大于最大线程数,如果小于,则创建一个新的线程来执行任务,如果大于,则执行饱和策略。
2. 线程池的拒绝策略有哪些,分别对应哪些场景?
• AbortPolicy中止策略:丢弃任务并抛出RejectedExecutionException异常。
○ 无特殊场景
• DiscardPolicy丢弃策略:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
○ 无关紧要的任务(博客阅读量)
• DiscardOldestPolicy弃老策略:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
○ 发布消息
• CallerRunsPolicy调用者运行策略:由调用线程处理该任务。
○ 不允许失败场景(对性能要求不高、并发量较小)
3. 如果自己设计一个线程池要考虑哪些问题?
• 初始创建多少线程?
• 没有可用线程了怎么办?
• 缓冲数组需要设计多长?
• 缓冲数组满了怎么办?
• 任务队列多长才好?
• 队列满了之后怎么办?应该采取什么策略?
• 线程池初始化,初始化多少线程才合适?
4. 高并发情况下如何使用线程池?
• 选择合适的阻塞队列
• 设置合理的线程数大小
○ CPU密集型:尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销
○ IO密集型:可以使用稍大的线程池,一般为2*CPU核心数+1。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间
○ 混合型:可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理
5. 1000个多并发线程,10台机器,每台机器4核的,设计线程池大小?
• 10 个机器,1000 个请求并发,平均每个服务承担 100 个请求。服务器是 4 核的配置。
• 那么如果是 CPU 密集型的任务,我们应该尽量的减少上下文切换,所以核心线程数可以设置为 5,队列的长度可以设置为 100,最大线程数保持和核心线程数一致。
• 如果是 IO 密集型的任务,我们可以适当的多分配一点核心线程数,更好的利用 CPU,所以核心线程数可以设置为 8,队列长度还是 100,最大线程池设置为 10。
• 当然,上面都是理论上的值。
• 我们也可以从核心线程数等于 5 开始进行系统压测,通过压测结果的对比,从而确定最合适的设置。
• 同时,我觉得线程池的参数应该是随着系统流量的变化而变化的。
• 所以,对于核心服务中的线程池,我们应该是通过线程池监控,做到提前预警。同时可以通过手段对线程池响应参数,比如核心线程数、队列长度进行动态修改。
6. 谈谈synchronized和ReentrantLock的区别?
• 两者都是可重入锁
• synchronized依赖于JVM,而ReentrantLock依赖于API
• synchronized使用更加简洁,而ReentrantLock锁的粒度更细
7. synchronized的原理
• 对同步代码块进行反编译,我们可以发现,我们可以看到monitorenter和monitorexit两个指令
○ monitorenter指令会去尝试获取monitor的所有权,如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
○ 如果该线程已经占用了monitor,只是重新进入,则monitor的进入数加1
○ 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
○ 执行指令monitorexit时,monitor的进入数减1,如果减1后进入数为0,那线程退出mointor
○ monitorexit指令出现了两次,第一次为同步正常退出释放锁,第二次为异常结束时被执行的释放monitor指令
• 对同步方法进行反编译,常量池中多出了一个ACC_SYNCHRONIZED标识符,JVM就是根据该标识符来实现方法的同步的。
○ 当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将会先获取monitor,获取成功之后才能执行方法体,方法执行完成之后,再释放monitor。
8. ReentrantLock的原理
• ReentrantLock主要利用CAS+AQS队列来实现。
• ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。
○ 非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
○ 公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
9. synchronized用在代码块、方法、静态方法时锁的都是什么?
• 当作用在代码块时,锁的就是括号里的对象实例
• 当作用在实例方法时,锁的就是当前对象实例this
• 当作用在静态方法时,锁的就是对象的Class实例
10. 什么是可重入锁?
当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的
11. synchronized的自旋锁、偏向锁、轻量级锁、重量级锁,分别介绍和联系?
• 自旋锁:自旋锁就是指当一个线程尝试获取某个锁时,如果该锁已经被其他线程占用,就一直循环检测锁是否释放,而不是进入线程挂起或睡眠状态
○ 如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源
○ JDK1.6引入了适应性自旋锁,自旋的次数由前一次的自旋成功与否决定,上一次成功了,那么下次的自旋次数会更多,反之会减少
○ 总结:自旋锁是为了降低线程切换的成本
• 偏向锁:当无竞争且只有一个线程使用锁的情况,使用的就是偏向锁,偏向锁只有初始化的时候需要一次CAS,而轻量级锁每次申请、释放锁都至少需要一次CAS,当出现锁竞争的时候,偏向锁会升级为轻量级锁
• 轻量级锁:无实际竞争,多个线程交替使用锁,且允许短时间的锁竞争,当出现激烈的锁竞争时,轻量级锁会升级为重量级锁
• 重量级锁:synchronized是通过对象内部的监视器锁monitor来实现的,它的本质是依赖于底层操作系统的互斥量(mutex),而操作系统实现线程之间的切换需要从用户态转换到核心态,状态之间的转换需要比较长的时间,所以效率低,所以我们称这种依赖于操作系统互斥量来实现的锁为重量级锁
12. 说说synchronized锁升级的过程,什么时候会发生锁升级?
• 采用synchronized进行加锁,锁状态一共有4个,分别是无锁、偏向锁、轻量级锁、重量级锁
• 无锁状态:当一个对象被创建出来,如果还没有开启偏向锁机制,markword最低的3位是001,代表没有加锁
• 偏向锁状态:当偏向锁机制启动(默认程序启动后过4s),新建的对象就会加上匿名偏向锁,锁信息记录的值是101。此时代表可偏向但是没有偏向具体的线程。
• 轻量级锁(自旋锁)状态:当有2个线程同时竞争一个锁时,jvm自动将偏向锁升级为轻量级锁,轻量级锁的好处在于不需要向内核申请锁,在用户态就可以完成;弊端在于,每个线程需要不断循环获取锁,会消耗CPU。
• 重量级锁:当线程的竞争比较激烈(JVM自动判断),就会将轻量级锁升级为重量级锁,此时会向内核申请一个锁,这个锁有一个等待队列,没有获取到锁的线程会加入等待队列,从而不需要消耗CPU,重量级锁的好处在于,高并发情况下,减少了线程自旋带来的cpu消耗,弊端在于需要向内核申请,带来了性能消耗
13. volatile关键字,他是如何保证可见性,有序性?
• 可见性:有volatile修饰的共享变量进行写操作的时候多出一条带lock前缀的指令,lock指令会做两件事
○ 将当前处理器缓存行的数据写回到系统内存
○ 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
• 有序性:有volatile修饰的变量,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
14. 谈谈Java内存模型?
Java内存模型与物理机的内存模型类似,主要是线程、主内存、工作内存三者之间的交互。共享数据存储在主内存,线程操作共享数据的时候,会在工作内存中复制一个副本,然后再工作内存中操作该副本,最后再回写回主内存。
15. 谈谈 ThreadLocal的实际应用场景?
• 线程间数据隔离,各线程的 ThreadLocal 互不影响
• 方便同一个线程使用某一对象,避免不必要的参数传递
• 全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
• Spring 事务管理器采用了 ThreadLocal
• Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
16. 谈谈ThreadLocal的原理?
• 每个Thread维护着⼀个ThreadLocalMap的引⽤
• ThreadLocalMap是ThreadLocal的内部类,⽤Entry来进⾏存储
• 调⽤ThreadLocal的set()⽅法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,只是传递进来的对象
• 调⽤ThreadLocal的get()⽅法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
• ThreadLocal本身并不存储值,它只是作为⼀个key来让线程从ThreadLocalMap获取value。所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响
17. Java有哪些锁种类?
• 公平锁/非公平锁
○ 公平锁是指多个线程按照申请锁的顺序来获取锁
○ 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁可重入锁
• 可重入锁:可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
• 独享锁/共享锁
○ 独享锁是指该锁一次只能被一个线程所持有
○ 共享锁是指该锁可被多个线程所持有
• 乐观锁/悲观锁:乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度
○ 悲观锁认为,不加锁的并发操作一定会出问题,悲观锁适合写操作非常多的场景
○ 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升,常常采用的是CAS算法,典型的例子就是原子类
• 偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的
○ 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价
○ 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
○ 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低
• 自旋锁:在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
18. CAS的原理,变量要用哪个关键字修饰?
• CAS是乐观锁的典型实现机制,CAS其实就是一个:我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
• 变量需要用volatile修饰,来保证它的可见性与有序性
19. 什么是死锁?
死锁是指两个或两个线程自己持有锁并去争抢其他资源造成的互相等待的现象。
20. 什么时候多线程会发生死锁?
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,而该资源又被其他线程锁定,从而导致每一个线程都得等其它线程释放其锁定的资源,造成了所有线程都无法正常结束,这就形成了死锁。
21. 出现死锁如何排查?
监测死锁可以使用jdk自带的工具。首先进入cmd命令,进入jdk安装目录下的bin目录下,执行jps命令,得到运行的线程的id,再执行jstack命令,查看结果
22. 死锁产生的条件?
• 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
• 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
• 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
• 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失
23. 如何避免死锁
• 避免嵌套锁
• 控制加锁顺序
• 设置加锁时限:在尝试获取锁的过程中若超过了这个时限该线程
|
|