面试总结-Java多线程
Java 中使用多线程的方式有哪些?
有三种: 继承Thread、实现Runnable接口以及实现Callable接口
Thread
class MyThread extends Thread {
public void run() {
// 定义线程要执行的任务
}
}
// 创建并启动线程
MyThread thread = new MyThread();
thread.start();
Runnable
class MyRunnable implements Runnable {
public void run() {
// 定义线程要执行的任务
}
}
// 创建线程对象,并启动线程
Thread thread = new Thread(new MyRunnable());
thread.start();
Callable
class MyCallable implements Callable<Integer> {
public Integer call() throws Exception {
// 定义线程要执行的任务,并返回结果
}
}
// 创建 FutureTask 对象
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
// 创建线程对象
Thread thread = new Thread(futureTask);
// 启动线程
thread.start();
// 获取线程执行结果
Integer result = futureTask.get();
说一下线程的几种状态?
新建、准备、运行、阻塞、等待、终止状态
如何实现多线程中的同步?
在多线程编程中,当多个线程访问共享资源时,可能会导致数据不一致或者不确定的结果。为了确保多线程的安全性和可靠性,需要使用同步机制来协调线程之间的访问,常见的同步机制包括:
-
synchronized 关键字:使用
synchronized
关键字可以实现对代码块或者方法的同步,确保同一时刻只有一个线程可以执行被同步的代码块或方法。synchronized
关键字可以修饰方法、代码块或静态方法,分别实现实例级别的同步和类级别的同步。 -
ReentrantLock 锁:
ReentrantLock
是 Java.util.concurrent 包提供的一种同步锁,它提供了与synchronized
关键字类似的功能,但更加灵活。可以使用lock()
方法获取锁,unlock()
方法释放锁,还可以使用tryLock()
方法尝试获取锁。 -
使用 volatile 关键字:
volatile
关键字可以确保线程之间的可见性,即一个线程对 volatile 变量的修改对其他线程是可见的。虽然volatile
关键字不能解决所有的线程安全问题,但在特定的场景下可以用来实现轻量级的同步。 -
使用 AtomicInteger 类:
AtomicInteger
是 Java.util.concurrent.atomic 包提供的原子类,它可以在并发环境下提供线程安全的原子操作,可以用来实现一些基本类型的原子操作,如增加、减少等。
谈谈线程死锁,如何有效的避免线程死锁?
线程死锁是指两个或多个线程互相持有对方所需的资源,并且在等待对方释放资源时陷入无限等待的状态,导致程序无法继续执行下去的情况。通常发生在并发编程中,如果不加以有效地避免和解决,会导致程序无法正常运行。下面是关于线程死锁以及如何避免的一些讨论:
典型的死锁场景: 考虑两个线程 A 和 B,它们分别持有资源 R1 和 R2,并且分别需要对方持有的资源才能继续执行。如果线程 A 持有资源 R1 并请求获取资源 R2,同时线程 B 持有资源 R2 并请求获取资源 R1,那么它们可能会发生死锁,因为它们都在等待对方释放资源,导致两个线程都无法继续执行。
避免线程死锁的方法:
以下是一些常见的避免线程死锁的方法:
- 避免嵌套锁: 尽量避免在一个锁的代码块中再获取另一个锁,以免因为多层锁嵌套而导致死锁的发生。如果确实需要多个锁,可以统一获取锁的顺序,以避免不同线程获取锁的顺序不一致而导致死锁。
- 使用 tryLock() 避免死锁: 对于 ReentrantLock 锁,可以使用其提供的 tryLock() 方法来尝试获取锁,并设置超时时间,如果在超时时间内未能获取到锁,则放弃获取锁并执行相应的处理逻辑,避免死锁的发生。
- 使用线程池: 使用线程池可以减少线程的创建和销毁开销,并且可以统一管理线程的生命周期和资源。通过合理配置线程池的大小和线程等待队列,可以有效地避免死锁的发生。
- 避免循环等待: 在设计程序时,尽量避免循环等待的情况发生。可以通过定义资源的获取顺序,或者使用资源层次结构来避免不同线程之间出现循环等待的情况。
- 定期检查和恢复: 可以定期检查程序中是否存在潜在的死锁情况,并且设计相应的机制来自动恢复死锁。例如,可以通过设置超时时间来获取锁,或者使用死锁检测算法来检测和解除死锁。
请谈谈 Thread 中 run() 与 start()的区别?
run()
方法是定义线程任务代码的地方,而 start()
方法是启动线程的方法。直接调用 run()
方法会在当前线程中同步执行任务代码,而调用 start()
方法会启动一个新的线程并异步执行任务代码。
synchronized和volatile关键字的区别?
synchronized
关键字可以用于修饰方法、代码块或者静态方法,用于实现对临界资源的访问控制,保证同一时刻只有一个线程可以访问临界资源。synchronized
适用于多个线程之间需要共享临界资源并且需要互斥访问的场景。volatile
关键字通常用于修饰变量,用于保证变量的可见性,即当一个线程修改了该变量的值后,其他线程可以立即看到最新的值,而不会使用过期的缓存值。volatile
适用于多个线程之间需要共享变量,并且只有一个线程修改变量值,其他线程只是读取变量值的场景。
什么是线程池?如何创建一个线程池?
线程池是一种用于管理和复用线程的机制,它可以有效地管理线程的生命周期、控制并发度、减少线程创建和销毁的开销,提高系统的性能和稳定性。线程池包含一组预先创建的线程,这些线程可以被重复利用来执行多个任务。
要创建一个线程池,可以通过 Java.util.concurrent 包中的 ExecutorService
接口及其实现类来实现。以下是创建线程池的一般步骤:
- 选择合适的线程池实现类: Java 提供了多种线程池实现类,如
ThreadPoolExecutor
、ScheduledThreadPoolExecutor
等,根据需求选择合适的实现类。 - 创建线程池对象: 使用
Executors
工厂类提供的静态方法创建线程池对象,或者直接使用线程池实现类的构造方法创建线程池对象。 - 配置线程池参数: 根据具体的需求配置线程池的参数,如核心线程数、最大线程数、线程空闲时间、任务队列类型等。
- 提交任务: 使用线程池对象的
execute()
或submit()
方法提交任务,线程池会根据配置的参数来执行任务。 - 关闭线程池: 在不再需要使用线程池时,调用线程池对象的
shutdown()
或shutdownNow()
方法来关闭线程池,释放资源。