【百面成神】多线程基础16问,你能坚持到第几问
前 言
🍉 作者简介:半旧518,长跑型选手,立志坚持写10年博客,专注于java后端
☕专栏简介:纯手打总结面试题,自用备用
🌰 文章简介:多线程最基础、重要的16道面试题
文章目录
- 1.线程池
- 2.创建线程有哪些方法
- 3.什么是ABA问题
- 4.什么是CAS
- 5.说一说线程的生命周期
- 6.JMM模型
- 7.线程死锁问题&线程池满的问题
- 8.锁升级的原理
- 9.ThreadLocal
- 10.volatile关键字的作用
- 11.synchornized和Lock的区别
- 12.并发和并行的区别
- 13.线程和进程的区别
- 14.start()和run()有什么区别?
- 15.如何停止一个线程?
- 16.notify和notifyAll有什么区别?
1.线程池
创建线程需要时间和资源,如果当需要使用一个线程时再创建一个线程,响应时间会变长。可以在程序启动时就创建若干线程来以备响应之需,这些线程由线程池来统一管理。
有以下参数:
核心线程数 corePoolSize
最大线程数 maxPoolSize
存活时间Keepalivetime
单位Unit
工作队列 workqueue
线程工厂 threadFactory
拒绝策略Handler
2.创建线程有哪些方法
继承Thread类
实现Runnable接口(如果要继承其它类,可以实现Runnable接口)
实现Callable接口(和FutureTask结合,有返回值、可以抛出异常)
使用线程池
3.什么是ABA问题
通过多次读取一个值,根据值是否有变化判断数据是否发生改变,但是可能出现值回退的情况。
4.什么是CAS
在引入CAS以前,java通过sychronized加锁,以避免临界资源竞争的问题。但是这是一种悲观锁,获取不到锁的线程会被挂起,上下文切换等也会造成时间损耗,还可能出现优先级较高的线程等待优先级较低的线程释放锁资源的问题。而CAS是一种乐观锁机制,他的全程是CompareAndSet,他他不会立刻加锁,CAS有三个值,内存值,预期值,新值,如果内存值=预期值,就说明临界资源没有被其它线程修改,可以将其更新为新值。
CAS能够从底层硬件级别对于cpu效率进行提升,因此其效率也很高。
但是,CAS可能有ABA问题
5.说一说线程的生命周期
线程刚刚创建时是NEW,开始运行会变成Runnable,如果被IO阻塞或者同步锁阻塞会变成Blocked,如果永久等待状态是Waiting,如果是等待被唤醒是Timed_Waiting,执行完成是Terminated
6.JMM模型
线程之间共享的变量存放在主存中,而每个线程的私有变量存放在各自的本地内存中。
7.线程死锁问题&线程池满的问题
线程A持有独占锁a,并且尝试获取独占锁b,线程B持有独占锁b,并且尝试获取独占锁a。
在工作中会先切换到问题出现的代码版本分支,使用jstack命令做线程dump,并且使用threadIO排查线程死锁问题。曾经遇到一个场景,就是将Lock改为了tryLock,但是没有设置时间参数。最后设置了时间参数解决了问题。
想要避免死锁,可以通过按顺序访问资源来实现。比如线程A,B都是先访问a,再访问b,释放锁的顺序与获取锁的顺序相反。就可以避免死锁。
如果实际可行,也可以一次性获取所有资源。
占用锁资源的线程再去申请锁资源时,如果申请不到锁资源,先释放它现有的锁资源。
还需要注意锁的粒度尽量设置的细点,尽量使用JUC提供并发类,而不要使用手写锁。
8.锁升级的原理
记录threadid,获得偏向锁;再次访问比较线程id,获得轻量级锁,进行一定次数的锁自旋,获得重量级锁。
9.ThreadLocal
每个线程都有一个自己的变量副本,彼此之间隔离,互相不干扰。使用ThreadLocal可以提供比使用sychorinized更简单的一种线程安全机制。在session管理,数据库连接等场景中会有应用。
10.volatile关键字的作用
可以保证修改的可见性,同时避免指令重排序。
11.synchornized和Lock的区别
作用范围:使用syn可以给代码块,方法和变量加锁,而使用lock只可以给代码块加锁
使用方法:使用lock需要手动释放锁
API:使用Lock可以知道是否成功获取锁,但是使用sync却无法做到
实现原理:
Lock的加锁和解锁过程是由java代码配合native方法实现的。
而synchornized是由JVM来直接管理其加锁和解锁的过程。
Synchronized的实现原理是:
先判断markword(对象头中的一个记录字段),是否为可偏向状态,如果是则获取偏向锁(这其实是为了提高单线程下的执行效率,可以理解成为没有锁,直接进入到同步代码块,并在markword中记录下这个线程id),如果由其它线程来抢占锁资源,就会根据当前状态(是否通过CAS算法竞争到锁资源)判断是否要进行锁膨胀,膨胀为轻量级锁。
轻量级锁使用CAS无锁算法。会先检查锁资源对象头的Mark word,看看当前的锁对象是否为无锁状态,如果没有就会先从栈帧中申请一个LockRecord空间。将Markword中的数据进行备份。然后通过CAS算法更新markword,将其指向LockRecord。如果更新成功,则进入同步代码块。如果没有更新成功,检查是否当前线程之前已经获取了锁还没有释放。如果已经获取也可以进入代码块,否则会进行多次锁自旋,需要膨胀为重量级锁。
在java内部每一个对象或类都有一个monitor监视器。重量级锁会通过monitor直接向操作系统申请互斥量。
虚拟机还会自动检查,如果符合条件会进行锁消除(无竞争可能)和锁粗化(循环中使用锁,会自动扩大加锁范围)。
Lock的实现原理:
Lock其实是一个接口,它有ReentrantLock等实现类。
以ReentrantLock为例。它继承了AQS,其底层最后都是调用的AQS的方法来实现的。本质上就是一个双向链表通过不断的进行CAS自旋操作来获取锁。这也是它性能好的原因:避免了线程进入内核态的阻塞状态。
12.并发和并行的区别
并发是为了cpu交替执行不同任务的能力,主要是为了避免cpu资源浪费在等待IO情形,提高cpu的利用率。
并行是在多核环境下,不同cpu执行不同任务的能力,不同核心之间互不干扰,是真正的同时执行。
常见的并发场景有:秒杀活动、股票交易系统等
高并发场景的瓶颈在于:
服务器带宽资源不够、web线程连接数不够、数据库连接上不去。
对应解决并发问题的思路有:
增加网络带宽、DNS域名解析器解析分发到多台服务器
负载均衡:如使用nginx
数据库查询优化,读写分离,分表,合理使用索引等
高并发场景下需要考虑的另一个问题是线程安全问题:
所谓线程安全就是指多线程环境下,同一代码每次执行的结果,都与单线程环境执行的结果一致,而且其它变量的结果,也与预期一致。
13.线程和进程的区别
进程就是一个独立的程序,具有独立的内存空间,比如桌面点开一个IE浏览器网页就是一个进程,点开一个QQ就是另外一个进程。
线程是进程中执行运算的最小单位,一个进程中有多个线程。线程之间既有本地内存,又有共享内存。
进程之间的通信方式有:
管道(半双工、单向流动)、信号量(是一个计数器、常常作为锁机制用来控制多个进程或者线程对共享资源的访问)、消息队列等
线程之间的通信方式有:
锁机制(互斥锁、读写锁等)
补充:可以接着说多线程引入的原因(即并发)
14.start()和run()有什么区别?
Start()内部调用了run()方法。
如果直接调用一个run()方法,不会创建一个新的线程,而是有原来的线程执行该方法。而使用start()方法一定会创建一个新的线程。
调用了start()线程是进入就绪状态,只有获取到了cpu资源才会进入开始状态,才会执行里面调用的run()方法,而不是立即执行。
15.如何停止一个线程?
Java没有提高api停止一个线程,在jdk1.0版本中,提供了suspend,resume,stop等api,但是由于潜在的死锁风险,后续版本中被弃用了。如果想要手工停止一个线程,可以设置一个使用vilotile修饰的布尔变量,在run方法中判断布尔变量的值,来决定是否继续执行run方法。
也可以是用interrupt来实现(推荐:分为有sleep和wait等相应中断的情况[try-catch异常即可]和不含上述方法[while循环增加判断条件即可]的情况)。
参考博客:Java如何停止一个线程_风在哪的博客-CSDN博客
16.notify和notifyAll有什么区别?
当一个线程调用了wait方法以后,它会进入等待池,等待池中的线程不会参与锁资源的竞争。
当调用notify方法时,会随机唤醒一个锁池中线程进入锁池。
当调用notifyAll方法时,则会唤醒所有等待池中的线程,使他们都进入到锁池。
进入到锁池的线程会竞争锁资源,优先级高的线程竞争到锁的可能性较大。
竞争到锁资源以后,线程就会执行synchorinized代码块,执行结束后释放锁,此时其它锁池中的对象会继续下一次竞争。