11-30
依赖注入的方法有哪些?
- 构造器注入
- Setter 方法注入
- 接口注入
- 注解注入
- 工厂方法注入
11-29
BeanFactory 和 FactoryBean 的区别?
- BeanFactory:管理 Bean 的容器,Spring 中生成的 Bean 都是由这个接口的实现来管理的。
- FactoryBean:通常是用来创建比较复杂的 bean,一般的 bean 直接用 xml 配置即可,但如果一个 bean 的创建过程中涉及到很多其他的 bean 和复杂的逻辑, 直接用 xml 配置比较麻烦,这时可以考虑用 FactoryBean,可以隐藏实例化复杂 Bean 的细节。
11-28
描述 Bean 的生命周期。
以下内容使用
Github Copilot
生成。
- 实例化 Bean:Spring 通过反射机制利用
<bean>
的 class 属性指定实现类实例化 Bean。 - 设置对象属性:Spring 利用
<bean>
的<property>
子元素设置 Bean 的属性。 - 调用 Bean 的初始化方法:如果
<bean>
的 init-method 属性指定了初始化方法,则 Spring 通过反射机制调用初始化方法。 - Bean 可以使用了:Bean 对象已经可以使用了,它是一个可用的 Bean 实例。
- 当容器关闭时,调用 Bean 的销毁方法:如果
<bean>
的 destory-method 属性指定了销毁方法,则 Spring 会通过反射机制调用销毁方法。
11-27
什么是 BeanDefinition?
BeanDefinition
用于管理 Spring 应用的对象和对象之间的依赖关系,是对象依赖关系的数据抽象。
11-26
描述 Spring 的两种 IOC 容器。
Spring 有两种 IOC 容器:BeanFactory
和 ApplicationContext
。
BeanFactory
:延迟注入(使用到某个 bean 的时候才会注入),相比于BeanFactory 来说会占用更少的内存,程序启动速度更快。 BeanFactory 提供了最基本的 ioc 容器的功能(最基本的依赖注入支持)。ApplicationContext
:容器启动的时候,一次性创建所有 bean。ApplicationContext 扩展了 BeanFactory , 除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用 ApplicationContext 会更多。
11-25
请描述 Spring AOP 的动态代理。
Spring AOP中的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。
JDK动态代理
如果目标类实现了接口,Spring AOP 会选择使用 JDK 动态代理目标类。代理类根据目标类实现的接口动态生成,不需要自己编写, 生成的动态代理类和目标类都实现相同的接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。
缺点:目标类必须有实现的接口。如果某个类没有实现接口,那么这个类就不能用JDK动态代理。
CGLIB动态代理
如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)可以在运行时动态生成类的字节码, 动态创建目标类的子类对象,在子类对象中增强目标类。
CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
优点:目标类不需要实现特定的接口,更加灵活。
11-24
描述AOP的两种实现方式。
AOP 有两种实现方式:静态代理 和 动态代理。
静态代理
代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,也称编译时增强。AspectJ使用的是静态代理。
缺点: 代理对象需要与目标对象实现一样的接口,并且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护。
动态代理
代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。
1.2 动态代理
11-23
CountDownLatch 和 CyclicBarrier 的区别。
- CountDownLatch 的计数器只能使用一次。而 CyclicBarrier 的计数器可以使用
reset()
方法重置。 - CyclicBarrier 能处理更为复杂的业务场景,比如计算发生错误,可以结束阻塞,重置计数器,重新执行程序
- CyclicBarrier 提供
getNumberWaiting()
方法,可以获得 CyclicBarrier 阻塞的线程数量,还提供isBroken()
方法,可以判断阻塞的线程是否被中断,等等。 - CountDownLatch 会阻塞主线程,CyclicBarrier 不会阻塞主线程,只会阻塞子线程。
11-22
什么是 CyclicBarrier?
CyclicBarrier
是一个同步工具类,它允许一组线程互相等待,直到到达某个公共屏障点(common barrier point)。因为该 barrier 在释放等待线程后可以重用,
import java.util.concurrent.CyclicBarrier;
class Scratch {
public static void main(String[] args) {
// CyclicBarrier 示例程序
CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
System.out.println("所有线程执行完毕");
});
// 测试运行
new Thread(() -> {
try {
System.out.println("线程1开始执行");
Thread.sleep(1000);
System.out.println("线程1执行完毕");
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
System.out.println("线程2开始执行");
Thread.sleep(2000);
System.out.println("线程2执行完毕");
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
System.out.println("主线程执行完毕");
}
}
11-21
什么是 CountdownLatch?
CountDownLatch
是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
import java.util.concurrent.CyclicBarrier;
class Scratch {
public static void main(String[] args) {
// CountDownLatch 示例程序
CyclicBarrier barrier = new CyclicBarrier(2, () -> {
System.out.println("barrier end");
});
new Thread(() -> {
try {
System.out.println("thread 1");
barrier.await();
System.out.println("thread 1 end");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
System.out.println("thread 2");
barrier.await();
System.out.println("thread 2 end");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
11-20
线程池都有哪些状态?
可以通过
Thread.State
枚举类来表示线程的状态。 这个枚举包括 NEW(新建)、RUNNABLE(可运行)、BLOCKED(被阻塞)、WAITING(等待)、TIMED_WAITING(定时等待)和 TERMINATED(终止)等状态。 通过Thread.getState()
方法可以获取线程的状态。
- RUNNING:接受新任务并且处理阻塞队列中的任务
- SHUTDOWN:不接受新任务,但是处理阻塞队列中的任务
- STOP:不接受新任务,不处理阻塞队列中的任务,并且中断正在处理的任务
- TIDYING:所有任务都终止了,
workerCount
为 0,线程池的状态在转换为 TIDYING 状态时会执行钩子函数terminated()
- TERMINATED:
terminated()
执行完成
11-19
notify() 和 notifyAll() 有什么区别?
notify()
方法会随机唤醒等待队列中的一个线程,而 notifyAll()
方法会唤醒等待队列中的所有线程。
11-18
sleep() 和 wait() 有什么区别?
sleep()
方法是 Thread 类的静态方法,调用该方法会让当前线程进入休眠状态,不会释放锁,休眠时间到了之后会自动恢复运行状态。wait()
方法是 Object 类的方法,调用该方法会让当前线程进入等待状态,会释放锁,直到其他线程调用 notify() 或 notifyAll() 方法之后才会重新进入运行状态。
11-17
ThreadLocal 内存泄漏原因,如何避免?
如果在线程池中使用 ThreadLocal 会造成内存泄漏,因为当 ThreadLocal 对象使用完之后,应该要把设置的key,value 也就是 Entry 对象进行回收, 但线程池中的线程不会回收,而线程对象是通过强引用指向 ThreadLocalMap, ThreadLocalMap 也是通过强引用指向 Entry 对象,线程不被回收,Entry 对象也就不会被回收,从而出现内存泄漏。
解决方法:
- 在使用了 ThreadLocal 对象之后,手动调用 ThreadLocal 的 remove 方法,手动清除 Entry 对象。
- 将 ThreadLocal 变量定义成 private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能将通过 ThreadLocal 的弱引用访问到 Entry 的 value 值,进而清除掉。
11-16
线程之间如何进行通信?
- 通过共享内存或基于网络通信
- 如果是基于共享内存进行通信,则需要考虑并发问题,什么时候阻塞,什么时候唤醒
- 想 Java 中的 wait()、notify() 就是阻塞唤醒
- 通过网络就比较简单,通过网络连接将数据发送给对方,当然也要考虑到并发问题,处理方式就是加锁等方式。
11-15
什么是 ThreadLocal?
ThreadLocal 是 Java 中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据。
ThreadLocal 底层是通过 TreadLocalMap 来实现的,每个 Thread 对象中都存在一个 ThreadLocalMap,Map 的 key 为 ThreadLocal 对象,Ma p的 value 为需要缓存的值 。
11-14
synchronized 关键字的作用?
synchronized 关键字可以用于修饰一段代码或一个方法,用于实现现成的同步,表示该段代码不能被多个线程同时执行。
当一个线程进入被 synchronized 关键字修饰的代码块或方法时,其他试图访问的线程将被阻塞。
synchronized 关键字的作用体现在两个方面:
- 确保方法或代码块的互斥访问。(加锁与解锁)
- 确保内存可见性。(强制从缓存加载数据)
11-13
线程池有几种创建方式?
- newCachedThreadPool 创建一个可缓存线程池
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务
11-12
线程的 run()和 start()有什么区别?
每个线程都是通过某个特定 Thread 对象所对应的方法 run()来完成其操作的,run()方法称为线程体。通过调用 Thread 类的 start()方法来启动一个线程。
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start()只能调用一次。
start() 方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用 run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码, 所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用 start()方法而不是 run()方法。
11-11
守护线程与用户线程有什么区别?
- 守护线程:运行在后台,为其他前台线程服务。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。
- 用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。
11-10
主线程是不是守护线程?
主线程不是守护线程,它是一个用户线程,它的优先级是普通优先级,当所有的用户线程都结束了,主线程才会结束,如果主线程结束了,其他用户线程还在运行,那么程序也会继续运行。
11-9
什么是守护线程?
守护线程是一种特殊的线程,它的优先级很低,通常用来做后台作业,比如垃圾回收线程就是一个很称职的守护者,并且它不能持有任何资源, 因为它会在任何时候被停止,如果它持有资源,那么其他线程就无法访问这些资源了。
11-8
线程的状态有哪些?
- 新建状态(New):当线程对象对创建后,即进入了新建状态;
- 就绪状态(Runnable):当调用线程对象的
start()
方法,线程即进入就绪状态。 - 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
- 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对 CPU 的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。 根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞:运行状态中的线程执行
wait()
方法,使本线程进入到等待阻塞状态; - 同步阻塞 — 线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 其他阻塞 — 通过调用线程的
sleep()
或join()
或发出了I/O请求时,线程会进入到阻塞状态。当sleep()
状态超时.join()
等待线程终止或者超时. 或者 I/O 处理完毕时,线程重新转入就绪状态。
- 等待阻塞:运行状态中的线程执行
- 死亡状态(Dead):线程执行完了或者因异常退出了
run()
方法,该线程结束生命周期。
11-7
创建线程有哪几种方式?
- 继承 Thread 类,重写 run() 方法,调用 start() 方法启动线程。
- 实现 Runnable 接口,重写 run() 方法,创建 Thread 对象,调用 start() 方法启动线程。
- 实现 Callable 接口,重写 call() 方法,创建 FutureTask 对象,创建 Thread 对象,调用 start() 方法启动线程。
- 使用线程池创建线程。
使用线程池的方式创建是最优解,因为线程池可以有效的控制线程的数量,避免因为创建线程过多导致的内存溢出,提高了系统资源的利用率,同时线程池提供了更多更强大的功能。
11-6
都有哪些原子类?请举例说明。
java.util.concurrent
这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性。
- 原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
- 原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
- 原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
- 解决ABA问题的原子类:AtomicMarkableReference(通过引入一个boolean来反映中间有没有变过),AtomicStampedReference(通过引入一个int来累加来反映中间有没有变过)
11-5
什么是原子操作?
原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。 在Java中可以通过锁和循环CAS的方式来实现原子操作。CAS操作——Compare & Set,或是 Compare & Swap,现在几乎所有的CPU指令都支持CAS的原子操作。
原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。 int++
并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。
为了解决这个问题,必须保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点。 到JDK1.5,java.util.concurrent.atomic包提供了int和long类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。
11-4
execute 与 submit 的用法与区别。
execute 执行后没有返回结果,只有1种用法
方法 | 描述 |
---|---|
void execute(Runnable command) | 常规使用方法,用没有返回值的 execute 执行没有返回值的 Runnable 任务 |
submit 执行后有返回结果,有3种用法
方法 | 描述 |
---|---|
Future submit(Callable task) | 常规使用方法,传入有返回值的 callable 任务,最终返回 task 的返回值 |
Future submit(Runnable task, T result) | 由于 Runnable 没有返回值,但是可以自定义一个返回值用于返回 |
Future submit(Runnable task) | 强行使用 submit 执行 Runnable 方法,忽略返回值 |
11-3
Runnable 与 Callable 的区别。
Runnable 没有返回结果,即没有 return 语句;Callable 有返回结果。
可以通过 Executors 工厂类将 Runnable 封装为一个 Callable 对象。
11-2
请描述 Executor 框架的执行过程。
1、通过实现 Runnable 接口或 Callable 接口创建任务。
2、通过Executors的工厂方法创建线程池。
3、通过 ExecutorService.submit()
提交一个有返回结果的任务,返回类型为一个实现Future接口的对象;或通过ExecutorService.execute()方法执行一个没有返回结果的任务。
4、通过 FutureTask.get()
获取返回结果(如果有)。
11-1
说一说你理解的 Executor 框架。
Executor 是线程池的调度工具,线程池是 Executor 的一部分。
Executor 框架由三大部分组成
1、任务: 即被执行任务需要实现的接口:Runnable 接口或 Callable 接口。
2、线程池: 主要通过 ExecutorService 接口调用线程池,有2个关键实现类 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor。
3、异步计算的结果: Future 接口及其实现类 FutureTask。