线程常见面试题
1.线程和进程有什么区别?
进程:
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存,在指令运行过程中还需要用到磁盘,网络等设备,进程就是用来加载指令,管理内存,管理IO的.
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程.
进程就可以视为程序的一个实例,大部分程序可以同时运行多个实例进程(例如记事本,画图,浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐,360安全卫士等).
线程:
一个进程之内可以分为一到多个线程.
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行.
2.并发和并行有什么区别?
并发(concurrent)是同一时间应对多件事件的能力:
单核cpu下,线程实际还是串行执行的,操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为5毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人的感觉是同时运行的,总结为一句话就是:微观串行,宏观并行,一般会将这种线程轮流使用CPU的做法称为并发,concrrent.
并行(parallel)是同一时间动手做多件事情的能力:
多核cpu下,每个核(core)都可以调度运行线程,这时候线程可以是并行的,
3.线程创建的4中方式?
方法一:直接使用Thread
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
方法二:使用Runnable配合Thread
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
方法三:FutureTask配合Thread
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
方法四:线程池
4.线程有哪些状态?
站在JavaAPI的角度:
1.新建(NEW): new Thread(); 当使用new关键字创建Thread对象,此时的线程只是一个java对象并没有和操作系统真正的线程关联;
2.可运行状态(RUNABLE): 调用start方法,当调用线程对象的start,线程对象就会和操作系统真正的线程关联,可以由CPU调度执行;
3.终结状态:run方法中代码执行完毕,当线程中的代码被cup执行完毕后,线程就处于终结状态
新建->可运行->终结是不可逆的.
4.阻塞状态:没有获取线程锁的线程,线程在执行的时候,可能多个线程抢占同一把锁。没有获取锁的线程处于阻塞状态.
当获取锁的线程释放掉锁,重新抢占锁成功->可运行状态.
5.等待状态:获取锁的线程,调用锁对象.wait方法
当其他线程调用锁对象.notify方法唤醒等待状态的线程,重新获取锁成功-->可运行状态.
6.有时限的等待:
获取锁的线程,调用锁对象.wait(long)方法,当其他线程调用锁对象.notify方法唤醒等待状态的线程,
重新获取锁成功-->可运行状态.
时间到了重新获取锁成功--->可运行状态.
调用线程sleep(long)方法:
时间到了--->可运行状态,
5.为什么要使用线程池?
1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务.
2.可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程大概需要1MB内存,线程开的越多,消耗的内存就越大,最后死机).
6.线程池的构造方法里几个参数的作用分别都是什么?
线程池就是管理线程的一个容器;在java中线程池的实现,ThreadPoolExecutor.
ThreadPoolExecutor构造方法中的7个参数.
corePoolSize核心线程数目- 池中会保留的最多线程数;
maximumPoolSize最大线程数目 - 核心线程+急救线程的最大数目;
keepAliveTime生存时间- 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放,
unit 时间单位 - 救急线程的生存时间单位(秒, 毫秒等)
workQueue -工作队列,当没有空闲核心线程时,新来的任务会加入到此队列排队,队列满会创建救急线程执行任务,当我们使用submit方法向线程池中添加一个任务;如果所有的核心线程都在执行任务;该任务就会被加入WorkQueue
threadFactory线程工厂- 可以定制线程对象的创建,例如设置线程名字,是否是守护线程等
handler拒绝策略 - 当所有线程都在繁忙,workQueue也放满时,会触发拒绝策略.
7.线程池流程
1.当我们向线程池中添加任务(注意线程池中不能添加线程,只能添加任务,线程池中的线程是由线程池自己管理的)
2.有核心线程执行任务:
如果核心线程都在执行任务,将任务加入到任务队列 , 等待核心线程执行完成任务后,从队列中获取任务执行, 如果队列已经满了, 创建急救线程.由救急线程执行任务, 救急线程执行完成任务后, 如果在keepAliveTime时间内没有任务执行,则销毁. 如果救急线程也无法创建, 触发拒绝策略.
8.线程池的拒绝策略有哪些?
抛异常java.util.concurrent.ThreadPoolExecutor.AbortPolicy
由调用者执行任务 java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy
丢弃任务 java.util.concurrent.ThreadPoolExecutor.DiscardPolicy
丢弃最早排队任务,把新加任务添加到工作队列 java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy
9.notify()和notifyAll()有什么区别?
notify():唤醒锁对象等待区随机一个线程,
notifyAll()唤醒锁对象等待区的所有线程.
10.wait()和sleep()的区别?
相同点:wait() ,wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进去WATING Time_waiting的状态.
不同点:
方法归属不同:sleep(long) 时thread的静态方法,而wait() ,wait(long) 都是Object的成员方法,每个对象都有,
醒来时机不同: 执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来, wait(long) 和wait() 还可以被notify唤醒,wait()如果不唤醒就一直等待下去,(其他线程调用锁对象notify,可以把这个锁上处于wait状态的一个线程唤醒) 它们都可以被打断唤醒,
锁特性不同:
wait方法必须基于锁对象调用,直接调用报错,而sleep没有限制.
wait方法执行后会释放锁资源,允许其他线程获得该对象锁.
sleep如果在synchronized代码块中执行,并不会释放对象锁.
11.volatile能否保证线程安全
不能:
可见性:一个线程对共享变量进行了修改,另外一个线程能否看到修改后的结果
有序性:一个线程内代码是否按照编写顺序执行
原子性:一个线程内多行代码以一个整体运行,期间不能有其他线程的代码插队
volatile能否保证线程安全:
保证可见性,有序性(单例设计模式)
12.lock锁和synchronized锁区别
语法层面:
synchronized 是关键字:底层是通过c++代码实现锁(monitor)synchronized同步代码块执行完毕会自动释放锁
lock是一个接口:用 java 语言实现,需要手动调用 unlock 方法释放锁
功能层面:
具备基本的互斥、同步、锁重入功能
Lock 提供了许多 synchronized 不具备的功能:获取阻塞状态的线程,公平锁,可超时tryLock,多条件变量,Lock 有适合不同场景的实现
13.悲观锁和乐观锁区别
悲观锁:悲观锁的核心代表是synchronized Lock,
思想:
1.只有获取锁的线程才能操作共享变量,每次只有一个线程能获取锁成功,获取锁失败的线程都要进入阻塞队列等待;
2.线程从可运行到阻塞,再从阻塞到可运行,涉及到上下文切换,如果频繁发生会影响效率
3.synchronized和Lock在获取锁的时候,如果锁已经被其他线程占用,还会重试几次
乐观锁:乐观锁的代表是AtomicInteger,使用cas保证原子性
思想:
1.每次只有一个线程能修改成功共享变量,其他修改失败的线程不需要阻塞,不断重试直到成功
2.由于所有的线程一直运行,不需要阻塞,不牵扯频繁上下文切换
3.需要多核cup的支持 cup>线程数量
14.什么是cas
compare and switch(比较并交换)
作用:可以保证对共享变量的操作是原子性
原理:操作共享变量之前,先查看该共享变量是否进行了修改,如果进行了修改,重试;如果没有进行修改;修改共享变量
15.HashTable的默认初始容量是多少,扩容因子是多少?每次扩容多少?
/**
* Constructs a new, empty hashtable with a default initial capacity (11)
* and load factor (0.75).
*/
public Hashtable() {
this(11, 0.75f);
}
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
int newCapacity = (oldCapacity << 1) + 1;
16.HashTable在计算索引的时候,为什么不进行二次hash
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
hashtable默认容量是11,是一个质数(素数);扩容后也是质数,如果是质数取模的分散性本身就比较好;
计算出来的索引分散性比较好
17.ConcurrentHashMap的initcapacity和loadFactor与HashMap的含义相同吗?
HashMap中initcapacity是数组的初始容量,loadFactor是扩容因子当HashMap中元素的数量>initcapacity * loadFactor,数组会扩容一倍
1.8 ConcurrentHashMap的initcapacity是预计在ConcurrentHashMap中存储的元素个数,数组的长度由ConcurrentHashMap来计算,结果是2的N次幂
18.ThreadLocal
谈谈您对ThreadLocal的理解:
ThreadLocal可以实现对象的线程隔离,让每一个线程各用个的资源对象,避免争用实现的线程安全
这种方案可以采用方法内的局部变量实现;
局部变量也可以保证线程安全
局部变量是无法在多个方法直接共享的
ThreadLocal可以在一个线程内进行资源共享,在一个线程的多个方法中实现共享资源对象
ThreadLocal的原理:
每一个线程都有一个成员变量,类型是ThreadLocalMap,用来存储资源对象
当调用ThreadLocal的set方法的时候,就是将ThreadLocal作为Key,将资源对象作为value存储到当前线程的ThreadLocalMap属性中
当调用ThreadLocal的get方法的时候,就是将ThreadLocal作为Key,从当前线程的ThreadLocalMap属性中获取资源对象
当调用ThreadLocal的remove方法的时候,就是将ThreadLocal作为Key,从当前线程的ThreadLocalMap属性中删除资源对象
对资源进行线程隔离的,实际上是每个线程中的ThreadLocalMap成员变量
ThreadLocalMap扩容机制:
如果频繁在某个线程中使用不同ThreadLocal添加元素
当元素个数超过容器2/3进行扩容
ThreadLocalMap的默认容量16
ThreadLocalMap索引冲突:
开放地址法
19.@Async注解失效的
1.注解@Async的方法不是public方法
2.注解@Async的返回值只能为void或Future
3.注解@Async方法使用static修饰也会失效
4.spring无法扫描到异步类,没加注解@Async或@EnableAsync注解
5.调用方与被调用方不能在同一个类
6.类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象
7.在Async方法上标注@Transactional是没用的.但在Async方法调用的方法上标注@Transcational是有效的
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)