3.6 ReadWriteLock
ReentrantLock是一种排它锁,它在同一时刻都只允许一个线程进行访问。而ReadWriteLock在同一时刻可以允许多个读线程进行访问,但在写线程访问时,所有的其他读线程和写线程均被阻塞。
ReadWriteLock内部维护了一个重写的队列同步器,一个读锁和一个写锁。读锁lock()时调用同步器的tryAcquireShared()方法,写锁lock()时调用同步器tryAcquire()方法。
下面是通过ReadWriteLock实现的一个线程安全的Cache:
public final class Cache { private static final Map<String, Object> container = new HashMap<>(); private static final ReadWriteLock lock = new ReentrantReadWriteLock(); public static Object get(String key) { lock.readLock().lock(); try { return container.get(key); } finally { lock.readLock().unlock(); } } public static void put(String key, Object value) { lock.writeLock().lock(); try { container.put(key, value); } finally { lock.writeLock().unlock(); } } }
3.7 Condition
我们知道,任意一个Java对象都有一组监视器方法(wait,notify),这些方法与synchronized关键字配合,可以实现等待/通知模式。而在Lock接口中,Condition提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。
下面是使用Condition await/signal实现的一个有界队列:
public class BoundedQueue<T> { private Object[] elements; private int addIndex; private int removeIndex; private int size; private Lock lock = new ReentrantLock(); private Condition notFull = lock.newCondition(); private Condition notEmpty = lock.newCondition(); public void offer(T t) throws InterruptedException { lock.lock(); try { while (size == elements.length) { notFull.await(); } elements[addIndex] = t; if (++addIndex == elements.length) { addIndex = 0; } size++; notEmpty.signal(); } finally { lock.unlock(); } } public T poll() throws InterruptedException { lock.lock(); try { while (size == 0) { notEmpty.await(); } Object removed = elements[removeIndex]; if (++removeIndex == elements.length) { removeIndex = 0; } size--; notFull.signal(); return (T) removed; } finally { lock.unlock(); } } }
Condition与Object监视器方法相比,拥有两个额外特性:
1. 支持在等待状态中不响应中断:void awaitUninterruptibly()
2. 支持等待直至将来某个时间点:boolean awaitUntil(Date deadline) throws InterruptedException
3.8 ContdownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作。
CountDownLatch主要提供了两个API:
public CountDownLatch(int count); public void countDown(); public void await() throws InterruptedException();
CountDownLatch通过构造器传入一个count值,每次调用countDown()方法count的值就会减一。在count值不为0时,所有调用了await方法的线程都会被阻塞,直到count的值为0为止。
CountDownlatch是通过聚合一个AQS实现的,下面是它的主要实现:
public class CountDownLatch { /** * Synchronization control For CountDownLatch. * Uses AQS state to represent count. */ private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { setState(count); } int getCount() { return getState(); } protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } protected boolean tryReleaseShared(int releases) { for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } } } private final Sync sync; public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); } public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); } public void countDown() { sync.releaseShared(1); } public long getCount() { return sync.getCount(); }
3.9 Semaphore
Semaphore(信号量)用来控制同时访问特定资源的线程数量。它通过协调各个线程,以保证多线程合理的使用公共资源。
Semaphore主要API如下:
public Semaphore(int permits); public void acquire() throws InterruptedException; public void release(int permits);
Semaphore通过构造器传入一个初始许可证数量,每次通过void acquire()
方法尝试访问同步资源,访问成功则计数器值减一;通过release(int permits)
退出同步资源,退出成功则计数器值加一。因此通过Semaphore,我们可以控制并行访问资源的线程数量。
下面是一个使用Semaphore的例子:
public class DatabaseWriter { private static final int IO_THREAD_COUNT = 20; private static final int MAX_DB_CONNECTION_COUNT = 10; private static final ExecutorService threadPool = Executors.newFixedThreadPool(IO_THREAD_COUNT); private static final Semaphore semaphore = new Semaphore(MAX_DB_CONNECTION_COUNT); public static void main(String[] args) { for (int i=0; i< IO_THREAD_COUNT; i++) { threadPool.execute(() -> { try { semaphore.acquire(); System.out.println(Thread.currentThread() + " connect to database..."); semaphore.release(); } catch (Exception e) { e.printStackTrace(); } }); } threadPool.shutdown(); } }
Semaphore可以用作流量控制,特别是公共资源有限的应用场景。比如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动及时个线程并发的读取。但是读入内存后,要存储到数据库中,而数据库连接只有10个,这时我们必须控制只有10个线程可以同时获取到数据库连接保存数据。
Semaphore底层也是通过聚合一个AQS实现的,下面是它的主要实现:
public void acquire(int permits) throws InterruptedException { if (permits < 0) throw new IllegalArgumentException(); sync.acquireSharedInterruptibly(permits); } public void release(int permits) { if (permits < 0) throw new IllegalArgumentException(); sync.releaseShared(permits); } abstract static class Sync extends AbstractQueuedSynchronizer { Sync(int permits) { setState(permits); } protected boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); if (compareAndSetState(current, next)) return true; } } protected int tryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } } }
3.10 ThreadPoolExecutor
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理使用线程池能够带来3个好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立刻执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。
下面是线程池的构造函数定义:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
创建一个线程池的主要参数有:
- int corePoolSize:线程池的基本大小。当提交一个任务到线程池时,即使存在空闲的线程,线程池也会创建一个新的线程来执行任务,除非当前线程池的线程数量已经超过corePoolSize。如果调用了prestartAllCoreThreads()方法,线程池会提前创建并启动所有的基本线程。
- int maximumPoolSize:线程池允许创建的最大线程数量。如果队列已满,并且创建的线程数量小于maximumPoolSize,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界队列,那么这个参数就没有什么意义。
- long keepAliveTime(多余线程保持活动的时间):当线程池线程数量大于corePoolSize时,线程池的工作线程空闲后,保持存活的时间。
- TimeUnit unit:多余线程保持活动的时间的单位。
- BlockingQueue<Runnable> workQueue:用于保存等待执行的任务的阻塞队列。可以选择如下几个阻塞队列:
- ArrayBlockingQueue:是一个基于数组的有界阻塞队列。按FIFO对元素进行排序。
- LinkedBlockingQueue:基于链表的阻塞队列。按FIFO对元素进行排序。吞吐量通常高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等待另一个线程调用移除操作,否则插入操作一直处于阻塞状态。吞吐量通常高于LinkedBlockingQueue。静态工厂方法Executors.newCachedThreadPool()使用了这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
- ThreadFactory threadFactory:用于设置创建线程的工厂。ThreadFactory是一个仅仅拥有一个方法的接口:
Thread newThread (Runnable r);
- RejectedExecutionHandler handler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理新提交的任务。这个策略默认是AbortPolicy。JDK1.5提供了如下四种策略:
- AbortPolicy:直接抛出异常。
- CallerRunsPolicy:使用调用者所在的线程来运行任务。
- DiscardOldestPolicy:丢弃掉队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
- 当然也可以根据应用场景来实现RejectedExceptionHandler接口自定义策略。如记录日志或者持久化不能存储的任务。
通常可以使用两种API向线程池提交任务:
//用于提交不需要返回值的任务 void execute(Runnable command); //返回Future接口,通过调用其get()方法来获取返回值,get()方法会阻塞当前线程直到任务返回。 Future<T> submit(Callable<T> task);
另外,我们可以通过void shuntdown()
或者List<Runnable> shutdownNow()
方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后调用interrupt方法来中断线程,所以无法响应中断的任务可能永远无法停止。
void shuntdown()
方法将线程池的状态设为SHUTDOWN状态,可以达到拒绝任何新的任务的效果。但是不会对已经提交了的任务造成影响。
而List<Runnable> shutdownNow()
方法在void shuntdown()
方法的基础上,会尝试中断已经提交了的任务。如果任务忽略中断,那么效果和void shuntdown()
方法一样。List<Runnable> shutdownNow()
方法会返回线程池中所有处于等待状态的任务。
3.11 Executors
Executor框架主要由3大部分组成:
1. 任务。包括被执行的任务需要实现的接口:Runnable和Callable。
2. 任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口:ThreadPoolExecutor和ScheduledThreadPoolExecutor。
3. 异步计算的结果。包括接口Future和实现Future接口的FutureTask类。
ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
ScheduledThreadPoolExecutor可以在给定的时间延迟后运行命令,或者定期执行命令。
Executors工具类可以创建3中不同类型的ThreadPoolExecutor:SingleThreadExecutor,FixedThreadPool和CachedThreadPool。
1. Executors.newFixedThreadPool(int nThreads):创建使用固定线程数的线程池。它适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,适合负载较重的服务器。
2. Executors.newSingleThreadExecutor():创建使用单个线程的线程池。适用于需要保证顺序的执行各个任务,并且在任意时间点,不会有多个线程活动的应用场景。
3. Executors.newCachedThreadPool():创建一个大小无界的线程池。适用于执行大量短期的异步执行的任务,适合负载较轻的服务器。由于使用SynchronousQueue,并且maximumPoolSize无限,keepAliveTime为60s,因此吞吐量最好。与FixedThreadPool不同,当新的任务到来,如果有线程空闲,那么空闲的线程会直接接受该任务,如果没有空闲的线程,线程池可以无限创建新的线程。
3.11.1 Executors.newFixedThreadPool(int nThreads)的实现
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
可以看到,FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。
此外 keepAliveTime 为 0,也就是多余的空余线程会被立即终止(由于这里没有多余线程,这个参数也没什么意义了)。
这里选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,相当于没有上限。因此maximumPoolSize是一个无效参数。
下面是FixedThreadPool的任务处理流程:
1. 对于一个新的task,如果当前线程数少于核心线程数,新建新的线程执行任务。
2. 如果当前线程数等于核心线程数后,将任务加入阻塞队列。
3. 执行完任务的线程反复去队列中取任务执行。
FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,适合负载较重的服务器。
3.11.2 Executors.newSingleThreadExecutor()的实现
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1,其他参数和FixedThreadPool相同。
SingleThreadExecutor适用于需要保证顺序的执行各个任务,并且在任意时间点,不会有多个线程活动的应用场景。
3.11.3 Executors.newCachedThreadPool()的实现
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
可以看到,CachedThreadPool 没有核心线程,非核心线程数无上限,也就是全部使用外包,但是每个外包空闲的时间只有 60 秒,超过后就会被回收。
CachedThreadPool 使用的队列是 SynchronousQueue,这个队列的作用就是传递任务,并不会保存。
因此当提交任务的速度大于处理任务的速度时,每次提交一个任务,就会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。
下面是CachedThreadPool的任务处理流程:
1. 对于一个新的task,由于没有核心线程数量为0,因此首先执行Synchronous.offer(Runnable task),如果线程池中有空闲线程正在执行Synchronous.pool(timeAlive),那么主线程执行offer操作与空闲线程执行poll操作匹配成功,任务交给空闲线程执行,execute方法执行完成。
2. 当线程池为空或者线程池中没有空闲线程时,这种情况下offer操作失败,此时线程池创建新的线程执行任务,execute方法执行完成。
3. 执行任务的线程执行完毕后,会执行Synchronous.pool(timeAlive)操作,这个poll操作会让空闲线程最多等待60秒,如果60秒内没有获得新的任务执行,那么这个空闲线程将被终止。
因此,CachedThreadPool适用于执行大量短期的异步执行的任务,适合负载较轻的服务器。
3.11.4 ScheduledThreadPoolExecutor
Executors同样可以创建两种类型的ScheduledThreadPoolExecutor:
- Executors.newScheduledThreadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建固定线程数量的ScheduledThreadPool。适用于需要多个后台线程执行周期性任务,同时为了满足资源管理的需求而限制后台的线程数量。
- Executors.newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
适用于需要单个后台线程执行周期性任务,同时需要保证顺序的执行各个任务的场景。
ScheduledThreadPoolExecutor的执行主要分为两步:
1. 调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()
方法或者scheduleWithFixedDelay()
方法,会向ScheduledThreadPoolExecutor的DelayQueue(无界阻塞队列)添加一个ScheduledFutureTask。
2. 线程池从DelayQueue中获取ScheduledFutureTask,然后执行任务。
ScheduledFutureTask主要包含三个关键变量:
- long time:任务将要被执行的时间点
- long sequenceNumber:任务ID
- long period:任务执行的间隔周期
DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的ScheduledFutureTask排序,time小的排在前面。
下面是ScheduledThreadPoolExecutor的某个线程执行周期性任务的步骤:
1. 线程从DelayQueue中获取已到期的ScheduledFutureTask,并执行。
2. 执行完毕后线程修改这个task的time为下次要执行的时间。
3. 把修改后的task放回DelayQueue中。
下面是ScheduledThreadPoolExecutor的某个线程从DelayQueue中获取 ScheduledFutureTask的过程:
1. 获取Lock。
2. 如果PriorityQueue为空,则当前线程到Condition中等待。
3. 如果PriorityQueue的头元素的time比当前时间大,则在Condition中等待到time时间点。
4. 获取PriorityQueue的头元素,如果PriorityQueue不为空,则唤醒在Condition中等待的所有线程。
5. 释放Lock。
下面是ScheduledThreadPoolExecutor向DelayQueue中添加 ScheduledFutureTask的过程:
1. 获取Lock。
2. 向PriorityQueue中添加任务。
3. 如果添加的任务是头元素,唤醒所有等待在Condition中的线程。
4. 释放Lock。
3.12 常见问题
- 多线程顺序打印ABC
- 生产者消费者
4.Java虚拟机
4.1 JVM内存模型
运行时数据区.jpg
Java虚拟机运行时数据区包括:
- 程序计数器:线程私有的数据区,是当前线程所执行的字节码的行号指示器。字节码解释器在工作时通过修改这个计数器的值来选取下一条需要执行的字节码指令。
- 虚拟机栈:线程私有的数据区,虚拟机栈描述的虚拟机在执行Java方法时的内存模型:每一个方法在执行时都会创建一个栈帧(Stack Frame,方法运行时的基础数据结构),用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用到完成的过程,就对应着一个栈帧从入栈到出栈的过程。
- 本地方法栈:本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,区别在于虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为执行虚拟机使用到的native方法服务。
- 堆:Java堆(Java Heap)是被所有线程共享的一块内存区域,在虚拟机启动时创建。Java堆唯一的目的就是存放对象实例,所有的对象以及数组都在堆上分配。
- 方法区:方法区(Method Area)是各个线程共享的内存区域。它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据。
- 运行时常量池:行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期间和运行期间生成的各种字面量和符号引用。
[图片上传中...(常量池.png-fa20ce-1563929526139-0)]
字面量是指字符串或者数值。例如
int a = 8; String s = "hello";
这里的8
和"hello"
都是字面量。而符号引用是用于无歧义的定位到一个目标的。例如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language来表示Language类的地址。在程序运行时,JVM会去运行时常量池中查找这个符号引用并将其替换为直接引用(实际存储在内存中的地址)。
4.2 垃圾回收算法
JVM在清理堆内存时,首先要判断是否应该回收该对象。常见的判断算法有两种:引用计数法和可达性分析法。
- 引用计数法:为每一个对象添加一个引用计数器。每当有对象引用它时,计数器的值加1;当引用失效时,计数器的值减1。任何时刻计数器值为0的对象就是需要被回收的对象。缺点:无法解决循环引用的问题。
- 可达性分析法:通过一系列称为GC Roots的对象作为起点,从这些节点向下搜索,搜索走过的路径称为引用链(Reference Chain)。当一个对象到GC Roots 没有任何引用链相连时(就是从GC Roots 到这个对象不可达),则证明此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即Native方法)引用的对象。
JVM常见的垃圾回收算法包括:标记-清除算法,复制算法和标记-整理算法。
4.2.1 标记-清除算法
标记-清除算法,顾名思义分为标记和清除两个步骤:首先标记出所有需要回收的对象,然后统一清除待回收对象。它的缺点主要有两点:
- 效率问题。标记和清除两个操作效率都不高。
- 内存碎片问题。标记清除之后会产生大量的内存碎片,空间碎片太多可能导致无法分配较大的对象,从而再次触发新一轮的垃圾收集动作。
4.2.2 复制算法
为了解决内存碎片问题,出现了复制算法。它的思想是将堆内存分为两块,每次只使用其中一块。当第一块内存空间用完,就将所有存活的对象复制到另一块上,然后把已经使用过的内存一次性清理掉。
在真实的商业虚拟机中,又将堆内存分为了新生代(占用1/3堆内存)和老年代(占用2/3堆内存)。通常在新生代上使用复制算法。由于新生代中98%的对象都是朝生夕死的,所以并不需要按照1比1的比例来划分内存空间,而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间。每次使用Eden和其中一块Survivor空间。当回收时,将Eden和Survivor中还存活着的对象一次性复制到另一块Survivor中,最后清理掉使用过的Enden和Survivor空间。这意味着只有大约10%的内存会被浪费。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不足时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
分配担保:如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。
为什么要使用两块Survivor?答案是为了减少内存碎片。假如只有一块Eden和一块Survivor,那么垃圾回收时,Eden和Survivor区各自拥有一些存活对象,在清理了Survivor区后,此时将Eden区存活对象复制到Survivor区,必然造成了内存的不连续性。
Minor GC:从新生代(Eden和Survivor)空间回收内存。Eden区域满了就会触发Minor GC。
Major GC:从老年代空间回收内存。
Full GC:清理整个堆空间包括新生代和老年代。
4.2.3 标记-整理算法
复制收集算法在对象存活率较高的情况下就要进行较多的复制操作,效率将变低。更关键的是,需要有额外的内存空间进行分配担保,所以老年代一般不选用这种算法,而是采用了标记-整理算法。
标记-整理算法:标记过程与标记-清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
4.2.4 分代收集算法
分代收集算法根据对象存活周期的不同将内存划分为几块,一般是将Java堆分为新生代和老年代。在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,因此可以选用复制算法。而老年代中的对象存活率高,没有额外空间对它进行分配担保,就必须使用标记-清理或者标记-整理算法来进行垃圾收集。
4.3 CMS和G1
CMS和G1的区别如下:
1.堆(Heap)空间分配不同
- CMS 将堆逻辑上分成Eden,Survivor(S0,S1),Old 三块,并且他们是固定大小,JVM启动的时候就已经设定不能改变,并且是连续的内存块。
- G1 将堆分成多个大小相同的Region(区域),默认2048个,在1Mb到32Mb之间大小,逻辑上仍然是Eden,Survivor,Old三块,但这三块不是固定大小,会根据每次GC的信息做出调整。
- CMS本质上采用标记-清除算法,因此会产生大量的内存碎片。而G1则采用标记-复制算法,不会产生内存碎片。并且CMS仅仅用于老年代的垃圾回收(新生代使用ParNew回收器,采用标记-复制算法),而G1则可以完成所有内存区域的垃圾回收。
Stop the World机制,简称STW,即在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。在垃圾回收器标记阶段,JVM会执行Stop the World操作。
CMS.PNG
[图片上传中...(G1_2.PNG-e0c368-1571715147359-0)]
G1_2.PNG
ZGC.PNG
参考文章:http://huzb.me/2019/02/21/CMS-G1%E5%92%8CZGC/
参考文章:https://www.cnblogs.com/littleLord/p/5380624.html#initialMark
5. Spring
5.1 IOC源码
Spring源码分析之IOC
5.2 AOP源码
Spring源码分析之AOP
5.3 循环依赖问题
循环依赖其实就是循环引用,也就是两个或则两个以上的 bean 互相持有对方,最终形成闭环。比如 A 依赖于 B,B 又依赖于A。在 Spring 中这样的场景有很多,比如构造器参数中的循环依赖(Spring无法解决,会抛出exception),属性注入时的循环依赖。而其中,只有单例对象的属性注入循环依赖是可以被解决的。
Spring主要通过三级缓存来解决属性注入的循环依赖问题:
/** Cache of singleton objects: bean name --> bean instance */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256); /** Cache of early singleton objects: bean name --> bean instance */ private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16); /** Cache of singleton factories: bean name --> ObjectFactory */ private final Map<String, ObjectFactory<?>> singletonFactories = new H
ashMap<String, ObjectFactory<?>>(16);
- 一级缓存
singletonObjects
存放所有已经实例化完成的Bean。 - 二级缓存
earlySingletonObjects
存放刚被创建但是属性还未被填充的Bean。 - 三级缓存
singletonFactories
并不存放Bean,它存放着创建Bean实例的ObjectFactory对象。
Spring 创建Bean实例的过程是:
- 首先实例化Bean对象
- 接着填充Bean实例的属性
- 调用BeanPostProcessor,其中包括4步:(1)调用Bean前置处理器
BeanPostProcessor.postProcessBeforeInitialization
(2)检测Bean是否实现了InitializingBean
并调用其afterPropertiesSet
方法(3)检测Bean是否配置了init-method并调用init-method(4)调用Bean后置处理器BeanPostProcessor.postProcessAfterInitialization
- Bean创建最终完成,并添加到一级缓存
singletonObjects
中
而对于刚实例化完成却还未填充属性的Bean,Spring会将其保存在二级缓存earlySingletonObjects
中,来解决循环依赖的问题。
protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 从 singletonObjects 获取 beanA 的实例,因为还没完全创建成功,所以获取不到 Object singletonObject = this.singletonObjects.get(beanName); // 判断 beanA 是否正在创建中,在第 4 步已经把 beanA 标记为正在创建 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { // 从 earlySingletonObjects 中获取提前曝光的 beanA,这里依旧没有 singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { // 从 singletonFactories 获取 beanA 的对象工厂,在第 5 步已经把 beanA 的对象工厂添加进去 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 通过对象工厂获取 beanA 的早期引用 singletonObject = singletonFactory.getObject(); // 将 beanA 的早期引用放入缓存 earlySingletonObjects 中 this.earlySingletonObjects.put(beanName, singletonObject); // 将 beanA 的对象工厂从缓存 singletonFactories 中移除 this.singletonFactories.remove(beanName); } } } } // 返回 beanA 的早期引用 return (singletonObject != NULL_OBJECT ? singletonObject : null); }
getSingleton
方法由doGetBean
方法调用,doGetBean
首次调用getSingleton
方法会返回null,因此doGetBean
方法会调用createBean
方法并将该Bean对应的ObjectFactory放入三级缓存singletonFactory
中。此时doGetBean
方法再次调用getSingleton
方法时,首先从一级缓存和二级缓存中查找对应Bean,查找不到再从三级缓存中查找,并调用三级缓存中保存的ObjectFactory.getObject
方法(内部本质上是实例化Bean的过程)获得Bean,然后将获得Bean加入二级缓存,并将对应Bean从三级缓存中移除。
对于prototype作用域Bean,Spring容器无法完成依赖注入,因为“prototype”作用域的Bean,Spring容
器不进行缓存,因此无法提前暴露一个创建中的Bean。
5.3 Spring MVC执行流程
- 用户发送请求至前端控制器DispatcherServlet
- DispatcherServlet收到请求调用HandlerMapping,HadnlerMapping是一个接口,主要用于根据url找到对应的Bean,其实现类包括RequestMappingHandlerMapping(用于处理@RequestMapping注解)和BeanNameUrlHandlerMapping(通过对比url和bean的name找到对应的对象)等等。
- 处理器映射器根据请求url找到具体的Handler,生成处理器执行链HandlerExecutionChain(包括处理器对象和处理器拦截器)一并返回给DispatcherServlet。
- DispatcherServlet根据Handler获取HandlerAdapter并执行HandlerAdapter处理一系列的操作,如:参数封装,数据格式转换,数据验证等操作。常见的HandlerAdapter包括RequestMappingHandlerAdapter(和上面的RequestMappingHandlerMapping配对使用,针对@RequestMapping)和SimpleControllerHandlerAdapter等等。
SimpleControllerHandlerAdapter的主要方法
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
会直接调用return ((Controller) handler).handleRequest(request, response)
,因此handlerAdapter通常在handler的基础上增加了一些额外的功能,如参数封装,数据格式转换,数据验证等。
- 执行处理器Handler(Controller,也叫页面控制器)。
- Handler执行完成返回ModelAndView
- HandlerAdapter将Handler执行结果ModelAndView返回到DispatcherServlet
- DispatcherServlet将ModelAndView传给ViewReslover视图解析器
- ViewReslover解析后返回具体View(Handler返回的ModelAndView中不包含真正的视图,只返回一 个逻辑视图名称,ViewResolver会把该逻辑视图名称解析为真正的视图View对象)
- DispatcherServlet对View进行渲染视图(即将模型数据model填充至视图中)。
- DispatcherServlet响应用户。
6. JVM监控和故障排查
- DK监控和故障处理工具总结
有道词典
Integer i = 10 ...
详细X
The Integer I = 10; / / packing Int j = I; / / split open a case
有道词典
public static D ...
详细X
公共静态双返回对象的值(双d) { 返回新双(d); }
有道词典
private void wr ...
详细X
私人空间writeObject (io。ObjectOutputStream s) 抛出java.io.IOException { / /写元素计数和任何隐藏的东西 int expectedModCount = modCount; s.defaultWriteObject (); / /写大小的行为能力与克隆的兼容性() s.writeInt(大小); / /所有元素以适当的顺序写出来。 for (int i = 0;我<大小;我+ +){ s.writeObject (elementData[我]); } 如果(modCount ! = expectedModCount) { 抛出ConcurrentModificationException ();