多线程面试题(非常重要)!!!

多线程相关面试题



一、什么是线程?线程和进程的区别?

线程是进程中的⼀个执⾏单元,负责当前进程中程序的执⾏,⼀个进程中⾄少有⼀个线程。⼀个进程中是可以有多个线程。
区别:
1、地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
2、资源拥有:同一进程内的线程共享本进程的资源,但是进程之间的资源是独立的。
3、一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
4、进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
5、执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
6、线程是处理器调度的基本单位,但是进程不是。
7、两者均可并发执行。

二、并行和串行的区别

传输方式不同:并行是分为多行、同时执行;串行是排成一行、依次执行。
效率不同:并行传输效率高,一次可传输多个数据;串行传输一次可传输一个数据。

三、描述CPU和多线程的关系

1、第一阶段,单CPU时代,单CPU在同一时间点,只能执行单一线程
2、第二阶段,单CPU多任务阶段,计算机在同一时间点,并行执行多个线程。但这并非真正意义上的同时执行,而是多个任务共享一个CPU,操作系统协调CPU在某个时间点,执行某个线程,因为CPU在线程之间切换比较快,给人的感觉,就好像多个任务在同时运行。
3、第三阶段,多CPU多任务阶段,真正实现的,在同一时间点运行多个线程。

四、什么是线程安全/线程不安全?

 线程安全:就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
 线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据,线程安全问题都是由全局变量及静态变量引起的
  1. 描述下线程的生命周期?(画图)

线程在这里插入图片描述

  • 初始状态 : 线程对象创建完成 就是new创建了一个线程对象
  • 就绪状态 : 线程可以被执行 就是调用了start()方法后的状态
  • 运行状态 : 线程正在运行中 就是就绪状态的线程获取了CPU执行权执行线程
  • 阻塞状态 : 线程休眠 就是调用了Thread.sleep()方法使线程进入休眠
  • 等待队列 : 线程陷入无限的等待中 就是线程执行wait()方法,进入这个状态后
  • 锁池状态 : 线程被唤醒,但没有获取到锁 同步锁被别的线程占用,则该线程放入“锁池”中,进入锁池状态
  • 死亡状态 : 线程执行完毕或被关闭 就是run()方法执行完毕后,线程就进入死亡状态

六、wait、sleep、join 、yield的区别

wait 方法是属于 Object 类中的,wait 过程中线程会释放对象锁,只有当其他线程调用 notify 才能唤醒此线程。wait 使用时必须先获取对象锁,即必须在 synchronized 修饰的代码块中使用,那么相应的 notify 方法同样必须在 synchronized 修饰的代码块中使用,如果没有在synchronized 修饰的代码块中使用时运行时会抛出IllegalMonitorStateException的异常

sleep 方法是属于 Thread 类中的,sleep 过程中线程不会释放锁,只会阻塞线程,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态,可中断,sleep 给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会

join等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。

yield和 sleep 一样都是 Thread 类的方法,都是暂停当前正在执行的线程对象,不会释放资源锁,和 sleep 不同的是 yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。还有一点和 sleep 不同的是 yield 方法只能使同优先级或更高优先级的线程有执行的机会

七、 Synchronized和Lock的区别

相同点:Lock 能完成 synchronized 所实现的锁功能
不同点:Lock 有比 synchronized 更精确的线程语义和更好的性能。synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放,并且必须在finally 从句中释放

八、 ThreadLocal、Volatile、Synchronized 的作用和区别

volatile变量每次被线程访问时,都强迫线程从主内存中重读该变量的最新值,而当该变量发生修改变化时,也会强迫线程将最新的值刷新回主内存中。这样一来,不同的线程都能及时的看到该变量的最新值,但是volatile不能保证变量更改的原子性。
synchronized既能保证共享变量可见性,也可以保证锁内操作的原子性;volatile只能保证可见性(所有线程都能看到共享内存的最新状态)
ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。
synchronized和volatile比较:
volatile不需要同步操作,所以效率更高,不会阻塞线程,但是适用情况比较窄
volatile读变量相当于加锁(即进入synchronized代码块),而写变量相当于解锁(退出synchronized代码块)
synchronized既能保证共享变量可见性,也可以保证锁内操作的原子性;volatile只能保证可见性

九、 同步方法和同步块,哪个更好?

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

十、什么是死锁?如何避免死锁?

死锁就是两个线程相互等待对方释放对象锁。
避免死锁:
1. 避免⼀个线程同时获取多个锁
2. 避免⼀个线程在锁内同时占⽤多个资源,尽量保证每个锁只占⽤⼀个资源
3. 尝试使⽤定时锁,使⽤ lock.tryLock(timeout) 来代替使⽤内部锁机制。
4. 对于数据库锁,加锁和解锁必须在⼀个数据库连接⾥,否则会出现解锁失败的情况。

十一、描述你常用的线程池叫什么,线程池参数有哪些

五种线程池: ExecutorService threadPool = null;
1、threadPool = Executors.newCachedThreadPool();//有缓冲的线程池,线程数 JVM 控制
2、threadPool = Executors.newFixedThreadPool(3);//固定大小的线程池
3、threadPool = Executors.newScheduledThreadPool(2); //固定大小的线程池
4、threadPool = Executors.newSingleThreadExecutor();//单线程的线程池,只有一个线程在工作
5、threadPool = new ThreadPoolExecutor();//默认线程池,可控制参数比较多
线程池的作用:
1、降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
2、提高响应速度;任务来了,直接有线程可用可执行,而不需要先创建线程,在执行。
3、提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。
- 常见参数
1. corePoolSize:核心线程数量
2. maximumPoolSize:最大线程数量
3. keepAliveTime:非核心线程闲置时的存活时间
4. BlockingQueue:阻塞队列,存储等待执行的任务;
5. ThreadFactory:线程工厂,用来创建线程
6. RejectedExecutionHandler:队列已满,而且任务量大于最大线程数量的异常处理策略。

十二、 线程创建的几种方式

1. 继承 Thread 类
2. 实现 Runnable 接口方式
3. 通过Callable和Future创建线程
4. 线程池创建

十三、 线程的应用场景

异步(进程中线程数有限,windows 2k,linux 1k)
1、web服务器本身,各种专用服务器(如游戏服务器)
2、后台任务(定时向大量用户发送邮件)、异步处理(发微博、记录日志等)、分布式计算

十四、描述你所理解的锁升级、锁降级

锁升级的顺序为:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,且锁升级的顺序是不可逆的。
线程第一次获取锁获时锁的状态为偏向锁,如果下次还是这个线程获取锁,则锁的状态不变,否则会升级为CAS轻量级锁;如果还有线程竞争获取锁,如果线程获取到了轻量级锁没啥事了,如果没获取到会自旋,自旋期间获取到了锁没啥事,超过了10次还没获取到锁,锁就升级为重量级的锁,此时如果其他线程没获取到重量级锁,就会被阻塞等待唤起,此时效率就低了。
锁降级: 重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的。

十五、描述公平锁与非公平锁的区别和优缺点

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必唤醒所有线程,会减少唤起线程的数量。
缺点:可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

十六、描述轻量级锁、重量级锁的区别

轻量级锁:自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
优点:竞争的线程不会阻塞,提高了程序的响应速度。
缺点:如果始终得不到锁竞争的线程使用自选消耗CPU
适用场景:追求响应时间,同步块执行速度高

重量级锁:其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。
优点:线程竞争不使用自旋,不会消耗CPU。
缺点:线程阻塞,响应时间缓慢
适用场景:追求吞吐量,同步块执行时间长

十七、 描述自旋锁和互斥锁的区别和使用场景

两种锁的加锁原理:
互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。
自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。

两种锁的区别:
互斥锁的起始原始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销造成影响,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。

自旋锁的使用场景:
1、线程等待锁的时间较短
2、加锁的代码被频繁访问,竞争不激烈
3、CPU资源不紧张
4、多核处理器

互斥锁的使用场景:
1、线程等待锁的时间较长
2、单核处理器
3、临界区有IO等耗时操作
4、临界区操作复杂或者有大量循环
5、临界区竞争非常激烈

两种锁的区别:
互斥锁的起始原始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销造成影响,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值