1.什么是线程?线程与进程有什么区别?
线程是操作系统能够进行运算的最小调度单元,被包含在进程之中,是进程的实际运作单位。
进程是计算机中程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
区别:进程拥有独立的地址空间,一个进程崩溃后,在保护模式下不会影响到其他进程;线程是进程的一部分,没有独立的地址空间,一个线程死掉等于整个进程死掉。
2.什么是线程安全?如何保证线程安全?
线程安全是指一个操作即使在多线程环境中被频繁调用,也能保证正常的行为,不会出现数据不一致等问题。
保证线程安全的方法:使用同步代码块(synchronized)、锁(lock)、原子类(AtomicXXX)、使用不可变对象等。
3.简述java中的几种线程同步机制
synchronized:同步代码块或方法。
ReentrantLock:可重入锁,提供了比synchronized更灵活的操作
CountDownLatch:允许一个或多个线程等待其他线程操作完成
Seamphore:用于控制同时访问特定资源的线程数量
CyclicBarrier:允许一组线程相互等待,直到所有线程都达到某个屏障点
4.什么是死锁?如何避免死锁?
死锁是指两个或两个以上的线程在执行过程中,因竞争资源而造成的一种互相等待的现象,如果没有外力干涉,它们都将无法继续执行下去。
避免死锁:避免循环等待、避免持有锁后再申请其他锁、尝试使用锁排序等。
5.多线程中如何处理并发修改共享资源问题?
使用锁(如synchroized、ReentrantLock)来控制对共享资源的访问。
使用volatile关键字保证变量的可见性
使用原子类(如AtomicInteger)来保证原子性操作
6.什么是线程池?如何使用线程池?
线程池是一种用于管理线程的资源池,可以复用已创建的线程,降低创建和销毁线程的开销。
使用ExecutorService接口的实现类(如ThreadPoolExecutor)来创建和管理线程池。
7.简述Java中的线程优先级和调度机制
线程优先级:线程优先级范围为1到10,默认优先级为5
调度机制:基于优先级和线程饥饿的预防,Java线程调度是基于协作式和时间片的抢占式
8.什么是锁竞争?如何优化竞争?
锁竞争是指多个线程争夺同一把锁的现象
优化方案:减少锁的持有时间、使用读写锁(ReadWriteLock)、锁分离等。
9.简述Java中volatile关键字的作用和限制
作用:保证共享变量的可见性,即一个线程对共享变量的修改,对其他线程立即可见。
限制:不能保证原子性操作,只能保证可见性和有序性。
10.什么是线程本地变量(ThreadLocal)?如何使用线程本地变量?
线程本地变量是线程的局部变量,每个线程都有自己的变量副本
使用ThreadLocal<T>用来存储每个线程的局部变量
11.java多线程中常用的锁有哪些?
1.乐观锁:适用于读多写少的场景,通过版本号机制或CAS操作来避免数据冲突。
2.悲观锁:适用于写多读少的场景,始终对数据进行加锁保护,如synchronized关键字和Lock接口的实现类。
3.独占锁:也称排它锁,一次只能一个线程获取,如ReentrantLock.
4.共享锁:允许多个线程同时获取锁,如ReentrantReadWriteLock中的读锁。
5.公平锁:按照线程请求锁的顺序来分配锁,避免饥饿现象,例如ReentrantLock可以设置为公平锁。
6.非公平锁:不保证线程按取锁的顺序,可能会造成某些线程长时间无法获取锁,但通常有更高的性能,如ReentrantLock默认是非公平锁。
7.条件锁:Condition接口提供类似Object.wait()和Object.notify()的功能,允许线程在某个条件下等待或被唤醒。
在java中,锁可以通过以下方式实现:
synchronized关键字:隐式加锁,支持重入
java.util.concurrent.locks包中的接口和类:提供了Lock接口和其实现类如ReentrantLock,提供了更灵活的锁操作,如可中断的获取锁、尝试非阻塞地获取锁等。
AQS框架:用于构建自定义的锁或其他同步组件
12. 线程池中提交一个任务的流程是怎样的?
1.开始
2.提交Runnable
3.检查线程数 < corePoolSize
4.没有达到线程池的核心线程数量,则创建新线程,当前Runnable作为线程要执行的第一个任务。如果已经大于corePoolSize,则尝试将runnable加入到workQueue中
5.尝试将当前runnable加入到workQueue中,先检查workQueue是否满了,没有满就入队,等待被执行,如果队列也满了,那么检查当前线程数和线程池最大线程数(maximumPoolSize)做比较,尝试继续增加线程,如果比最大线程小,则创建新的线程,将runnable作为线程要执行的第一个任务;如果比最大线程都要大,则拒绝当前任务;
13.线程池中有几种状态?分别是如何变化的?
状态:
- RUNNING:会接收新任务并且会处理队列中的任务
- SHUTDOWN:不会接收新任务,但是会处理队列中的任务,任务处理完后会中断所有线程
- STOP:不会接收新任务并且不会处理队列中的任务,并且会直接中断所有线程
- TIDYING:所有线程都停止了之后,线程池状态就会转为TIDYING,一旦达到此状态,就会调用线程池的terminated()
- TERMINATED terminated()执行完之后会转变为TERMINATED
变化状态:
- RUNNING -> SHUTDOWN(从running状态手动调用shutdown()或者线程池对象GC)
- SUNNING -> STOP(手动调用shutdownNow())
- SHUTDOWN -> STOP(手动调用shutdown()紧接着又调用shutdownNow()触发)
- SHUTDOWN -> TIDYING(线程池所有线程都停止后自动触发)
- STOP -> TIDYING(线程池所有线程都停止后自动触发)
- TIDYING -> TERMINATED(线程池自动调用terminated());
14.线程有哪些状态?
- 新建(new):线程已经被创建但尚未启动的状态。
- 就绪(runnable):线程已经在JVM中执行,等待获取CPU时间片。
- 阻塞(blocked):线程正在等获取一个监视器锁(例如,进入synchronized块).
- 等待(waiting):线程正在等待其他线程执行特定操作(如通过Object.wait()、Thread.join())。
- 计时等待(timed_waiting):线程正在等待其他线程执行特定操作,但是设置了超时时间(如通过Thread.sleep(long millis)、 Object.wait(long timeout)、Thread.join(long millis))。
- 终止(TERMINATED):线程已经执行完成。
public class ThreadStateDemo implements Runnable {
public void run() {
// 新建状态 -> 就绪状态(当线程启动时)
System.out.println("当前线程:" + Thread.currentThread().getName() + " 运行");
// 就绪状态 -> 阻塞状态(等待获取锁)
synchronized (this) {
try {
// 阻塞状态 -> 等待状态(调用wait方法)
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 等待状态 -> 就绪状态(当其他线程调用notify或notifyAll方法)
// 就绪状态 -> 终止状态(当线程执行完成)
System.out.println("当前线程:" + Thread.currentThread().getName() + " 终止");
}
public static void main(String[] args) {
// 创建线程并启动
Thread thread = new Thread(new ThreadStateDemo());
thread.start();
// ...
}
}
15.concurrentHashMap的扩容机制
1.7版本
- 1.7版本的ConcurrentHashMap是基于Segment分段实现的
- 每个Segment相当于一个小型的hashMap
- 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
- 先生成新的数组,然后转移到新数组中
- 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
1.8版本
- 1.8版本的ConcurrentHashMap不再基于Segment实现
- 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容则该线程一起进行扩容
- 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
- ConcurrentHashMap支持多线程同时扩容
- 在扩容之前也会先生成一个新的数组
- 在转移元素时,先将原数据分组,将每组分给不同的线程来进行元素转移,每个线程负责一组或多组元素转移工作
16.ReentrantLock中tryLock()和lock()的区别
- tryLock()表示尝试加锁,加到锁返回true,没加到返回false
- lock()表示阻塞加锁,线程会阻塞直到加到锁为止,不加到锁就会一直阻塞在这一行代码,方法也没有返回值
17.Sychronized的偏向锁、轻量级锁、重量级锁
- 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次又来获取这个锁就可以直接获取到。
- 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,如果此时又第二个线程来竞争锁,偏向锁会升级为轻量级锁,轻量级锁底层是通过自旋来实现的,并不会阻塞。
- 重量级锁:如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
- 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都需要操作系统去进行的,比较耗时间,自旋锁是通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取锁,如果获取到了则表示获取到了锁,这个过程线程会一直在运行中,相对而言是没有太多操作系统资源消耗,比较轻量。
18.Sychronized和ReentrantLock的区别
- synchronized是一个关键字,ReentrantLock是一个类
- synchronized是自动加锁和释放,ReentrantLock需要手动加锁和释放
- synchronized的底层是JVM实现的,ReentrantLock是API层面的
- synchronized是非公平锁,ReentrantLock可以选择公平锁和非公平锁(默认是非公平锁)
- synchronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中的int类型的state标识来标识锁的状态
- synchronized底层有一个锁升级的过程
19.并发、并行、串行之间的区别
- 串行:一个任务执行完,才能执行下一个任务
- 并行:两个任务同时执行
- 并发:两个任务整体上看起来是同时执行,在底层,两个任务被拆分成了很多份,然后一个一个执行,站在更高的角度来看两个任务是同时在执行的
20.什么是守护线程?
线程分为用户线程和守护线程,用户线程就是普通线程,守护线程就是JVM的后台线程,比如垃圾回收线程就是一个守护线程,守护线程会在其他普通线程都停止运行之后自动关闭。我们可以通过设置Thread.setDaemon(true)来把一个线程设置为守护线程。守护线程其实就是JVM后台用来保证用户线程正常运行而执行一些内容的线程
21.AQS的理解?AQS如何实现重入锁?
- AQS是一个JAVA线程同步的框架,是JDK中很多锁工具的核心实现框架
- 在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制排队或者放行的。在不同场景下有不同的意义。
- 在可重入锁这个场景下,state就用来表示加锁的次数,0表示无锁,每 加一次锁,state就加1,释放锁state就减1,可重入锁只有当state为0时,才表示全部解锁了。
22.线程池为什么是先添加队列而不是创建最大线程?
当线程池中的核心线程都很忙时,如果继续往线程池中添加任务,那么任务会先放入队列,队列满了之后,才会新开线程。这就相当于一个公司本来有10个程序员,本来这10个程序员能正常处理各种需求,但是随着公司的发展,需求在慢慢的增加,这些需求也只会在待开发列表中,然后这10个程序员再从待办任务里面获取需求进程处理。只有待开发的列表满了,10名程序员处理不完这些任务了,就只能增加新员工(新开线程)。
23.线程之间是如何通讯的?
- 线程之间可以通过共享内存或基于网络来进行通信
- 如果是通过共享内存来通信,则需要考虑并发问题,什么时候阻塞,什么时候唤醒
- 像java中的wait()、notify()就是阻塞和唤醒
- 通过网络就比较简单了,通过网络连接将通信数据发送给对方,当然也要考虑到并发问题,处理方式就是加锁等方式