1. 多线程如何避免死锁
- 定义
死锁指的是两个或多个线程在执行期间,因争夺资源而出现互相等待的状况,致使这些线程无法继续执行。为避免死锁,可从以下方面着手:
- 按顺序加锁:多个线程对多个锁加锁时,要保证加锁顺序一致。例如线程 A 和线程 B 都需获取锁 L1 和 L2,那么都要按先获取 L1 再获取 L2 的顺序操作。示例代码如下:
java
// 定义两个锁
Object lock1 = new Object();
Object lock2 = new Object();
// 线程 A
Thread threadA = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread A acquired lock1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread A acquired lock2");
}
}
});
// 线程 B
Thread threadB = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread B acquired lock1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread B acquired lock2");
}
}
});
threadA.start();
threadB.start();
- 限时加锁:运用带有超时时间的锁获取方法,像
ReentrantLock
的tryLock(long timeout, TimeUnit unit)
方法。若在规定时间内未获取到锁,线程可放弃,避免无限期等待。示例代码如下:
java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TimeLimitedLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 执行需要加锁的操作
System.out.println("Lock acquired, doing something...");
} finally {
lock.unlock();
}
} else {
System.out.println("Failed to acquire lock within 1 second.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) {
TimeLimitedLockExample example = new TimeLimitedLockExample();
example.doSomething();
}
}
- 减少锁的持有时间:尽可能缩短线程持有锁的时长,尽快释放锁,从而降低死锁概率。例如,把无需加锁的操作置于加锁代码块之外。示例代码如下:
java
public class ReduceLockHoldingTime {
private final Object lock = new Object();
public void process() {
// 不需要加锁的操作
System.out.println("Doing some non - locked work...");
synchronized (lock) {
// 需要加锁的操作
System.out.println("Acquired lock, doing locked work...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 不需要加锁的操作
System.out.println("Doing more non - locked work...");
}
public static void main(String[] args) {
ReduceLockHoldingTime example = new ReduceLockHoldingTime();
example.process();
}
}
- 使用资源分级:对资源进行分级,线程按资源等级顺序依次获取资源,避免循环等待。
-
原理
死锁的产生需满足四个必要条件:互斥条件、请求和保持条件、不剥夺条件和循环等待条件。避免死锁就是要破坏其中一个或多个条件。按顺序加锁和资源分级破坏了循环等待条件;限时加锁破坏了不剥夺条件;减少锁的持有时间可降低死锁发生概率。
- 要点
- 加锁顺序要一致,防止循环等待。
- 合理设置锁的超时时间,避免无限期等待。
- 尽量缩短锁的持有时间,提升并发性能。
- 合理对资源分级,避免循环依赖。
- 应用
可深入了解死锁的检测和恢复机制,如使用 jstack
工具检测死锁,以及在检测到死锁后如何进行恢复操作。还可研究分布式系统中的死锁问题及解决办法。
2. 多线程的好处以及问题
-
好处
- 提高性能:能充分利用多核处理器资源,并行执行多个任务,提升程序整体性能。例如在数据处理任务中,可将数据分成多部分,每个线程处理一部分,最后合并结果。
- 提高响应性:在 GUI 程序里,使用多线程可避免主线程长时间阻塞,保证界面响应性。比如文件下载的 GUI 程序,将下载任务放于子线程执行,主线程可继续响应用户其他操作。
- 资源共享:多个线程可共享进程资源,减少资源开销。例如多个线程可共享同一个数据库连接池。
-
问题
- 死锁:如前文所述,多个线程因争夺资源而互相等待。
- 竞态条件:多个线程同时访问和修改共享资源,会导致数据不一致。例如多个线程同时对一个计数器进行自增操作,可能使计数器值不准确。
- 上下文切换开销:线程切换需保存和恢复上下文信息,会带来一定开销。线程数量过多时,上下文切换开销会显著增加,影响程序性能。
- 原理
多线程借助操作系统的多任务处理能力,把一个程序分解为多个可并行执行的线程,以此提高程序执行效率。但多个线程同时访问共享资源时,若没有正确的同步机制,就会出现数据不一致问题。线程切换需操作系统调度,会消耗一定时间和资源。
- 要点
- 好处有提高性能、响应性和资源共享。
- 问题包括死锁、竞态条件和上下文切换开销。
- 应用
可了解多线程的同步机制,如 synchronized
关键字、Lock
接口等,以及如何运用这些机制解决多线程问题。还可研究线程池的使用,更好地管理线程资源。
3. 多线程共用一个数据变量需要注意什么?
- 线程安全:多个线程同时访问和修改共享数据变量时,要保证数据的一致性和完整性。可使用同步机制,如
synchronized
关键字、Lock
接口、Atomic
类等。示例代码如下:
java
import java.util.concurrent.atomic.AtomicInteger;
// 使用 AtomicInteger 保证线程安全
public class SharedVariableExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void increment() {
counter.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Counter value: " + counter.get());
}
}
- 可见性:一个线程对共享数据变量的修改,要确保其他线程能及时看到。可使用
volatile
关键字保证变量的可见性。示例代码如下:
java
public class VolatileExample {
private static volatile boolean flag = false;
public static void main(String[] args) {
Thread writer = new Thread(() -> {
flag = true;
System.out.println("Flag is set to true");
});
Thread reader = new Thread(() -> {
while (!flag) {
// 等待 flag 变为 true
}
System.out.println("Flag is now true");
});
reader.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
writer.start();
}
}
- 避免死锁:使用同步机制时,要留意避免死锁发生。
- 原理
多个线程同时访问和修改共享数据变量时,若没有正确的同步机制,就会出现数据不一致问题。synchronized
关键字和 Lock
接口能保证同一时间只有一个线程可访问共享数据变量,从而保证数据一致性。volatile
关键字可保证变量的可见性,即一个线程对变量的修改会立即刷新到主内存中,其他线程能立即看到。
- 要点
- 保证线程安全,使用同步机制。
- 保证变量的可见性,使用
volatile
关键字。 - 避免死锁的发生。
- 应用
可深入了解并发容器,如 ConcurrentHashMap
、CopyOnWriteArrayList
等,这些容器在多线程环境下可安全使用,无需额外的同步机制。
4. 什么是线程池
- 定义
线程池是一种线程管理机制,它预先创建一定数量的线程,当有任务提交时,从线程池中获取一个空闲线程来执行任务。任务执行完毕后,线程不会销毁,而是返回线程池等待下一个任务。
- 原理
线程池的核心是一个线程集合和一个任务队列。线程池初始化时会创建一定数量的线程,这些线程会不断从任务队列中获取任务并执行。当任务队列中有任务时,线程会立即执行;当任务队列中没有任务时,线程会进入等待状态。
- 要点
- 预先创建线程,避免频繁创建和销毁线程的开销。
- 使用任务队列管理任务,提高任务处理效率。
- 可控制线程数量,避免系统资源过度消耗。
- 应用
可了解 ThreadPoolExecutor
类的使用,它是 Java 中线程池的核心实现类,通过配置不同参数可实现不同类型的线程池。示例代码如下:
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is being executed by " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed.");
});
}
// 关闭线程池
executor.shutdown();
}
}
5. 什么是 Spring,有哪些优点
- 定义
Spring 是一个轻量级的 Java 开发框架,提供了 IoC(控制反转)和 AOP(面向切面编程)等功能,有助于开发者更高效地构建企业级应用。
- 优点
- IoC(控制反转):将对象的创建和依赖关系管理交给 Spring 容器,降低代码耦合度。例如,一个类需要依赖另一个类的实例,通过 Spring 的 IoC 容器,可将依赖的实例注入到该类中,无需在类内部手动创建依赖对象。
- AOP(面向切面编程):能在不修改原有代码的情况下对程序进行增强,如日志记录、事务管理等。例如,在方法执行前后添加日志记录代码,或在方法执行出现异常时进行事务回滚。
- 事务管理:提供统一的事务管理机制,简化事务处理代码。可通过注解或配置文件声明事务属性,Spring 会自动管理事务的开始、提交和回滚。
- 集成性好:可与其他框架如 Hibernate、MyBatis 等无缝集成。例如,在 Spring 项目中能方便地使用 Hibernate 进行数据库操作。
- 轻量级:Spring 框架的核心非常轻量级,对系统资源消耗较小。
- 原理
IoC 通过依赖注入(DI)实现,Spring 容器负责创建和管理对象,并将对象间的依赖关系注入到对象中。AOP 通过代理模式实现,Spring 会在目标对象的方法执行前后插入额外代码,实现对目标对象的增强。
- 要点
- IoC 降低代码耦合度。
- AOP 实现程序增强。
- 统一的事务管理机制。
- 良好的集成性。
- 轻量级。
- 应用
可深入了解 Spring 的各种模块,如 Spring MVC、Spring Boot 等,以及它们的使用场景和优势。例如,Spring Boot 可帮助开发者快速搭建 Spring 应用,减少配置工作。
6. ApplicationContext 和 beanfactory 的区别
- 加载时机:
BeanFactory
在需要获取 Bean 时才加载和实例化 Bean;而ApplicationContext
在容器启动时就会加载和实例化所有的单例 Bean。 - 功能特性:
ApplicationContext
除具备BeanFactory
的基本功能外,还提供更多企业级功能,如国际化支持、事件发布、资源加载等。例如,使用ApplicationContext
可方便地实现多语言支持,通过发布和监听事件实现组件间的通信。 - 使用场景:
BeanFactory
适用于资源有限、对内存和性能要求较高的环境;ApplicationContext
适用于大多数企业级应用开发,提供更丰富的功能和更好的开发体验。
- 原理
BeanFactory
是 Spring 框架的基础接口,定义了 Bean 的基本操作方法。ApplicationContext
是 BeanFactory
的子接口,继承其功能并进行了扩展。
- 要点
- 加载时机不同,
ApplicationContext
提前加载单例 Bean。 ApplicationContext
功能更丰富。- 根据不同使用场景选择合适的容器。
- 应用
可了解 ApplicationContext
的不同实现类,如 ClassPathXmlApplicationContext
、FileSystemXmlApplicationContext
等,以及它们的使用方法。
7. Spring Bean 生命周期
Spring Bean 的生命周期包含以下几个阶段:
- 实例化:Spring 容器依据 Bean 的定义创建 Bean 的实例,可通过构造函数、工厂方法等方式进行。
- 属性赋值:Spring 容器将 Bean 的属性值注入到 Bean 实例中,可通过 XML 配置、注解等方式实现。
- 初始化:调用 Bean 的初始化方法,如
InitializingBean
接口的afterPropertiesSet()
方法或自定义的初始化方法。示例代码如下:
java
import org.springframework.beans.factory.InitializingBean;
public class MyBean implements InitializingBean {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("Initializing MyBean with name: " + name);
}
}
- 使用:Bean 可供应用程序使用。
- 销毁:当 Spring 容器关闭时,调用 Bean 的销毁方法,如
DisposableBean
接口的destroy()
方法或自定义的销毁方法。示例代码如下:
java
import org.springframework.beans.factory.DisposableBean;
public class MyDisposableBean implements DisposableBean {
@Override
public void destroy() throws Exception {
System.out.println("Destroying MyDisposableBean");
}
}
- 原理
Spring 容器负责管理 Bean 的生命周期,通过反射机制创建 Bean 的实例,并使用依赖注入的方式为 Bean 的属性赋值。在 Bean 的初始化和销毁阶段,Spring 会调用相应的方法,开发者可在这些方法中进行初始化和清理工作。
- 要点
- 实例化、属性赋值、初始化、使用和销毁是 Bean 生命周期的主要阶段。
- 可通过实现特定接口或配置自定义方法参与 Bean 的初始化和销毁过程。
- 应用
可深入了解 Spring 的 Bean 后置处理器,它能在 Bean 生命周期的各个阶段进行干预,实现更复杂的功能。
8. 事务的实现方式
在 Spring 中,事务的实现方式主要有两种:
- 编程式事务管理:通过编写代码管理事务的开始、提交和回滚。可使用
TransactionTemplate
或PlatformTransactionManager
实现。示例代码如下:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@Service
public class ProgrammaticTransactionService {
@Autowired
private PlatformTransactionManager transactionManager;
public void doTransaction() {
TransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 执行数据库操作
System.out.println("Doing database operations...");
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
}
- 声明式事务管理:通过配置文件或注解声明事务属性,Spring 会自动管理事务的开始、提交和回滚。常用注解有
@Transactional
。示例代码如下:
java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DeclarativeTransactionService {
@Transactional
public void doTransaction() {
// 执行数据库操作
System.out.println("Doing database operations...");
}
}
- 原理
编程式事务管理通过手动调用事务管理器的方法控制事务流程。声明式事务管理通过 AOP 实现,Spring 会在目标方法执行前后插入事务管理代码,实现对事务的自动管理。
- 要点
- 编程式事务管理灵活性高,但代码复杂度高。
- 声明式事务管理代码简洁,易于维护,但灵活性相对较低。
- 应用
可了解不同的事务管理器,如 DataSourceTransactionManager
、JtaTransactionManager
等,以及它们的使用场景。
9. 事务的传播级别
- 定义
事务的传播级别定义了一个事务方法调用另一个事务方法时,事务的行为方式。Spring 提供了 7 种事务传播级别:
- PROPAGATION_REQUIRED:若当前存在事务,则加入该事务;若当前没有事务,则创建一个新的事务。这是最常用的传播级别。
- PROPAGATION_SUPPORTS:若当前存在事务,则加入该事务;若当前没有事务,则以非事务方式执行。
- PROPAGATION_MANDATORY:若当前存在事务,则加入该事务;若当前没有事务,则抛出异常。
- PROPAGATION_REQUIRES_NEW:创建一个新的事务,若当前存在事务,则将当前事务挂起。
- PROPAGATION_NOT_SUPPORTED:以非事务方式执行,若当前存在事务,则将当前事务挂起。
- PROPAGATION_NEVER:以非事务方式执行,若当前存在事务,则抛出异常。
- PROPAGATION_NESTED:若当前存在事务,则在嵌套事务内执行;若当前没有事务,则创建一个新的事务。
- 原理
事务的传播级别通过事务管理器实现,事务管理器会根据传播级别的定义决定如何处理事务。
-
要点
- 不同的传播级别适用于不同的业务场景。
- 理解传播级别的含义和行为,正确选择传播级别。
- 应用
可深入了解嵌套事务的实现原理和使用场景,以及在不同数据库中的支持情况。
10. 事务的嵌套失效
事务嵌套失效通常由以下原因导致:
- 方法调用问题:在同一个类中,一个非事务方法调用另一个事务方法,事务会失效。因为 Spring 的事务是通过 AOP 代理实现的,同一个类中的方法调用不会经过代理对象,所以事务不会生效。可将事务方法提取到另一个类中来解决此问题。
- 异常处理问题:若事务方法中捕获了异常但没有重新抛出,事务不会回滚。因为 Spring 的事务管理基于异常触发回滚,若没有异常抛出,事务会正常提交。需确保在事务方法中捕获异常后重新抛出,或在
@Transactional
注解中指定需要回滚的异常类型。 - 传播级别设置问题:若传播级别设置不当,也会导致事务嵌套失效。例如,使用
PROPAGATION_NOT_SUPPORTED
传播级别,会以非事务方式执行,即使在事务方法中调用也不会生效。需根据业务需求正确设置传播级别。
- 原理
Spring 的事务管理通过 AOP 代理实现,只有通过代理对象调用事务方法,事务才会生效。事务的回滚基于异常触发,若没有异常抛出,事务会正常提交。
- 要点
- 避免在同一个类中进行非事务方法调用事务方法。
- 正确处理异常,确保异常能够被抛出。
- 合理设置事务的传播级别。
- 应用
可了解如何通过 AOP 代理解决同一个类中方法调用事务失效的问题,以及如何使用 @Transactional
注解的 rollbackFor
属性指定需要回滚的异常类型。
友情提示:本文已经整理成文档,可以到如下链接免积分下载阅读