1. 基础概念
1.1. 什么是线程?线程和进程的区别是什么?
线程:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中
进程:整个程序是一个进程,一个进程可以包含多个线程,这些线程可以并发执行,共享进程的资源
1.2. Java中如何创建线程?有几种方式?
三种方式:
1.2.1. 继承Thread类的方式:
- 继承Thread类
- 重写run方法
- 创建线程对象
- 调用start()方法启动。
1.2.2. 通过实现Runable来创建线程
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable对象
- 把MyRunnable任务对象交给Thread线程对象处理。
- 调用线程对象的start()方法启动线程
1.2.3. 通过FutureTask来创建线程
- 创建任务对象
-
- 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
- 把Callable类型的对象封装成FutureTask(管理多线程的返回结果)。
- 把FutureTask对象交给Thread对象。
- 调用Thread对象的start方法启动线程。
- 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果
1.3. 三种创建方式对比:
1.4. 什么是守护线程(Daemon Thread)?如何设置守护线程?
守护线程:当核心线程都结束之后,守护线程也会陆续关闭。
守护线程:当非守护线程结束后,守护线程就没有继续执行下去的必要了,就会陆续关闭,不管执行完没有,比如垃圾回收、性能监控
设置守护线程:在 Java 中,可以通过Thread
类的setDaemon(true)
方法将一个线程设置为守护线
1.5. 什么是线程的生命周期?线程有哪些状态?
- 线程的生命周期:线程的生命周期指的是线程从创建到销毁的整个过程,期间会经历不同的状态变化。
- 线程的六种状态:
1.6. start()方法和run()方法的区别是什么?
start()
方法用于启动一个新的线程,它会在新的线程中调用run()
方法,实现多线程并发执行
run()
方法只是一个普通的方法,直接调用run()
方法并不会启动新的线程,而是在当前线程中顺序执行run()
方法中的代码
2. 线程同步与锁
2.1. 什么是线程安全?如何实现线程安全?
线程安全就是在多线程环境下,程序的执行结果和单线程环境下的执行结果一致,不会出现数据不一致或其他异常情况。
1.加锁:如synchronized
关键字,保证同一时刻只有一个线程可以访问共享资源
2.使用不可变对象,例如String类
2.2. synchronized
关键字的作用是什么?如何使用?
- 作用:
synchronized
关键字用于实现线程同步,它可以保证在同一时刻只有一个线程能够访问被synchronized
修饰的代码块或方法,从而避免多个线程同时访问共享资源导致的数据不一致问题。 - 使用:synchronized(锁对象)
2.3. synchronized
修饰静态方法和实例方法的区别是什么?
锁的对象不同:修饰实例方法时,锁的对象是当前实例对象
修饰静态方法时,锁的对象是当前类的Class
对象
2.4. 什么是死锁?如何避免死锁?
死锁:死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去
破坏互斥条件:无法被破坏,因为锁就是通过互斥来解决线程安全问题
破坏请求与保持条件:一次申请所有需要的资源
破坏不可剥夺条件:如果线程已占用部分资源,且他还要申请其他资源,如果申请不到,就主动释放它自己占有的资源
破坏循环等待条件:按照顺序申请资源,释放资源则逆序
2.5. 什么是volatile
关键字?它的作用是什么?
- volatile 关键字:
volatile
用于修饰变量,保证变量的可见性和禁止指令重排序。
保证可见性:当一个变量被声明为volatile
时,它会保证对该变量的写操作会立即刷新到主内存中,而读操作会从主内存中读取最新的值。这样可以确保多个线程对该变量的操作是可见的,避免出现数据不一致的问题。
禁止指令重排序:编译器和处理器为了提高性能,可能会对指令进行重排序。volatile
关键字可以禁止这种重排序,保证代码的执行顺序和编写顺序一致。
2.6. volatile
和synchronized
的区别是什么?
每个线程都会有自己的工作空间,每个线程只能访问各自的工作空间,而一些共享变量会被加载到每个线程的工作空间中。
- 作用范围不同:
-
volatile
只能修饰变量。synchronized
关键字可以修饰方法和代码块
- 同步机制不同:
-
volatile
是一种轻量级的同步机制,它不会阻塞线程synchronized
是一种重量级的同步机制,它会阻塞线程
- 使用场景不同:
-
volatile
适用于一个变量被多个线程读取,而只有一个线程写入的场景,例如状态标志位。synchronized
适用于多个线程对共享资源进行读写操作的场景,需要保证数据的一致性。
2.7. 什么是ReentrantLock
?它与synchronized
的区别是什么?
ReentrantLock
是一个可重入锁,它提供了与synchronized
关键字类似的同步功能,但更加灵活。
synchronized
是隐式锁:当进入同步代码块或方法时自动获取锁,退出时自动释放锁。
ReentrantLock
是显式锁:需要手动调用lock()
方法获取锁,调用unlock()
方法释放锁,通常需要在finally
块中释放锁,以确保锁一定会被释放。
ReentrantLock
提供了更多的灵活性,例如可以实现公平锁(ReentrantLock(true)
),可以尝试获取锁
2.8. 什么是wait()
、notify()
和notifyAll()
方法?它们的作用是什么?
wait () 方法:用于使当前线程进入等待状态,当前线程会释放对象的锁。
notify () 方法:用于唤醒在此对象监视器上等待的单个线程。如果有多个线程在等待,则随机选择一个线程唤醒。
notifyAll () 方法:notifyAll()
方法用于唤醒在此对象监视器上等待的所有线程。
2.9. 为什么wait()
和notify()
方法必须在synchronized
块中调用?
因为wait()
方法在调用时会释放对象的锁,而notify()
和notifyAll()
方法在调用时需要获取对象的锁。如果不在synchronized
块中调用,就无法保证线程安全,可能会出现竞态条件
2.10. 什么是线程间的通信?如何实现?
线程间的通信:线程间的通信是指多个线程之间通过某种机制进行信息交换和协调
使用wait()
、notify()
和notifyAll()
方法
3. 3. 线程池
3.1. 什么是线程池?为什么要使用线程池?
一个装线程的池子,池子中的线程可以复用, 当任务执行完,线程不会销毁,而是归还给池子中。
1、可以降低资源的消耗
2、提高响应速度
3、便于线程管理
3.2. Java中如何创建线程池?有哪些常见的线程池实现?
创建方式:
在 Java 中,可以使用 Executors
工具类或者直接使用 ThreadPoolExecutor
类来创建线程池。
常见线程池实现:
- FixedThreadPool:固定大小的线程池,线程数量固定。
- CachedThreadPool:可缓存的线程池,线程数量不固定,会根据任务数量动态创建和销毁线程。当有新任务提交时,如果线程池中有空闲线程,则执行任务;如果没有空闲线程,则创建新线程执行任务。当线程空闲时间超过一定时间(默认 60 秒),会被销毁。
- SingleThreadExecutor:单线程的线程池,线程池中只有一个线程,所有任务按照顺序依次执行。
- ScheduledThreadPool:定时任务线程池
3.3. Executors
类提供了哪些创建线程池的方法?
newFixedThreadPool(int nThreads)
:创建一个固定大小的线程池newCachedThreadPool()
:创建一个可缓存的线程池。newSingleThreadExecutor()
:创建一个单线程的线程池。newScheduledThreadPool(int corePoolSize)
:创建一个定时任务线程池
3.4. ThreadPoolExecutor
的核心参数有哪些?它们的作用是什么?
6大核心参数:
核心线程数量:线程池会一直保持这些线程的存活,即使它们处于空闲状态
最大线程的个数:当任务数量超过核心线程数量且队列已满时,线程池会创建新线程,直到线程数量达到最大线程数量。
临时线程存活时间:当线程空闲时间超过该时间时,会被销毁
存活时间的单位:
任务队列:用于存储等待执行的任务
线程工厂:用于创建线程
拒绝策略:当任务队列已满且线程数量达到最大线程数量时,新提交的任务会触发拒绝策略。
3.5. 什么是线程池的饱和策略(拒绝策略)?有哪些常见的拒绝策略?
是指当任务队列已满且线程数量达到最大线程数量时,新提交的任务的处理方式
常见拒绝策略:
AbortPolicy
:默认的拒绝策略,直接抛出RejectedExecutionException
异常,阻止系统正常运行。CallerRunsPolicy
:由调用线程(提交任务的线程)来执行该任务。DiscardPolicy
:直接丢弃新提交的任务,不做任何处理。DiscardOldestPolicy
:丢弃任务队列中最老的任务,然后尝试重新提交新任务。
3.6. 如何关闭线程池?shutdown()
和shutdownNow()
的区别是什么?
关闭线程池的方法
可以使用 shutdown() 或 shutdownNow() 方法关闭线程池。
区别:
shutdown()
:该方法会平滑地关闭线程池,不再接受新任务,但会继续执行队列中已有的任务,直到所有任务执行完毕。调用该方法后,线程池的状态会变为SHUTDOWN
。shutdownNow()
:该方法会立即关闭线程池,尝试停止所有正在执行的任务,并返回队列中未执行的任务列表。调用该方法后,线程池的状态会变为STOP
。
3.7. 如何监控线程池的状态?
可以通过 ThreadPoolExecutor
提供的一些方法来监控线程池的状态:
getTaskCount()
:返回线程池已接收到的任务总数。getCompletedTaskCount()
:返回线程池已完成的任务数量。getActiveCount()
:返回线程池中正在执行任务的线程数量。getPoolSize()
:返回线程池当前的线程数量。getQueue().size()
:返回任务队列中等待执行的任务数量。