1、BeanFactory 和 ApplicationContext 有什么区别?
-
BeanFactory 可以理解为含有 bean 集合的工厂类。BeanFactory 包含了种 bean 的定义,以便在接收到客户端请求时将对应的 bean 实例化。
-
BeanFactory 还能在实例化对象的时生成协作类之间的关系。此举将 bean 自身与 bean 客户端的配置中解放出来。BeanFactory 还包含了 bean 生命周期的控制,调用客户端的初始化方法( initialization methods)和销毁方法( destruction methods)。
-
从表面上看, application context 如同 bean factory 一样具有 bean 定义、 bean 关联关系的设置,根据请求分发 bean 的功能。但 application context 在此基础上还提供了其他的功能。
-
-
-
-
Spring Bean 的生命周期简单易懂。在一个 bean 实例被初始化时,需要执行一系列的初始化操作以达到可用的状态。同样的,当一个 bean 不在被调用时需要进行相关的析构操作,并从 bean 容器中移除。
-
Spring bean factory 负责管理在 spring 容器中被创建的 bean 的生命周期。 Bean 的生命周期由两组回调( call back)方法组成。
-
-
Spring 框架提供了以下四种方式来管理 bean 的生命周期事件:
InitializingBean 和 DisposableBean 回调接口
Bean 配置文件中的 Custom init()方法和 destroy()方法
@PostConstruct 和@PreDestroy 注解方式
3、Spring IOC 如何实现
Spring 中的 org.springframework.beans 包和 org.springframework.context 包构成了Spring 框架 IoC 容器的基础。
BeanFactory 接口提供了一个先进的配置机制,使得任何类型的对象的配置成为可能。ApplicationContex 接口对 BeanFactory(是一个子接口)进行了扩展,在 BeanFactory 的基础上添加了其他功能,比如与 Spring 的 AOP 更容易集成,也提供了处理 message resource 的机制(用于国际化)、事件传播以及应用层的特别配置,比如针对 Web 应用的WebApplicationContext。
org.springframework.beans.factory.BeanFactory 是 Spring IoC 容器的具体实现,用来包装和管理前面提到的各种 bean。BeanFactory 接口是 Spring IoC 容器的核心接口。
4、说说 Spring AOP
面向切面编程,在我们的应用中,经常需要做一些事情,但是这些事情与核心业务无关,比如,要记录所有 update*方法的执行时间时间,操作人等等信息,记录到日志, > 通过 spring 的 AOP 技术,就可以在不修改 update*的代码的情况下完成该需求。
5、Spring AOP 实现原理
Spring AOP 中的动态代理主要有两种方式, JDK 动态代理和 CGLIB 动态代理。JDK 动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类。
如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB ( Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意, CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final,那么它是无法使用 CGLIB 做动态代理的。
6、动态代理( cglib 与 JDK)
JDK 动态代理类和委托类需要都实现同一个接口。也就是说只有实现了某个接口的类可以使用 Java 动态代理机制。但是,事实上使用中并不是遇到的所有类都会给你实现一个接口。因此,对于没有实现接口的类,就不能使用该机制。而 CGLIB 则可以实现对类的动态代理。
7、Spring 事务实现方式
1、编码方式
所谓编程式事务指的是通过编码方式实现事务,即类似于 JDBC 编程实现事务管理
声明式事务管理又有两种实现方式:基于 xml 配置文件的方式;另一个实在业务方法上进行@Transaction 注解,将事务规则应用到业务逻辑中
8、Spring 事务底层原理
a、划分处理单元——IOC
由于 spring 解决的问题是对单个数据库进行局部事务处理的,具体的实现首相用 spring中的 IOC 划分了事务处理单元。并且将对事务的各种配置放到了 ioc 容器中(设置事务管理器,设置事务的传播特性及隔离机制)。
b、 AOP 拦截需要进行事务处理的类
Spring 事务处理模块是通过 AOP 功能来实现声明式事务处理的,具体操作(比如事务实行的配置和读取,事务对象的抽象),用 TransactionProxyFactoryBean 接口来使用 AOP功能,生成 proxy 代理对象,通过 TransactionInterceptor 完成对代理方法的拦截,将事务处理的功能编织到拦截的方法中。读取 ioc 容器事务配置属性,转化为 spring 事务处理需要的内部数据结构( TransactionAttributeSourceAdvisor),转化为TransactionAttribute 表示的数据对象。> c、对事物处理实现(事务的生成、提交、回滚、挂起)
spring 委托给具体的事务处理器实现。实现了一个抽象和适配。适配的具体事务处理器:DataSource 数据源支持、 hibernate 数据源事务处理支持、 JDO 数据源事务处理支持, JPA、 JTA 数据源事务处理支持。这些支持都是通过设计PlatformTransactionManager、 AbstractPlatforTransaction 一系列事务处理的支持。为常用数据源支持提供了一系列的 TransactionManager。
d、结合
PlatformTransactionManager 实现了 TransactionInterception 接口,让其与TransactionProxyFactoryBean 结合起来,形成一个 Spring 声明式事务处理的设计体系。
9、如何自定义注解实现功能
创建自定义注解和创建一个接口相似,但是注解的 interface 关键字需要以@符号开头。> 注解方法不能带有参数;
注解方法返回值类型限定为:基本类型、 String、 Enums、 Annotation 或者是这些类型的数组;
注解方法可以有默认值;
注解本身能够包含元注解,元注解被用来注解其它注解。
10、Spring MVC 运行流程
1.spring mvc 将所有的请求都提交给 DispatcherServlet,它会委托应用系统的其他模块负责对请求 进行真正的处理工作。
2.DispatcherServlet 查询一个或多个 HandlerMapping,找到处理请求的 Controller.
3.DispatcherServlet 请请求提交到目标 Controller
4.Controller 进行业务逻辑处理后,会返回一个 ModelAndView
5.Dispathcher 查询一个或多个 ViewResolver 视图解析器,找到 ModelAndView 对象指定的视图对象
6.视图对象负责渲染返回给客户端。
11、Spring MVC 启动流程
在 web.xml 文件中给 Spring MVC 的 Servlet 配置了 load-on-startup,所以程序启动的时候会初始化 Spring MVC,在 HttpServletBean 中将配置的 contextConfigLocation > 属性设置到 Servlet 中,然后在 FrameworkServlet 中创建了 WebApplicationContext, DispatcherServlet 根据 contextConfigLocation 配置的 classpath 下的 xml 文件初始化了 Spring MVC 总的组件。
12、Spring 的单例实现原理
Spring 对 Bean 实例的创建是采用单例注册表的方式进行实现的,而这个注册表的缓存是ConcurrentHashMap 对象。
13、Spring 框架中用到了哪些设计模式
代理模式—在 AOP 和 remoting 中被用的比较多。
单例模式—在 spring 配置文件中定义的 bean 默认为单例模式。
模板方法—用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
前端控制器—Spring 提供了 DispatcherServlet 来对请求进行分发。
视图帮助(View Helper )—Spring 提供了一系列的 JSP 标签,高效宏来辅助将分散的代码整合在视图里。
依赖注入—贯穿于 BeanFactory / ApplicationContext 接口的核心理念。
工厂模式—BeanFactory 用来创建对象的实例。
14、为什么选择 Netty
1) API 使用简单,开发门槛低;
2) 功能强大,预置了多种编解码功能,支持多种主流协议;
3) 定制能力强,可以通过 ChannelHandler 对通信框架进行灵活的扩展;
4) 性能高,通过与其它业界主流的 NIO 框架对比, Netty 的综合性能最优;
5) 成熟、稳定, Netty 修复了已经发现的所有 JDK NIO BUG,业务开发人员不需要再为NIO 的 BUG 而烦恼;
6) 社区活跃,版本迭代周期短,发现的 BUG 可以被及时修复,同时,更多的新功能会被加入;
7) 经历了大规模的商业应用考验,质量已经得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它可以完全满足不同行业的商业应用。
正是因为这些优点, Netty 逐渐成为 Java NIO 编程的首选框架。
CAS 是 compare and swap 的缩写, 即我们所说的比较交换。
cas 是一种基于锁的操作, 而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住, 等一个之前获得锁的线程释放锁之后, 下一个线程才可以访问。而乐观锁采取了一种宽泛的态度, 通过某种方式不加锁来处理资源, 比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V) 、 预期原值(A) 和新值(B)。如果内存地址里面的值和 A 的值是一样的, 那么就将内存里面的值更新成 B。CAS 是通过无限循环来获取数据的, 若果在第一轮循环中, a 线程获取地址里面的值被 b 线程修改了, 那么 a 线程需要自旋, 到下次循环才有可能机会执行。
java.util.concurrent.atomic 包 下 的 类 大 多 是 使 用 CAS 操 作 来 实 现 的(AtomicInteger,AtomicBoolean,AtomicLong)。
一个线程 a 将数值改成了 b, 接着又改成了 a, 此时 CAS 认为是没有变化, 其实是已经变化过了, 而这个问题的解决方案可以使用版本号标识, 每操作一次 version 加 1。在 java5中, 已经提供了 AtomicStampedReference 来解决问题。
CAS 机制所保证的知识一个变量的原子性操作, 而不能保证整个代码块的原子性。比如需要保证 3 个变量共同进行原子性的更新, 就不得不使用 synchronized 了。
之前说过了 CAS 里面是一个循环判断的过程, 如果线程一直没有获取到状态, cpu 资源会一直被占用。
17、TCP 粘包/拆包的解决办法
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度(不够的可以通过补 0 填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
18、Netty 线程模型
首先, Netty 使用 EventLoop 来处理连接上的读写事件,而一个连接上的所有请求都保证在一个 EventLoop 中被处理,一个 EventLoop 中只有一个 Thread,所以也就实现了一个连接上的所有事件只会在一个线程中被执行。一个 EventLoopGroup 包含多个 EventLoop,可以把一个 EventLoop 当做是 Reactor 线程模型中的一个线程,而一个 EventLoopGroup 类似于一个 ExecutorService
19、说说 Netty 的零拷贝
“零拷贝”是指计算机操作的过程中, CPU 不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间( User Space)而直接在内核空间( Kernel Space)中传输到网络的方式。
首先明确一下, 不是说 ReentrantLock 不好, 只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock, 可能本身是为了防止线程 A 在写数据、 线程 B 在读数据造成的数据不一致, 但这样, 如果线程 C 在读数据、 线程 D 也在读数据, 读数据是不会改变数据的,没有必要加锁, 但是还是加锁了, 降低了程序的性能。因为这个, 才诞生了读写锁ReadWriteLock 。ReadWriteLock 是 一 个 读 写 锁 接 口 , ReentrantReadWriteLock 是ReadWriteLock 接口的一个具体实现, 实现了读写的分离, 读锁是共享的, 写锁是独占的,读和读之间不会互斥, 读和写、 写和读、 写和写之间才会互斥, 提升了读写的性能。
这个其实前面有提到过, FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类, 可以对这个异步运算的任务的结果进行等待获取、 判断是否已经完成、 取消任务等操作。当然, 由于 FutureTask 也是 Runnable 接口的实现类, 所以FutureTask 也可以放入线程池中。
22、 synchronized 和 ReentrantLock 的区别
synchronized 是和 if、 else、 for、 while 一样的关键字, ReentrantLock 是类, 这是二者的本质区别。既然 ReentrantLock 是类, 那么它就提供了比 synchronized 更多更灵活的特性, 可以被继承、 可以有方法、 可以有各种各样的类变量, ReentrantLock 比 synchronized 的扩展性体现在几点上:
(1) ReentrantLock 可以对获取锁的等待时间进行设置, 这样就避免了死锁
(2) ReentrantLock 可以获取各种锁的信息
(3) ReentrantLock 可以灵活地实现多路通知
另外, 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁, synchronized 操作的应该是对象头中 mark word, 这点我不能确定。
就像它的名字一样, 对于并发间操作产生的线程安全问题持乐观状态, 乐观锁认为竞争不总是会发生, 因此它不需要持有锁, 将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量, 如果失败则表示发生冲突, 那么就应该有相应的重试逻辑。
还是像它的名字一样, 对于并发间操作产生的线程安全问题持悲观状态, 悲观锁认为竞争总是会发生, 因此每次对某资源进行操作时, 都会持有一个独占的锁, 就像 synchronized, 不管三七二十一, 直接上了锁就操作资源了。
(2) synchronized 修饰修改变量的方法
25、 synchronized、 volatile、 CAS 比较
(1) synchronized 是悲观锁, 属于抢占式, 会引起其他线程阻塞。
(2) volatile 提供多线程共享变量可见性和禁止指令重排序优化。
( 3) CAS 是基于冲突检测的乐观锁( 非阻塞)
26、 sleep 方法和 wait 方法有什么区别?
这个问题常问, sleep 方法和 wait 方法都可以用来放弃 CPU 一定的时间, 不同点在于如果线程持有某个对象的监视器, sleep 方法不会放弃这个对象的监视器, wait 方法会放弃这个对象的监视器
27、 ThreadLocal 是什么?有什么用?
ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射, 各个线程之间的变量互不干扰, 在高并发场景下, 可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。简单说 ThreadLocal 就是一种以空间 换 时 间 的 做 法 , 在 每 个 Thread 里 面 维 护 了 一 个 以 开 地 址 法 实 现 的ThreadLocal.ThreadLocalMap, 把数据进行隔离, 数据不共享, 自然就没有线程安全方面的问题了。
28、 为什么 wait()方法和 notify()/notifyAll()方法要在同步块中被调用
这是JDK 强制的, wait()方法和 notify()/notifyAll()方法在调用前都必须先获得对象的锁
Synchronized 关键字, Lock 锁实现, 分布式锁等。
线程调度器选择优先级最高的线程运行, 但是, 如果发生以下情况, 就会终止线程的运行:
( 1) 线程体中调用了 yield 方法让出了对 cpu 的占用权利
( 2) 线程体中调用了 sleep 方法使线程进入睡眠状态
( 5) 在支持时间片的系统中, 该线程的时间片用完
31、 ConcurrentHashMap 的并发度是什么
ConcurrentHashMap 的并发度就是 segment 的大小, 默认为 16, 这意味着最多同时可以有16 条线程操作 ConcurrentHashMap, 这也是 ConcurrentHashMap 对 Hashtable 的最大优势, 任何情况下, Hashtable 能同时有两条线程获取 Hashtable 中的数据吗?
32、 Linux 环境下如何查找哪个线程使用 CPU 最长
( 1) 获取项目的 pid, jps 或者 ps -ef | grep java
( 2) top -H -p pid, 顺序不能改变
Java 中的死锁是一种编程情况, 其中两个或多个线程被永久阻塞, Java 死锁情况出现至少两个线程和两个或更多资源。
Java 发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。
( 1) 是多个线程涉及到多个锁, 这些锁存在着交叉, 所以可能会导致了一个锁依赖的闭环。例如:线程在获得了锁 A 并且没有释放的情况下去申请锁 B, 这时, 另一个线程已经获得了锁 B, 在释放锁 B 之前又要先获得锁 A, 因此闭环发生, 陷入死锁循环。
所以要避免死锁, 就要在一遇到多个对象锁交叉的情况, 就要仔细审查这几个对象的类中的所有方法, 是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。
如果线程是因为调用了 wait()、 sleep()或 者 join()方法而导致的阻塞, 可以中断线程, 并且通过抛出 InterruptedException 来唤醒它;如果线程遇到了 IO 阻塞, 无能为力, 因为 IO 是操作系统实现的, Java 代码并没有办法直接接触到操作系统。
前面有提到过的一个问题, 不可变对象保证了对象的内存可见性, 对不可变对象的读取不需要进行额外的同步手段, 提升了代码执行效率。
多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。
38、 如果你提交任务时, 线程池队列已满, 这时会发生什么
(1) 如果使用的是无界队列 LinkedBlockingQueue, 也就是无界队列的话, 没关系, 继续添加任务到阻塞队列中等待执行, 因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列, 可以无限存放任务
( 2 ) 如 果 使 用 的 是 有 界 队 列 比 如 ArrayBlockingQueue , 任 务 首 先 会 被 添 加 到ArrayBlockingQueue 中, ArrayBlockingQueue 满了, 会根据 maximumPoolSize 的值增加线程数量, 如果增加了线程数量还是处理不过来, ArrayBlockingQueue 继续满, 那么则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务, 默认是 AbortPolicy
抢占式。一个线程用完 CPU 之后, 操作系统会根据线程优先级、 线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
40、 什么是线程调度器(Thread Scheduler)和时间分片(TimeSlicing)?
线程调度器是一个操作系统服务, 它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它, 它的执行便依赖于线程调度器的实现。时间分片是指将可用的CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。线程调度并不受到 Java 虚拟机控制, 所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级) 。
很多 synchronized 里面的代码只是一些很简单的代码, 执行时间非常快, 此时等待的线程都加锁可能是一种不太值得的操作, 因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized 里面的代码执行得非常快, 不妨让等待锁的线程不要被阻塞, 而是在synchronized 的边界做忙循环, 这就是自旋。如果做了多次忙循环发现还没有获得锁, 再阻塞, 这样可能是一种更好的策略。
42、 Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?
Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构, 可以具有完全不同的性质, 并且可以支持多个相关类的条件对象。
(3) 可以让线程尝试获取锁, 并在无法获取锁的时候立即返回或者等待一段时间
(4) 可以在不同的范围, 以不同的顺序获取和释放锁
老生常谈的问题了, 首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法, 我总结一下:
Semaphore 就是一个信号量, 它的作用是限制某段代码块的并发数。Semaphore 有一个构造函数, 可以传入一个 int 型整数 n, 表示某段代码最多只有 n 个线程可以访问, 如果超出了 n, 那么请等待, 等到某个线程执行完毕这段代码块, 下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1, 相当于变成了一个 synchronized 了。
Executors 为 Executor, ExecutorService, ScheduledExecutorService, ThreadFactory 和 Callable类提供了一些工具方法。Executors 可以用于方便的创建线程池
46、 线程类的构造方法、 静态块是被哪个线程调用的
这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、 静态块是被 new 这个线程类所在的线程所调用的, 而 run 方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑, 那么我举个例子, 假设 Thread2 中 new 了 Thread1, main函数中 new 了 Thread2, 那么:
(1)Thread2 的构造方法、 静态块是 main 线程调用的, Thread2 的 run()方法是 Thread2 自己调用的
(2) Thread1 的构造方法、 静态块是 Thread2 调用的, Thread1 的 run()方法是 Thread1 自己调用的
同步块, 这意味着同步块之外的代码是异步执行的, 这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。
如果可运行的线程数量多于可用处理器的数量, 那么有线程将会被闲置。大量空闲的线程会占用许多内存, 给垃圾回收器带来压力, 而且大量的线程在竞争 CPU 资源时还将产生其他性能的开销。
JVM 在可创建线程的数量上存在一个限制, 这个限制值将随着平台的不同而不同, 并且承受着多个因素制约, 包括 JVM 的启动参数、 Thread 构造函数中请求栈的大小, 以及底层操作系统对线程的限制等。如果破坏了这些限制, 那么可能抛出 OutOfMemoryError 异常。
更多文章,请关注公众号【程序员李木子】,有免费的电子书哦