多线程面试题整理(二)

164 阅读6分钟

「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战」。

1.ThreadLocal、Volatile、Synchronized 的作用和区别

  1. ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。

  2. volatile用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最终值。volatile很容易被误用,用来进行原子性操作。

  3. Synchronized 是Java利用锁的机制自动实现的,一般有同步方法和同步代码块两种使用方式。关键字保证了数据读写一致和可见性等问题。

ThreadLocal与Synchronized区别: ThreadLocal和Synchonized都用于解决多线程并发访问,他们两者的区别: 一句话来说,Synchronized是为了让多线程进行数据共享,而ThreadLocal为了让多线程进行数据隔离。

(synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问,而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享,而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。)

2.同步方法和同步块,哪个更好?

同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。

3.什么是死锁?如何避免死锁?

死锁:使用同步的多个线程同时持有对方运行时所需要的资源,就会造成线程的死锁,多线程同步时, 多个同步代码块嵌套,很容易就会出现死锁,

如何避免线程死锁:

我们只要破坏产生死锁的四个条件中的其中一个就可以了。 破坏互斥条件:这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

破坏请求与保持条件:一次性申请所有的资源。

破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。

4.描述你所理解的锁升级、锁降级

  锁升级的顺序为:   无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,且锁升级的顺序是不可逆的。

  线程第一次获取锁获时锁的状态为偏向锁,如果下次还是这个线程获取锁,则锁的状态不变,否则会升级为CAS轻量级锁;如果还有线程竞争获取锁,如果线程获取到了轻量级锁没啥事了,如果没获取到会自旋,自旋期间获取到了锁没啥事,超过了10次还没获取到锁,锁就升级为重量级的锁,此时如果其他线程没获取到重量级锁,就会被阻塞等待唤起,此时效率就低了。

  锁降级: 重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的。

5.描述公平锁与非公平锁的区别和优缺点

公平锁:线程按照申请锁的顺序来获取锁;在并发环境中,每个线程都会被加到等待队列中,按照FIFO的顺序获取锁。

非公平锁:线程不按照申请锁的顺序来获取锁;一上来就尝试占有锁,如果占有失败,则按照公平锁的方式等待。

  • 公平锁

优:线程按照顺序获取锁,不会出现饿死现象(注:饿死现象是指一个线程的CPU执行时间都被其他线程占用,导致得不到CPU执行)。

缺:整体吞吐效率相对非公平锁要低,等待队列中除一个线程以外的所有线程都会阻塞,CPU唤醒线程的开销比非公平锁要大。

  • 非公平锁

优:可以减少唤起线程上下文切换的消耗,整体吞吐量比公平锁高。

缺:在高并发环境下可能造成线程优先级反转和饿死现象。

6.描述轻量级锁、重量级锁的区别

经量级锁: 竞争的线程不会阻塞,提高了程序的响应速度。如果始终得不到锁竞争的线程使用自旋会消耗CPU。追求响应时间。同步块执行速度非常快。

重量级锁: 线程竞争不使用自旋,不会消耗CPU。线程阻塞,响应时间缓慢。追求吞吐量。同步块执行速度较长。

7.描述自旋锁和互斥锁的区别

自旋锁: 是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。自旋锁一直占用着CPU,他在未获得锁的情况下,一直运行(自旋),所以占用着CPU ,CPU效率降低。

互斥锁: 运行的多个任务可能都需要使用同一种资源。互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。

8.常见的线程池?,线程池的作用是什么?

常见线程池:

  • 缓存池

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  • 定长池

newFixedThreadPool可以创建一个定长的线程池。定长线程池最多只能同时执行一定个数的线程,这个容量在new的时候设定。

  • 定时池

newScheduledThreadPool创建一个定长线程池,支持延迟执行和周期性任务执行。后一种执行方式类似于单片机的定时器中断。

  • 单线程线程池

newSingleThreadExecutor创建一个单线程化的线程池,这个线程池当前池中的线程死后(或发生异常时),才能重新启动新的一个线程来替代原来的线程继续执行下去。也就是说按照单线程的模式,会按照线程添加的顺序,一个一个的执行这些线程的工作。

线程池作用:      

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

第三:提高线程的可管理性。