JEP 491虚拟线程和synchronizedJava24解决了虚拟线程同步使用问题 - 极道调度方式调度方式分为协作式调度和抢占式调度协作式调度：线程不让出执行权会导致其他线程饿死。抢占式调度：操作系统或运行时强制中断正在运行的线程，将 CPU 分配给其他线程。是否切断线程由调度器决定。即使你的 Java 代码没有调用 yield()，操作系统仍然可以在任何时刻抢占线程。在java中虚拟线程就是协作式调度，平台线程就是抢占式调度。Java 虚拟线程（Virtual Thread）只有在发生“可挂起（park）”的点时，才会与当前平台线程（carrier / platform thread）解绑，平台线程才有机会去执行其他虚拟线程。如阻塞，或者线程礼让。import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; public class Main { public static void main(String[] args) throws InterruptedException { // 使用虚拟线程创建一个固定数量的 carrier threads ExecutorService executor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual().factory()); //平台线程小于50 int busyThreads = 50; int newThreadIndex = 0; // 提交“死循环虚拟线程” for (int i = 0; i < busyThreads; i++) { int id = i; executor.submit(() -> { System.out.println("Busy virtual thread " + id + " started"); while (true) { // 纯 CPU 循环，不阻塞 } }); } // 等待一会儿，让死循环线程占满平台线程 Thread.sleep(1000); // 提交一个新虚拟线程尝试执行 executor.submit(() -> { System.out.println("New virtual thread " + newThreadIndex + " started"); }); // 注意：你可能看不到 New virtual thread 的打印 // 因为所有平台线程都被占满 Thread.sleep(10000); } } 上述发生的原因是java的协程是协作式调度的go1.13之前是协作式调度为主，1.14之后支持抢占式调度。现在 Go 采用的是混合调度策略，兼具两种调度的优势：运行时向 M（OS线程）发送信号实现强制抢占，防止 goroutine 长时间占用 CPUpackage main import ( "fmt" "time" ) func main() { busyThreads := 50 newThreadIndex := 0 // 启动"死循环"goroutine for i := 0; i < busyThreads; i++ { id := i go func() { fmt.Printf("Busy goroutine %d started\n", id) // 纯 CPU 循环，不阻塞 for { } }() } // 等待一会儿，让死循环 goroutine 开始执行 time.Sleep(2 * time.Second) // 提交新 goroutine go func() { fmt.Printf("New goroutine %d started\n", newThreadIndex) }() // 等待 10 秒 time.Sleep(10 * time.Second) } 因此go就不会出现这样的问题。但在1.13中就会有问题。java21和25都存在这个问题。背景简单来说就是synchronized阻塞的时候，无法切换到其他协程，导致当前平台线程被阻塞，不能运行其他虚拟线程。理论上可以使用ReentrantLock来解决虚拟线程是Java 21通过JEP 444引入的轻量级线程，由JDK而非操作系统提供。虚拟线程显著减少了开发、维护和观察高吞吐量并发应用程序的工作量，使应用程序能够使用大量线程。线程必须被_调度_才能做有用的工作，即被分配到处理器核心上执行。对于平台线程，JDK依赖于操作系统的调度器。对于虚拟线程，JDK有自己的调度器，它将虚拟线程分配给平台线程，然后由操作系统像往常一样调度。虚拟线程在执行阻塞操作（如I/O）时会卸载，当阻塞操作准备完成时，操作将虚拟线程重新提交给JDK的调度器，调度器将虚拟线程重新安装到平台线程上以继续运行代码。为何虚拟线程被固定到平台线程在Java中，虚拟线程被固定（pinned）到平台线程的原因主要与synchronized关键字的实现机制有关。虚拟线程在synchronized方法内部运行代码时无法卸载。考虑以下synchronized从套接字读取字节的方法：synchronized byte getData() { byte buf = ...; int nread = socket.getInputStream().read(buf); // Can block here ... }期望：如果该read方法没有实际字节二阻塞，我们希望正在运行虚拟线程getData从其载体上写着。这将释放一个平台线程，以便 JDK 的调度程序可以在其上安装不同的虚拟线程。实际：不幸的是由于getData是synchronized，JVM将正在运行的虚拟线程固定getData到其载体上。固定可防止虚拟线程卸载。因此，该read方法不仅会阻塞虚拟线程，还会阻塞其载体，从而阻塞底层操作系统线程，直到有字节供读取原因java中的synchronized关键字是基于监视器实现的，每个对象都与一个监视器关联，该监视器可以被获取（锁定）、持有一段时间，然后释放（解锁）。一次只能有一个线程持有对象的监视器。在JVM中，哪个线程持有对象紧绳器的信息是基于平台线程（即操作系统线程）来跟踪的，而不是基于虚拟线程。当虚拟线程执行synchronized实例方法并获取与实例关联的监视器时，JVM是虚拟线程的载体平台线程持有监视器，而不是虚拟线程本身。如果虚拟线程在synchronized方法中卸载，JDK的调度器可能会将另一个虚拟线程安装到刚刚释放的平台线程上。由于这个新虚拟线程的载体，JVM会认为它持有与实例关联的监视器，这将破坏互斥性，因此JVM积极组织虚拟现场在synchronized方法中卸载。如果虚拟线程在执行synchronized方法时调用了Object.wait(), 它会在JVM中阻塞知道被Object.notify()唤醒，并且载体重新获取监视器。由于它在synchronized方法中执行，以及它的载体在JVM中阻塞，所以虚拟线程也被固定危害频繁的固定（尤其是长时间固定）可能会损害可扩展性，导致饥饿甚至死锁，因为没有虚拟线程可以运行，因为所有平台线程要么被虚拟线程固定，要么在JVM中被阻塞。JEP 491旨在改变JVM对synchronized关键字的实现，允许虚拟线程在synchronized方法或语句中获取、持有和释放监视器，独立于它们的载体。这将允许虚拟线程在被阻塞时卸载，释放其载体平台线程给JDK调度器，从而提高应用程序的可扩展性通过允许在synchronized方法和语句中阻塞的虚拟线程释放其底层平台线程，供其他虚拟线程使用，从而提高Java代码在使用synchronized时的可扩展性。解决了虚拟线程在synchronized方法中不能卸载的问题，这个问题限制了能够用于处理应用程序工作负载的虚拟线程数量。同时，改进了诊断工具，以识别虚拟线程未能释放平台线程的情况，通过JDK Flight Recorder (JFR)记录jdk.VirtualThreadPinned事件来识别代码中的问题区域。由于synchronized关键字不再固定虚拟线程，因此不再需要jdk.tracePinnedThreads系统属性，该属性会在虚拟线程在synchronized方法中阻塞时打印堆栈跟踪，但可能会引起性能问题。解决了开发者在选择synchronized和java.util.concurrent.locks包中的API时的困惑，现在可以根据实际需要选择，而不必因为虚拟线程固定问题而被迫使用ReentrantLock。JEP 491提议重新实现synchronized关键字，允许虚拟线程获取、持有和释放监视器，而不需要将这些操作与它们的载体线程绑定，解决了虚拟线程与监视器交互的问题。本提案涉及对Object.wait()和Object.notify()机制的更改，允许虚拟线程在等待和重新获取监视器时挂起和恢复，而不会锁定其载体平台线程。本提案识别了一些虚拟线程仍然会固定的情况，例如在类初始化器内阻塞、等待其他线程初始化类以及在类加载期间解析符号引用时阻塞。
LongAdder实现原理Java并发工具类- LongAdder的实现原理过程 - 知乎 (zhihu.com)(4 封私信 / 80 条消息) 2020-10-22：谈谈java中LongAdder和LongAccumulator相同点和不同点？ - 知乎 (zhihu.com)Longadder的功能和atomicLong实际的作用其实是一样的，都是一个线程安全的元子类。其区别在于，AtomicLong的value是一个。而LongAdder将value分成了多个，分散了线程竞争，将本来对一个变量的多线程竞争，分散到多个变量上。AtomicLong中有个内部变量value保存着实际的long值，所有的操作都是针对该变量进行。也就是说，高并发环境下，value变量其实是一个热点，也就是N个线程竞争一个热点。LongAdder的基本思路就是分散热点，将value值分散到一个数组中，不同线程会命中到数组的不同槽中，各个线程只对自己槽中的那个值进行CAS操作，这样热点就被分散了，冲突的概率就小很多。如果要获取真正的long值，只要将各个槽中的变量值累加返回。组成先来看看LongAdder类中的重要属性继承了public class LongAdder extends Striped64 implements SerializableStriped64是什么？代表着64字节，即正好是一个CPU缓存行的大小。通常对intel来说，CPU读取内存的数据，会一次性读取64个字节，即正好是一个缓存行的大小，并且是一个原子性操作，不会有多个CPU同时读取这64个字节。Striped64类中的主要属性Ceel类Cell类上有一个@Contended的注解，这个注解的意思就是Cell代表着一个64字节的类@sun.misc.Contended static final class Cell { volatile long value; Cell(long x) { value = x; } final boolean cas(long cmp, long val) { return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); } // Unsafe mechanics private static final sun.misc.Unsafe UNSAFE; private static final long valueOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> ak = Cell.class; valueOffset = UNSAFE.objectFieldOffset (ak.getDeclaredField("value")); } catch (Exception e) { throw new Error(e); } } }base变量：当没有线程竞争时，首先会对base进行增加。Cells数组：当检测有base变量在被线程占有时，就会尝试去cell数组的变量加。总和为base变量+cells数组中的value /** * Table of cells. When non-null, size is a power of 2. */ transient volatile Cell[] cells; /** * Base value, used mainly when there is no contention, but also as * a fallback during table initialization races. Updated via CAS. */ transient volatile long base; ADD具体过程add方法/** * Adds the given value. * * @param x the value to add */ public void add(long x) { Cell[] as; long b, v; int m; Cell a; if ((as = cells) != null //当cells！=null，说明可能，那么就尝试对base变量的CAS || !casBase(b = base, b + x)) {//如果CAS base变量失败 就进入下面代码 boolean uncontended = true; //表示没有竞争 if (as == null || (m = as.length - 1) < 0 ||//情况1 cell数组中没有初始化 (a = as[getProbe() & m]) == null || // 情况2 cell数组中的cell对象没有初始化 !(uncontended = a.cas(v = a.value, v + x))) //情况3 CAS Cell对应的value值 //综上，满足上述三种情况之一，就会进入这个方法，方法详情见后续 longAccumulate(x, null, uncontended); } }我longAccumlate方法主要思想：不要让主线程在一个竞争店进行循环，让它去别的竞争点尝试如果抢占当前cell失败，那么尝试抢占Base变量；如果抢占Base变量失败，那么尝试从Cell数组中换一个Cell来抢占final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) { int h; if ((h = getProbe()) == 0) { ThreadLocalRandom.current(); // force initialization h = getProbe(); wasUncontended = true; } boolean collide = false; // True if last slot nonempty for (;;) { Cell[] as; Cell a; int n; long v; //对应情况二：cell数组不为空，但是数组中的cell对象为null if ((as = cells) != null && (n = as.length) > 0) { if ((a = as[(n - 1) & h]) == null) { if (cellsBusy == 0) { // Try to attach new Cell Cell r = new Cell(x); // Optimistically create if (cellsBusy == 0 && casCellsBusy()) { boolean created = false; try { // Recheck under lock Cell[] rs; int m, j; if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) { rs[j] = r; created = true; } } finally { cellsBusy = 0; } if (created) break; continue; // Slot is now non-empty } } collide = false; } else if (!wasUncontended) // 如果到了这里，可以理解为有线程在竞争 wasUncontended = true; // 那么将wasUncotended设置为true说明有竞争 else if (a.cas(v = a.value, ((fn == null) ? v + x : //选中cell对象不为空，对cell的value进行cas fn.applyAsLong(v, x)))) break; else if (n >= NCPU || cells != as) //将cell对象的value值CAS失败，就进入这里。如果数量大于CPU核心数，就没有必要进行扩容了或者 collide = false; // 将collide设置为false；再下一次循环时，就会进入下一行代码。 else if (!collide) collide = true; //将collide设置为true，下一次循环跳入到扩容代码 else if (cellsBusy == 0 && casCellsBusy()) { try { if (cells == as) { // 判断cell是否被其他线程改动 Cell[] rs = new Cell[n << 1]; //对Cell进行扩容 for (int i = 0; i < n; ++i) rs[i] = as[i]; cells = rs; } } finally { cellsBusy = 0; } collide = false; continue; // Retry with expanded table } h = advanceProbe(h); //换一个随机数 即换一个对象 } //对应第一种情况 Cells数组为空需要初始化 else if (cellsBusy == 0 && cells == as && casCellsBusy()) { boolean init = false; try { // Initialize table if (cells == as) { Cell[] rs = new Cell[2]; rs[h & 1] = new Cell(x); cells = rs; init = true; } } finally { cellsBusy = 0; } if (init) break; } //如果以上俩个都不满足，则进入尝试对base的add操作，因为可能之前操作base的线程已经操作完成了，所以可以尝试，不要让线程只等待在无意义的自旋上 else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; // Fall back on using base } }longAccumulateLongAccumulator是LongAdder的增强版。LongAdder只能针对数值的进行加减运算，而LongAccumulator提供了自定义的函数操作。其构造函数如下：public LongAccumulator(LongBinaryOperator accumulatorFunction, long identity) { this.function = accumulatorFunction; base = this.identity = identity; }通过LongBinaryOperator，可以自定义对入参的任意操作，并返回结果（LongBinaryOperator接收2个long作为参数，并返回1个long）。LongAccumulator内部原理和LongAdder几乎完全一样，都是利用了父类Striped64的longAccumulate方法。public class LongAccumulatorTest { public static void main(String[] args) throws InterruptedException { // 创建一个 LongAccumulator 对象，用于累加 x+y，初始值为 0 LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0); // 创建一个固定大小为 8 的线程池 ExecutorService executor = Executors.newFixedThreadPool(8); // 对整数范围从 1 到 4 进行遍历 IntStream.range(1, 5).forEach(i -> // 提交任务给线程池，每个任务对 accumulator 进行累加 executor.submit(() -> accumulator.accumulate(i))); // 休眠 2000 毫秒，等待所有提交的任务执行完成 Thread.sleep(2000); // 获取当前累加结果并重置累加器 System.out.println("累加结果："+accumulator.getThenReset()); } }
有栈协程与无栈协程浅谈有栈协程与无栈协程 - 知乎 (zhihu.com)相关Java 虚拟线程和 Kotlin 协程走的两个方向。Java 是有栈协程，协程上下文保存在与执行处独立的栈上，所以异步函数可以直接像正常函数那样写，程序执行到阻塞部分时 JVM 内部自动挂起；Kotlin 是无栈协程，因为干预不了 JVM，所以协程上下文必须保存在堆上，同时得显式声明函数是可挂起的，否则不知道什么函数支持挂起（suspend 在转译为字节码时会添加 Continuation 参数）。干预不了 JVM，既是 Kotlin 协程的优势，也是它的劣势。因为不需要 JVM 出手，这个特性可以在旧版本 JVM 上实现，也可以在不支持有栈协程的平台上实现，比如 WASM。又因为必须标记可挂起函数，Kotlin 协程也暴露出典型的函数染色问题。如今虽不敢说协程已经是红的发紫，但确实是越来越受到了大家的重视。Golang中的已经是只有goroutine，以至于很多go程序员是只知有协程，不知有线程了。就连C++也在最新的C++20中原生支持协程。更不用说很多活跃的语言如python，java等，也都是支持协程的。尽管这些协程可能名称不同，甚至用法也不同，但它们都可以被划分为两大类，一类是有(stackful) 协程，例如 goroutine，libco；一类是无栈 (stackless) 协程，例如C++的协程。这里我们想说的一点是所谓的有栈，无栈并不是说这个协程运行的时候有没有栈，而是说协程之间是否存在调用栈（callbackStack）。其实仔细一想即可，但凡是个正在运行的程序，不管你是协程也好，线程也好，怎么可能在运行的时候不使用栈空间呢，调用参数往哪搁，局部变量往哪搁。我们知道的所有主流语言在调用另外一个函数的时候都存在一个调用栈，我们来解释一下调用栈这个词。函数的栈帧指esp和ebp之间的一块地址。拿图上来说ebp存储着Frame Pointer指向的地址，Return Address当然就是我们执行完最新的栈帧以后下一步要执行的指令地址。esp当然就是当前指向栈顶的指针了。有栈协程函数运行在调用栈上，把函数作为一个协程，那么这个协程的上下文就是这个函数及其嵌套函数的（连续的）栈帧存储的值，以及此时寄存器存储的值。如果我们调度协程，也就是保存当前正在运行的协程上下文，然后回复下个将要运行的协程上下文。这样我们就轻松完成了协程的调度。并且因为保存的上下文和普通函数执行的上下文是一样的，所有协程可以在任意嵌套函数中挂起（无协程不行）。有栈协程的优点在易用性上，通常只需要调用对应的方法，就可以切换上下文挂起协程。在有栈协程调度时，需要频繁切换上下文，开销较大。单从实现上看，有栈协程更接近于内核级线程，都需要为每个线程保存单独上下文（寄存器、栈等），区别在于油站协程的调度由应用程序自行实现，对内核是透明的，而内核级线程的调度由系统内核完成，是抢占式的。列入Golang在进行协程调度时会保存栈寄存器和程序计数器（汇编支持，再进行调度）。很多地方又把协程称为subroutine，subroutine是什么，就是函数。上古时期的计算机科学家早就给出了概念，coroutinue就是可以中断并恢复执行的subroutine，从这个角度来看协程拥有调用栈并不是一个奇怪的事情。我们再来在思考coroutinue与subroutine相比有什么区别，你会发现区别仅有一个，就是coroutinue可以中断并恢复，对应的操作就是yield、resume，这样看来subroutinue只不过是coroutinue的一个子集罢了。也就是说把携程当做一个特殊的函数使用，有栈协程就是我们理想中协程该有的模样。既然把其当做一个特殊的函数去调用，对我们来说最严峻的挑战就是如何像切换函数一样去切换协程，难点在于除了像函数一样切换出去，还要在满足某种条件的时候切换回来，我们的做法可以是在协程内部存储自身的上下文，并在需要切换的时候把上下文切换就可以了，我们知道其实上下文本质就计算器，所以保存上下文实际上就是把寄存器的值保存下来，有俩种方法，一种是使用汇编，libco就使用了这种方法。另一种是ucontext.h，这个封装好的库也可以帮助我们完成相关工作。汇编的话我们来看一看libco中对于32位机器的上下文切换操作是如何完成的：// 获取第一个参数 movl 4(%esp), %eax // 参数的类型我们暂且理解为一个拥有八个指针的数组，即regs | regs[7] | | regs[6] | | regs[5] | | regs[4] | | regs[3] | | regs[2] | | regs[1] | | regs[0] | -------------- <---EAX movl %esp, 28(%eax) movl %ebp, 24(%eax) movl %esi, 20(%eax) movl %edi, 16(%eax) movl %edx, 12(%eax) movl %ecx, 8(%eax) movl %ebx, 4(%eax) // 想想看，这里eax加偏移不就是对应了regs中的值吗？这样就把所有寄存器中的值保存在了参数中 // ESP偏移八位就是第二个参数的偏移了，这样我们就可以把第二个参数regs中的上下文切换到寄存器中了 movl 8(%esp), %eax movl 4(%eax), %ebx movl 8(%eax), %ecx movl 12(%eax), %edx movl 16(%eax), %edi movl 20(%eax), %esi movl 24(%eax), %ebp movl 28(%eax), %esp ret // 这样我们就完成了一次协程的切换我们可以看到其实就是参数中传入两个协程的上下文结构，然后第一个参数执行保存上下文，然后把第二个参数的上下文存入寄存器，这样就执行了两个协程的切换。当然我们上面提到了调用栈，那么既然有调用栈，那么肯定有一个执行的顺序，即一定要把栈顶的协程全部运行完才可以运行下一层的协程，这样说可能比较抽象，我们举一个简单的例子：主协程A中执行协程B，此时调用栈是在[A，B]和[A]之间切换，因为B会主动让出执行权，然后调用栈上此时就只有一个A了B协程中执行C,D协程,此时调用栈是在[A，B，C]，[A，B]，[A，B，D]之间转换的，这样我们总是只能在调用栈顶的协程运行完成以后才能去执行更低一层的协程，当然，这也是典型的非对称协程，即协程之间有明显的调用关系。当然我的描述中也可以看出有栈协程设计到对于寄存器的保存和修改，也涉及到对每一个协程栈（实际运行）的分配。对于寄存器来说，线代寄存器基本都是上百个字节的数据，还有每一个协程的栈，如果选择了共享栈，又涉及到对栈上数据的拷贝，显然在效率上来说相比无栈协程的确是有一些损失的。无栈协程相比于有栈直接切换栈帧的思路，无栈写成在不改变函数调用栈的情况下，采用类似生成器generator的思路实现了上下文切换。通过编译器将生成器改写为对应的迭代器类型（内部其实就是一个状态机）。而无栈写成需要再编译器将代码便以为对应的状态机代码，挂起的位置在编译器确定。无栈写成的优点在性能上，不需要保存单独的上下文，内存占用低，切换成本低，性能高。缺点是需要编译器提供语法支持，无栈协程的实现是通过编译器对语法糖做支持，比如C#的yield return，aysncawait，编译器将这些带有关键字的方法编译为生成器，以及对应类型作为状态机。只有状态机支持才能进行协程调度，例如Rust的tokio，基于Future的用户态线程，根据poll方法获取Future状态，它不可以再任意嵌套函数中挂起（同步代码未实现状态机）。那么所谓的无栈协程是什么呢？其实无栈协程本质上就是一个状态机（state machine），它可以理解为在另一个角度去看问题，即同一协程的切换本质不过是指令寄存器的改变。这里推荐一篇文章，其内容是用C语言实现一个协程，其实就是一个无栈协程的实现。我们来看一个使用libco的协程的例子，当然libco是一个有栈协程：void* test(void* para){ co_enable_hook_sys(); int i = 0; poll(0, 0, 0. 1000); // 协程切换执行权，1000ms后返回 i++; poll(0, 0, 0. 1000); // 协程切换执行权，1000ms后返回 i--; return 0; } int main(){ stCoRoutine_t* routine; co_create(&routine, NULL, test, 0);// 创建一个协程 co_resume(routine); co_eventloop( co_get_epoll_ct(),0,0 ); return 0; }这段代码实际的意义就是主协程跑一个协程去执行test函数，在test中我们需要两次从协程中切换出去，这里对应了两个poll操作（hook机制，有兴趣的朋友可以点击这里），hook后的poll所做的事情就是把当前协程的CPU执行权切换到调用栈的上一层，并在超时或注册的fd就绪时返回（当然样例这里就只是超时了）。那么无栈协程跑相同的代码是怎么样的呢？其实就是翻译成类似于以下代码：struct test_coroutine { int i; int __state = 0; void MoveNext() { switch(__state) { case 0: return frist(); case 1: return second(); case 2: return third(); } } void frist() { i = 0; __state = 1; } void second() { i++; _state = 2; } void third() { i--; } };我们可以看到相比与有栈协程中的test函数，这里把整个协程抽象成一个类，以原本需要执行切换的语句处为界限，把函数划分为几个部分，并在某一个部分执行完以后进行状态转移，在下一次调用此函数的时候就会执行下一部分，这样的话我们就完全没有必要像有栈协程那样显式的执行上下文切换了，我们只需要一个简易的调度器来调度这些函数即可。从执行时栈的角度来看，其实所有的协程共用的都是一个栈，即系统栈，也就不比我们自行去给协程分配栈，因为是函数调用，我们当然也不必去显式保存寄存器的值，而且相比有栈协程把局部变量放在新开的空间上，无栈协程直接使用系统栈使得CPU cache局部性更好，同时也使得无栈协程的中断和函数返回几乎没有区别，这样也可以凸显出无栈协程的高效。对称协程非对称协程其实对于“对称”这个名词，阐述的实际是协程之间的关系，用大白话来说就是对称协程就是说协程之间人人平等，没有谁调用谁一说，大家都是一样的，而非对称协程就是协程之间存在明显的调用关系。go语言的协程就是对称协程，而腾讯libco提供的协程就是非对称协程。简单来说就是这样：非对称协程（asymmetric coroutines）是跟一个特定的调用者绑定的，协程让出CPU时，只能让回给调用者。何为非对称呢？在于协程的调用(call/resume)和返回(return/yield)的地位不对等。程序控制流转移到被调用者协程，而被调用者只能返回最初调用它的协程。对称协程（symmetric coroutines）在于协程调用和返回的地位是对等的。启动之后就跟启动之前的协程没有任何关系了。协程的切换操作，一般而言只有一个操作，yield，用于将程序控制流转移给其他的协程。对称协程机制一般需要一个调度器的支持，按一定调度算法去选择yield的目标协程。
Java ConcurrentSkipListMap线程安全的跳表实现——Java ConcurrentSkipListMap 原理解析 - 知乎 (zhihu.com)前言LevelDB、RockDB以及Pebble等高性能键值对存储引擎都采用SkipList来作为自己的内存数据结构。相比使用红黑树，SkipList具有以下优点数据结构比红黑树更简单具备更好的并发优势。红黑树在插入式可能涉及到整颗树的rebalance，而SkipList可以仅在某个局部进行操作，要锁定的节点更少，从而实现更好的性能。本文基于JAVA JUC中实现的ConcurrentSkipList，来介绍一种线程安全的SkipList的无锁实现跳表基础为了更好地了解 ConcurrentSkipListMap 的实现，我们将它的核心逻辑抽离出来，先理解它的跳表部分实现，再看它在并发访问上做了哪些优化。此处示例代码结构尽可能保持与 java.util.concurrent.ConcurrentSkipListMap 一致，仅对部分代码命名做出修改，以便后续对并发部分进行讲解，因此可能会包含一些不符合常见代码规范的部分。ConcurrentSkipListMap 整体的数据结构如下图所示：ConcurrentSkipList中定义了以下三种节点数据节点Node：实际保存数据的节点，包含Map的Key/Value，并使用next指针串成一个链表节点索引节点Index：用于快速查找，内部包含数据节点的应用。Right指针指向下一个索引节点，down指向下一层的索引节点头结点HeadIndex：Index类的子类，相比Index多了一个Level字段，标识当前的SkipList的层数，具体代码定义如下// 此处简化了一些，要求 Key 实现了 Comparable 接口。 // 实际的 ConcurrentSkipListMap 允许传入自定义的 Key Comparator。 @NotThreadSafe public class SkipListMap<K extends Comparable<K>, V> { private static class Node<K, V> { final K key; private Object value; private Node<K, V> next; ... } private static class Index<K, V> { Node<K, V> node; Index<K, V> down; Index<K, V> right; ... } private static class HeadIndex<K, V> extends Index<K, V> { int level; ... } }初始化在初始化时，会初始化跳表的头节点，形成如下图所示的数据结构对应代码如下private HeadIndex<K, V> head; private final static Object BASE_HEADER = new Object(); /** * 初始化数据节点：Key=null, Value=BASE_HEADER，初始化 level=1 * 其他指针全部初始化为 null */ public SkipListMap() { head = new HeadIndex<K, V>(new Node<K, V>(null, BASE_HEADER, null), null, null, 1); }SkipList的头结点不直接存储数据，仅作为跳表的头使用。插入数据的插入我们以最上面已经插入过 7 条数据的跳表为例。现有的数据 Key 分别为 {1, 3, 5, 7, 9, 11, 13}，我们要插入一条 Key 为 10 的记录。寻找插入位置，插入数据节点在寻找插入位时，需要先从head节点开始，找到离待插入位置最近的索引，逻辑如下向右查找：如果当前节点的right节点不为空且right节点的key小于插入数据的key则向右查找向下查找：如果不能向右查找（right位null或者right.node.key大于待插入的key），则向下或向右移动一层当无法向下或者向右移动时，返回该索引对应数据节点Node如上图所示，在访问到第3部的Index节点时，有由于它的右侧节点的值为11>10，无法向右侧移动；同时它的down节点为null，无法向下移动，因此返回它对应的数据节点，即key=7的节点（图中标红部分）注意：因为Index节点内部包含的是Node节点的引用，而不是使用down指针指向Node节点，因此在第3步对应的节点的down指针为null。对应代码如下public Node<K, V> findPredecessor(K key) { if (key == null) { throw new NullPointerException(); } Index<K, V> current = head, right = current.right; while (true) { // 向右查找 if (right != null) { Node<K, V> rightNode = right.node; // right.node.key 小于待插入key，继续向右寻找 if (key.compareTo(rightNode.key) > 0) { current = right; right = right.right; continue; } } // 不能继续向右寻找，向下移动一层 if (current.down == null) { // 不能继续向下寻找，说明到达第一层，可以直接返回 return current.node; } current = current.down; right = current.right; } }利用 Index 查找到 Key = 7 的节点数据后，继续在底层数据节点使用 next 指针向右寻找，直到找到实际插入位置的前驱节点，即 Key=9 的数据，并将其插入到链表中：这部分代码如下public void put(K key, V value) { Node<K, V> nodeToInsert = null; // 利用索引查找前驱节点 for (Node<K,V> prev = findPredecessor(key), next = prev.next;;) { int cmp = Integer.MIN_VALUE; // 在数据节点链表中移动到待插入位置 while (next != null && (cmp = key.compareTo(next.key)) > 0) { prev = next; next = next.next; } // 遇到相等的值，将其覆盖 if (cmp == 0) { next.value = value; return; } // 插入节点 nodeToInsert = new Node<>(key, value); nodeToInsert.next = next; prev.next = nodeToInsert; break; } ... }构建索引按照概率提升Level在这里的SkipList中，SkipList提升一层概率是1/2。java这里使用了一个比较聪明的办法：随机生成一恶搞32位的整形数，从这个数最低位开始向最高位遍历，如果这一位位1，则将SkipList提升一层。这样，这个SkipList的Level提升概率为1/2，且最大层数位31.在这个过程中，随机数只生成了一次，并且计算操作都是位运算，使得这个过程耗时较短。但这种做法也丧失了一定灵活性，比如无法配置SkipList的最大层数，并且1/2在实践中并不一定是是最佳的概率。比如一些键值数据库的内存SkipList就是使用自然对数1/e作为提升概率，而非1/2.提升概率低可能会使SkipList总体层数较少,影响查找性能;提升概率过高需要移动更多的指针节点,影响SkipList的写入性能(特别是在并发场景下,更多的指针自动意味着更多的冲突)此处插入节点 level 的计算逻辑如下：public void put(K key, V value) { ... // Java 的 Random 对象为了保证线程安全，使用了 CAS 等手段，可能会影响一定的性能，在实际的 // ConcurrentSkipListMap 中使用了线程独立的随机数生成 ThreadLocalRandom 来生成随机数 int rnd = random.nextInt(Integer.MAX_VALUE); // 0x80000001(最高位和最低位为1)，此处保证生成的数必须是正偶数 if ((rnd & 0x80000001) != 0) { return; } int level = 1; while (((rnd >>>= 1) & 1) != 0) { ++level; } ... }计算完成新的level值后情况分为俩种情况一level ≤ head.level，新的 level 小于 SkipList 的层数，只需要为插入节点建立 level 个 Index 索引即可，如下图所示，假设新计算的 level 值为 1：情况二level > head.level，新的 level 大于等于 SkipList 的层数，则需要更新 SkipList 整体的层数，具体操作为：为待插入节点建立 level 个 Index 索引，并将这些索引记录下来；为 head 节点上层 level - head.level 个 Index 索引，并将这些节点的 right 指针指向上一步建立的在同一层的 Index 节点（并且限制了每次只能将 SkipList 提升一层）；更新头节点；如下图所示，假设新的 level 大于等于 2，则新的 leve 值限制为 3：需要注意的是,虽然旧的head节点实际对应仍然是HeadIndex对象,但在之后的使用中都只会将其当成普通Index对象进行处理,因此此处将看做普通的Index节点.这部分对应的代码如下：public void put(K key, V value) { ... int maxLevel = head.level; Index<K, V> indexToInsert = null; // 第一种情况 if (level <= maxLevel) { for (int i = 0; i < level; ++i) { // 注意第二个参数，遍历 level 次，每次都向上创建一个 Index // new 的 Index 的 down 指针指向上一个 Index indexToInsert = new Index<>(nodeToInsert, indexToInsert, null); } } else { // 第二种情况 // 最多向上扩展一层 level = maxLevel + 1; Index<K, V>[] indexes = new Index[level+1]; for (int i = 1; i <= level; ++i) { indexToInsert = new Index<>(nodeToInsert, indexToInsert, null); // 记录新创建的Index，下标为该Index对应的层数 indexes[i] = indexToInsert; } HeadIndex<K, V> newHead = head; Node<K, V> oldBase = head.node; int oldLevel = head.level; for (int j = oldLevel+1; j <= level; ++j) { // head.right 指向该层新创建的 Index newHead = new HeadIndex<>(oldBase, newHead, indexes[j], j); } // 更新head指针 head = newHead; // 更新level为旧head对应的层数，用于后面right指针的构建 indexToInsert = indexes[level = oldLevel]; } ... }连接剩余的right指针再上面索引建立完成之后,可以看到新节点Key=10以及它左侧索引的right指针并没有指向正确的位置.如上图所示,因此我们还需要将索引指向正确的位置.具体做法就是从head开始,遍历每一层,找到新插入的Index节点的前驱节点,然后更新它的right指针,如下图:代码如下public void put(K key, V value) { ... // 注意 level > head.level 的情况下 // level 的值最后被更新为 oldHead.level indexToInsert = indexes[level = oldLevel]; } ... // 要构建right指针的层数 // 如果新节点level小于等于原来的level，则就在这个新节点的最高level开始构建； // 如果新节点level大于原本level的情况，则从旧head那一层开始插入 // 即：insertionLevel = min(oldHead.level, newNode.level) int insertionLevel = level; // 从 head.level 开始逐渐向下层遍历，找到需要构建right指针的位置 int currentLevel = head.level; for (Index<K, V> currentIndex = head, rightIndex = currentIndex.right, newIndex = indexToInsert;;) { // 向右移动到本次插入的节点，寻找新索引的前驱节点 if (rightIndex != null) { if (key.compareTo(rightIndex.node.key) > 0) { currentIndex = rightIndex; rightIndex = rightIndex.right; continue; } } // 到达要重建right指针的层数 if (currentLevel == insertionLevel) { currentIndex.right = newIndex; newIndex.right = rightIndex; // 这一层重建完成，将要重建的层数减一 insertionLevel--; newIndex = newIndex.down; // 重建完成，直接退出 if (insertionLevel == 0) { return; } } // 向下移动一层 currentLevel--; currentIndex = currentIndex.down; rightIndex = currentIndex.right; } }连接剩余的right指针在上面索引建立完成之后，可以看到新节点（Key=10）以及它左侧索引的 right 指针并没有指向正确的位置。如图所示,因此我们还需要指向正确的位置。具体的做法就是从head开始，遍历每一层，找到新插入的Index节点的前驱节点，然后更新它的right指针代码如下public void put(K key, V value) { ... // 注意 level > head.level 的情况下 // level 的值最后被更新为 oldHead.level indexToInsert = indexes[level = oldLevel]; } ... // 要构建right指针的层数 // 如果新节点level小于等于原来的level，则就在这个新节点的最高level开始构建； // 如果新节点level大于原本level的情况，则从旧head那一层开始插入 // 即：insertionLevel = min(oldHead.level, newNode.level) int insertionLevel = level; // 从 head.level 开始逐渐向下层遍历，找到需要构建right指针的位置 int currentLevel = head.level; for (Index<K, V> currentIndex = head, rightIndex = currentIndex.right, newIndex = indexToInsert;;) { // 向右移动到本次插入的节点，寻找新索引的前驱节点 if (rightIndex != null) { if (key.compareTo(rightIndex.node.key) > 0) { currentIndex = rightIndex; rightIndex = rightIndex.right; continue; } } // 到达要重建right指针的层数 if (currentLevel == insertionLevel) { currentIndex.right = newIndex; newIndex.right = rightIndex; // 这一层重建完成，将要重建的层数减一 insertionLevel--; newIndex = newIndex.down; // 重建完成，直接退出 if (insertionLevel == 0) { return; } } // 向下移动一层 currentLevel--; currentIndex = currentIndex.down; rightIndex = currentIndex.right; } }读取在实现了 put() 方法后，get() 方法的实现就比较简单了。直接调用 findPredecessor() 方法找到离待查找节点最近的索引，之后遍历进行查找即可，代码如下：public V get(K key) { // 寻找前驱节点 for (Node<K, V> prev = findPredecessor(key), next = prev.next;;) { if (next == null) { return null; } int compare = key.compareTo(next.key); if (compare == 0) { V result = (V) next.value; return result; } // 对应 key 不存在，直接退出 if (compare < 0) { break; } next = next.next; } return null; }删除删除也比较类似。 首先找到待删除的数据节点，移除数据节点：public V remove(K key) { for (Node<K, V> prev = findPredecessor(key), next = prev.next;;) { if (next == null) { return null; } Object value = next.value; int compare = key.compareTo(next.key); // 对应的 Key 不存在 if (compare < 0) { return null; } // 移动到要删除的节点位置 if (compare > 0) { prev = next; next = next.next; continue; } // 将节点标记为删除状态，便于后续删除对应的索引 next.value = null; // 删除 node 节点 prev.next = next.next; // 删除该数据节点对应的索引 cleanIndex(key); if (head.right == null) { reduceLevel(); } return (V) value; } }删除数据节点后，需要删除数据节点对应的索引。这里 SkipListMap 的做法是从 head 节点开始，逐个遍历索引，如果某个索引对应数据的 value 为 null，则删除这个索引：// 整体查找逻辑和findPredecessor()相同，只是添加了删除流程 // 在 ConcurrentSkipListMap 中实际上该逻辑就是由findPredecessor()方法实现的 // 这里为了作为区分单独实现了一个方法 private void cleanIndex(K key) { for (Index<K, V> currentIndex = head, rightIndex = currentIndex.right;;) { if (rightIndex != null) { Node<K, V> rightNode = rightIndex.node; if (rightNode.value == null) { currentIndex.right = rightIndex.right; rightIndex = currentIndex.right; continue; } if (key.compareTo(rightNode.key) > 0) { currentIndex = rightIndex; rightIndex = rightIndex.right; continue; } } if (currentIndex.down == null) { return; } currentIndex = currentIndex.down; rightIndex = currentIndex.right; } }在删除索引后，如果最上层已经没有索引了，需要降低跳表的层数，即将 head 向下移动一层：// ConcurrentSkipListMap 需要在最上面三层都为空的情况下，才会将整体 level 减少一层； // 这是为了最大程度地减少层数降低可能带来的数据丢失，具体参考后面线程安全部分的讲解 // 这里为了保证一致也采用相同的设计 private void reduceLevel() { HeadIndex<K, V> down; HeadIndex<K, V> down2Level; if (head.level > 3 && (down = (HeadIndex<K, V>) head.down) != null && (down2Level = (HeadIndex<K, V>) down.down) != null && down2Level.right == null && down.right == null && head.right == null) { head = down; } }并发问题对于上面的 SkipList，我们用 10 个线程并发写入 1 ~ 100 这 100 数字，最终得到的 SkipList 如下：// 索引节点（只展示索引对应的Key） level 5: 95 level 4: 51 60 73 95 level 3: 8 37 38 46 51 59 60 73 95 level 2: 0 0 5 8 14 16 17 26 31 33 ...... 63 66 68 73 81 95 96 level 1: 0 0 1 5 8 9 12 14 15 16 17 17 18 20 21 23 25 26 28 31 33 ...... 97 // 数据节点（展示Key和value） level 0: {0:0, 1:1, 2:2, 3:3, 4:4, 5:5}可以看到最终的输出结果完全不符合预期。 发生这个问题的主要原因是 SkipListMap 的 put() 方法并不是原子的。每次 put() 操作都需要对 Index、Node 等多个节点的指针进行移动操作，在移动时，可能被其他线程介入，导致之前的写入被覆盖，我们以下面这段代码为例：nodeToInsert = new Node<>(key, value); nodeToInsert.next = next; prev.next = nodeToInsert;我们暂时不考虑 Java 的指令重排序以及 CPU 缓存与内存的延迟同步问题，假设有下面两个线程按如下顺序执行：Thread 1Thread 2nodeToInsert = new Node() nodeToInsert = new Node()nodeToInsert.next = next nodeToInsert.next = nextprev.right=nodeToInsert prev.right=nodeToInsert可以看到前驱节点最终指向的是 Thread 2 创建的节点，而 Thread 1 的写入操作被覆盖了。除了以上并发写入的场景，在并发读写的场景下，读线程可能会读取到一些处于中间态的数据（比如调用了 remove() 方法但还没有完全删除的数据）。因此，我们上面实现的 SkipListMap 是线程不安全的。悲观锁悲观锁就是一种避免冲突的办法，我们同一时间只允许一个线程进入 put() 等方法，比如使用 Java 的 synchronized 关键字：CompletableFuture.supplyAsync(() -> { for (int j = 0; j < 100; j++) { synchronized (Main.class) { skipListMap.put(j, j); } } return null; });这种锁总是假设最坏的情况，认为每次自己拿数据的时候别人都会修改，所以共享资源每次只允许一个线程使用。但在使用上面这种悲观锁的情况下，同一时间只能有一个线程可以读取跳表的内容，会极大程度地影响跳表的性能。当然，我们也可以使用读写锁来改善并发读的性能（比如 Java 的 ReentrantReadWriteLock），这种锁允许多个线程同时进行读操作，但在并发读写的情况下，读写操作还是会被其他的写操作阻塞。还有一个最大的问题在于，调表这种数据结构在写入时修改存在一定的局部性。不同于红黑树等树状结构在插入时平衡操作可能涉及整个树，跳表在插入或删除时只涉及一小部分的指针变动。比如以下情况，我们向SkipList同事插入俩个节点：在上图中，我们用不同颜色标识除了插入时可能受到影响的节点，可以看到这俩个节点在插入时对彼此没有影响，在这种情况下，跳表是可以并发写入的。这也是RocksDB、Pebble等数据库选用跳表作为内存数据库的主要原因，跳表具有更好的并发性能。如果使用悲观锁，将无法有效的利用跳表这种特性。为了保证上面这种并发写入的特性，我们是否可以让每一个节点都持有一个悲观锁，同时只让一个线程对某个节点进行修改？或许这个方案可行，但是为每个节点都维持一个锁会造成很大的开销。这样每次写操作都涉及多次加锁和解锁操作，而加锁和解锁涉及内核态和用户态之间的转换，会造成较大的性能损失。因此，再Java的ConcurrentSkipListMap以及RocksDB等数据库中，使用乐观锁来解决上面提到的问题。乐观锁乐观锁和悲观锁相反，它认为在大多数情况下都不会出现冲突，它在修改数据前会进行冲突的检测，如果发生冲突的话就不修改数据。假设我们要将一个值为 a 的变量修改为 b，乐观锁的做法如下：获取并记录待修改的值 a；计算更新后的值 b；比较当前内存对应位置的值和 a 是否相等，如果相等则将其赋值为 b；否则不进行修改。现代 CPU 几乎都提供了指令来实现以上操作，我们称为 CAS（Compare And Swap 或 Compare And Set）。CAS 操作是原子的，因此 CAS 是一种常见的实现乐观锁的方法。让我们回到跳表的实现上，我们在修改跳表对应的指针时，就可以使用 CAS 算法，来保证自己的修改不会被其他线程篡改，比如在插入数据节点时，我们就可以使用下面的代码：while(true) { // 寻找到待插入位置 Node<K, V> prevNode = findPredecessor(key), nextNode = prevNode.next; // newNode 是新建的节点，不会存在冲突问题 Node<K, V> newNode = new Node<>(); newNode.next = nextNode; // 在插入时，如果prevNode.next还是nextNode，说明这个指针没被其他线程修改，不存在冲突 // 可以将next指针设置为newNode if (CompareAndSet(prevNode.next, nextNode, newNode) { break; } // 如果修改失败，返回到for循环开头重试修改 }可以看到，由于乐观锁并没有阻塞线程的执行，只是进行了冲突检测，因此在不存在冲突的情况下可以实现多个节点的鬓发写入，让跳表有更好的性能。同时，使用这种方式时，写入操作不会阻塞跳表的读取操作，再写入比较频繁的场景下，对系统的读读性能的影响也比较小。当然CAS实现乐观锁这种方式也存在一定的缺陷，比如：如果数据冲突比较严重，现成可能会不断重试，陷入空转，占用大量CPU资源；ABA问题总体来看，跳表数据这种数据结构具有较好的局部性，修改时只会涉及整个跳表中的一小部分数据，并发写入时发生冲突的概率较小，在数据量较大时尤其如此；而 ABA 问题不会影响跳表的功能正确性，因此跳表是一种非常适合使用 CAS 来实现无锁的数据结构。无锁跳表实现删除在ConcurrentSkipListMap中，删除操作比较特殊，除了使用CAS修改指针以外，它还是用了一些其他机制来防止出错。假设我们有b、n和f三个数据节点，形成链表b->n->f->g，如果我们仅仅使用CAS操作删除节点时 ，可能会发生以下问题场景一多线程并发删除假设线程1要删除节点n，线程2要删除节点f（f的后继节点为g），它们会各自调用CAS方法，如下：线程 1：CAS(b.next, n, f)线程 2：CAS(n.next, f, g)假设线程1先执行，线程2再执行，由于线程1的CAS操作并没有修改节点n的next指针，因此线程2的CAS操作也会成功，最终形成如下所试点链表最终得到的链表为 b -> f -> g，线程 2 的修改操作失败了。场景二:多线程并发写入假设线程 1 要删除节点 n，线程 2 要在节点 n 后面插入一个新节点 g，它们会各自调用以下 CAS 操作：线程 1：CAS(b.next, n, f)线程 2：CAS(n.next, f, g)，此处节点 g 在初始化时已经将 next 指针指向了 f我们还是假设线程 1 先执行（实际上它们的执行顺序不影响实际结果），由于线程 1 没有修改节点 n 的 next 指针，因此线程 2 的 CAS 操作也能执行成功，形成如下链表：最终形成链表遍历的结果为 b -> f，线程 2 插入的数据节点最终丢失了。根据上面俩种异常情况,可以知道删除操作除了需要保证前驱节点修改的并发安全以外,也要防止这个过程中待删除节点的next指针被修改,才能保证删除的正确性。为此ConcurrentSkipListMap采用插入maker节点的方式来防止其他线程修改待删除节点的next指针。ConcurrentSkipListMap的数据节点删除主要分为三步。使用 CAS 操作将 n 的 value 设置为 null，这样其他线程在使用 get() 方法获取节点 n 的值时，获取到的结果也为 null，调用方会认为这个值已经被删除；但是此时其他的写入或者删除操作可能会继续修改 n 的 next 指针；使用 CAS 操作使节点 n 的 next 指针指向一个新节点，我们称这个节点为 marker 节点。这个节点的 key 是 null，value 指向自身。存在 marker 节点时，其他任何一个线程想要通过 CAS 操作修改节点 n 的 next 指针时，都不可能成功（具体示例参考扩展阅读），从而防止在并发写入的情况下出现删除错误：最后使用 CAS 操作将 b 的 next 指针指向 f，之后 n 和 marker 节点会被 GC 自动回收。由于在删除的时候我们插入了一个 marker 节点，这个节点可能会影响其他节点的遍历、插入等操作。比如此时另一个线程想在节点 n 后面插入一个节点，它会调用 CompareAndSet(n.next, f, newNode)，但由于此时 n.next 的值为 marker 节点，就会 CAS 失败导致重试；并且由于 marker 节点的 key 为 null，它还会影响其他线程的遍历过程。为了避免这些问题，我们可以让其他线程帮忙删除对应的节点。当其他线程在遍历数据节点的过程中发现某个数据节点的 value 为 null，它就会帮忙执行删除流程。比如在 get() 方法中就存在这样一段代码：private V doGet(Object key) { Comparator<? super K> cmp = comparator; outer: for (;;) { for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; ... // 查找节点的过程中发现节点 n 的 value 为 null // 帮助执行删除流程 if ((v = n.value) == null) { // n is deleted n.helpDelete(b, f); break; } ... } } return null; }helpDelete() 方法的代码如下：// helpDelete 是数据节点内部的实现方法，这里的this就是上面的节点 n static final class Node<K,V> { ... void helpDelete(Node<K,V> b, Node<K,V> f) { // 在多线程情况下节点n的前驱或者后继节点可能又被修改，需要再次确认 if (f == this.next && this == b.next) { // 如果后继节点的value不等于自身，说明它不是marker节点 if (f == null || f.value != f) // 帮忙插入marker节点 casNext(f, new Node<K,V>(f)); else // marker节点已经插入，帮忙删除节点n b.casNext(this, f.next); } } ... }理解上面的流程后，ConcurrentSkipListMap 的 remove() 方法实现就很简单了，整体方法如下：final V doRemove(Object key, Object value) { if (key == null) throw new NullPointerException(); Comparator<? super K> cmp = comparator; // 两层循环 // "break;" 表示退出内部循环，进行重试 // "break outer;" 表示退出外层循环，结束方法 outer: for (;;) { for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; // 移动到链表结尾，直接退出 if (n == null) break outer; Node<K,V> f = n.next; // 再次检查，如果b.next被修改过，就进行重试 if (n != b.next) break; // 遇到被删除节点，帮忙删除，然后进行重试 if ((v = n.value) == null) { n.helpDelete(b, f); break; } // 检查前驱节点是否被删除，如果前驱节点被删除就进行重试 if (b.value == null || v == n) break; // key < n.key，已经越过要删除的节点，直接退出 if ((c = cpr(cmp, key, n.key)) < 0) break outer; // key > n.key，还没有到达删除位置，继续向右移动 if (c > 0) { b = n; n = f; continue; } // remove 方法中value参数恒为 null，此处可忽略 // ConcurrentSkipListMap 的 doRemove 方法还支持传值 // 节点的key、value和参数的key、value都相同才进行删除 // 主要是用于一些其他的map相关操作 if (value != null && !value.equals(v)) break outer; // 到达待删除节点，执行删除的第一步：将节点n的值设置为null作为标记 if (!n.casValue(v, null)) break; // 执行删除的第二和第三步，插入marker节点，并将b的next指针指向f if (!n.appendMarker(f) || !b.casNext(n, f)) // 如果失败了，使用 findNode 遍历链表，重新删除数据节点和对应的索引 findNode(key); // retry via findNode else { // 使用 findPredecessor 方法清除索引 findPredecessor(key, cmp); // clean index // 最顶层没有数据，尝试减少层数 if (head.right == null) tryReduceLevel(); } @SuppressWarnings("unchecked") V vv = (V)v; return vv; } } return null; }在上面的删除过程中，还需要介绍一下 findNode()、findPredecessor() 和 tryReduceLevel() 方法的实现。由于 findNode() 方法也调用了 findPredecessor() 方法，我们先介绍 findPredecessor() 方法。它的作用就是遍历索引，查找离 key 前驱索引对应的数据节点，并在遍历的过程中删除被标记删除节点的索引。实际功能和我们基础链表中的 cleanIndex() 方法相同，如下：private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) { if (key == null) throw new NullPointerException(); // don't postpone errors for (;;) { // 和基础链表相同，从上层逐渐向下查找到离key最近的索引对应的数据节点 for (Index<K,V> q = head, r = q.right, d;;) { if (r != null) { Node<K,V> n = r.node; K k = n.key; // 如果这个索引对应的数据节点被删除 if (n.value == null) { // 使用 CAS 操作将索引 r 移除，即q.next = r.next // 移除失败的话就进行重试 if (!q.unlink(r)) break; // restart // 移除成功，更新right索引位置，继续查找数据节点 r = q.right; // reread r continue; } // 后面为查找流程，和基础链表流程相同 if (cpr(cmp, key, k) > 0) { q = r; r = r.right; continue; } } if ((d = q.down) == null) return q.node; q = d; r = d.right; } } }findNode() 方法的主要效果是遍历跳表，查找某个 key 对应的数据节点。并且在这个过程中将标记被删除的数据节点删除：private Node<K,V> findNode(Object key) { if (key == null) throw new NullPointerException(); // don't postpone errors Comparator<? super K> cmp = comparator; outer: for (;;) { // 遍历节点 for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; if (n == null) break outer; Node<K,V> f = n.next; if (n != b.next) // inconsistent read break; // 如果某个数据节点被标记为删除，则将其删除 if ((v = n.value) == null) { // n is deleted n.helpDelete(b, f); break; } // 前驱节点被删除，则进行重试 if (b.value == null || v == n) // b is deleted break; // 它还实现了值的查找功能，提供给 Map 中的其他方法调用， // 但 `put()` 方法主要是用它来删除所有已经被删除的节点 if ((c = cpr(cmp, key, n.key)) == 0) return n; if (c < 0) break outer; b = n; n = f; } } return null; }然后是 tryReduceLevel 方法。减少跳表层数这个操作即使使用了 CAS 操作，也是非线程安全的。如果 head 节点在向下一层的过程中，其他线程向跳表插入了新的节点，并且在最上层构建了索引，在 head 向下移动之后这个索引就会丢失。虽然这样不会影响数据的正确性，但可能会影响跳表整体的性能。因此，为了降低这种问题发生的概率，ConcurrentSkipListMap 在跳表最上面 3 层都为空的情况下，才会尝试将整体层数减少一层。private void tryReduceLevel() { HeadIndex<K,V> h = head; HeadIndex<K,V> d; HeadIndex<K,V> e; if (h.level > 3 && (d = (HeadIndex<K,V>)h.down) != null && (e = (HeadIndex<K,V>)d.down) != null && e.right == null && d.right == null && h.right == null && // 使用CAS操作将跳表整体减少一层 casHead(h, d) && // try to set // 减少一层之后发现这一层已经存在数据 h.right != null) // recheck // 恢复到开始状态 casHead(d, h); // try to backout }插入ConcurrentSkipListMap 的插入也和我们实现的基础链表一样，插入操作可以分为以下三步： 1. 遍历寻找插入位置，插入数据节点； 2. 按照概率提升层数，为新的数据节点构建索引； 3. 连接剩余的 right 指针。插入数据节点private V doPut(K key, V value, boolean onlyIfAbsent) { Node<K,V> z; // added node if (key == null) throw new NullPointerException(); Comparator<? super K> cmp = comparator; // 两层循环，分别用来重试和结束操作 outer: for (;;) { // 使用findPredecessor查找前驱节点，顺便删除需要被删除的索引 for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { if (n != null) { Object v; int c; Node<K,V> f = n.next; if (n != b.next) // inconsistent read break; // 遍历过程中遇到已经被删除的节点，则帮助插入marker节点或删除该数据节点 if ((v = n.value) == null) { // n is deleted n.helpDelete(b, f); break; } // 前驱节点已经被删除，进行重试 if (b.value == null || v == n) // b is deleted break; // 还未移动到待插入位置，继续向右移动 if ((c = cpr(cmp, key, n.key)) > 0) { b = n; n = f; continue; } // 存在重复值，仅将其值覆盖 if (c == 0) { if (onlyIfAbsent || n.casValue(v, value)) { @SuppressWarnings("unchecked") V vv = (V)v; return vv; } // 如果覆盖值失败，进行重试 break; // restart if lost race to replace value } // else c < 0; fall through } // 创建新数据节点 z = new Node<K,V>(key, value, n); // 将其插入到节点b和n之间，如果插入失败就进行重试 if (!b.casNext(n, z)) break; // restart if lost race to append to b break outer; } // 内层for循环结束 } // 外层for循环结束 ... return null; }可以看到插入数据节点的操作和基础链表基本相同，只是涉及修改操作时使用了 CAS 操作，并且在遍历查找的过程中加入了一致性检查和帮助删除的操作。一致性检查指插入之前需要检查对应的前驱和后继节点的状态是否符合预期，包括：插入之前前驱节点 b 的 next 指针必须指向后继节点 n前驱节点 b 必须没有被删除。如果数据不一致可能导致新插入的数据丢失构建索引构建索引的过程和普通 SkipList 基本类似，只是将修改操作换成了 CAS，并添加了一部分一致性检测，读者可以自行阅读以下代码：// 使用 ThreadLocalRandom 提升随机数性能 int rnd = ThreadLocalRandom.nextSecondarySeed(); if ((rnd & 0x80000001) == 0) { // test highest and lowest bits int level = 1, max; while (((rnd >>>= 1) & 1) != 0) ++level; Index<K,V> idx = null; HeadIndex<K,V> h = head; if (level <= (max = h.level)) { for (int i = 1; i <= level; ++i) idx = new Index<K,V>(z, idx, null); } else { // try to grow by one level level = max + 1; // hold in array and later pick the one to use @SuppressWarnings("unchecked")Index<K,V>[] idxs = (Index<K,V>[])new Index<?,?>[level+1]; for (int i = 1; i <= level; ++i) idxs[i] = idx = new Index<K,V>(z, idx, null); for (;;) { h = head; int oldLevel = h.level; // 一致性检测，其他节点的插入导致SKipList层数已经增长过了 // 则放弃更新头节点 if (level <= oldLevel) // lost race to add level break; HeadIndex<K,V> newh = h; Node<K,V> oldbase = h.node; for (int j = oldLevel+1; j <= level; ++j) newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j); if (casHead(h, newh)) { h = newh; idx = idxs[level = oldLevel]; break; } } } ... }连接剩余的right指针和基础 SkipList 实现一样，这里需要从 head 节点所在的层数开始，遍历每一层，查找新插入节点的前驱节点。在并发条件下，头节点 head 可能随时发生变化，因此需要用局部变量 h 保存本次插入时对应的头结点，从这个节点开始遍历（在上一步构建索引的时候已将对应值更新到局部变量 h）// 从新插入节点的最高层开始重建 right 指针 splice: for (int insertionLevel = level;;) { // 头结点的层数 int j = h.level; // t 为本次新插入的最上层索引 for (Index<K,V> q = h, r = q.right, t = idx;;) { // 遍历到跳表的末尾 if (q == null || t == null) break splice; if (r != null) { Node<K,V> n = r.node; // compare before deletion check avoids needing recheck int c = cpr(cmp, key, n.key); // 删除检查，如果某个节点已经被标记为删除，则帮助删除 if (n.value == null) { // 删除失败，重试 if (!q.unlink(r)) break; // 继续遍历 r = q.right; continue; } // 遍历向右移动 if (c > 0) { q = r; r = r.right; continue; } } ... }逐层向下遍历，直到到达 insertionLevel 时，就可以开始连接 right 指针了：// 到达要连接right指针的层数 if (j == insertionLevel) { // 连接right指针，失败就进行重试 if (!q.link(r, t)) break; // restart // 如果这次新插入的节点被另一个线程删除，则使用 findNode 方法将其删除，并停止插入 if (t.node.value == null) { findNode(key); break splice; } // 到达最底层，结束操作 if (--insertionLevel == 0) break splice; } // right指针连接成功，连接下一层，将 t 指针向下移动 if (--j >= insertionLevel && j < level) t = t.down; q = q.down; r = q.right; }可以看到在连接 right 指针的过程中，可能存在本次插入的节点被另一个线程删除的情况，此时我们会调用 findNode() 方法遍历整个跳表，将所有被标记为删除的节点删除。查找查找方法的主要逻辑和基础跳表类似，并且逻辑和上面的 findNode() 逻辑基本相同，读者可以自行阅读以下代码：private V doGet(Object key) { if (key == null) throw new NullPointerException(); Comparator<? super K> cmp = comparator; outer: for (;;) { for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; if (n == null) break outer; Node<K,V> f = n.next; if (n != b.next) // inconsistent read break; if ((v = n.value) == null) { // n is deleted n.helpDelete(b, f); break; } if (b.value == null || v == n) // b is deleted break; if ((c = cpr(cmp, key, n.key)) == 0) { @SuppressWarnings("unchecked") V vv = (V)v; return vv; } if (c < 0) break outer; b = n; n = f; } } return null; }总结ConcurrentSkipListMap 的核心机制就是 CAS 操作。当它在试图修改某个节点的指针时，会使用局部变量保存该指针的原始状态，并利用 CAS 操作进行修改，如果修改失败，则使用 for 循环进行重试。对于删除这种较为危险的操作，ConcurrentSkipListMap 会使用标记的方式，先阻止其他线程对待删除节点的进一步操作，再对该节点进行删除，从而降低删除的危险性。通过标记的方式，也可以让其他线程在操作时帮助进行删除，避免阻塞其他操作，进一步提升写性能。整体来看，CAS 操作相比互斥锁开销较低，并且能推动开发者更细粒度地对临界区进行控制，减少对其他线程的阻塞，但由于它使用 for 循环进行重试操作，有可能占用一定的 CPU 资源。因此 CAS 操作更适用于使用多核 CPU，并且临界区较小（并发操作共享变量的事件较短）的场景。当然，这些原则都不是绝对的，在实际业务场景中，还是需要通过详细的性能和压力测试选择最适合的锁机制。扩展阅读使用 marker 节点后的并发写分析对于删除这一章节我们分析到的异常情况，假如我们使用了 marker 节点，是否还会出现类似问题呢？ 并发删除： 对于链表 b -> n -> f -> g，线程 1 要删除节点 n，线程 2 要删除节点 f。假设它们按照如下顺序执行删除操作：线程 1线程 2CAS(n.value, valueN, null) CAS(f.value, valueF, null) CAS(n.next, f, markerN) CAS(f.next, g, markerF)CAS(b.next, n, f) CAS(n.next, f, g)当线程 2 在执行 CAS(n.next, f, g) 操作修改节点 n 的 next 指针时，线程 1 已经将它的 next 指针改为了 markerN 节点，因此这个 CAS 操作会失败。失败后线程 2 会重新返回方法入口，重新进行删除流程。由于此时节点 f 已经被标记为删除状态，它可能被线程 2 或其他线程调用 helpDelete() 方法删除。对于其他异常情况，读者可以自行分析它的正确性。无锁链表的其他删除节点实现方式标志位在上面的分析中，我们知道引入 marker 节点的主要作用是标记待删除节点的 next 指针，防止它被其他线程篡改。因此除了使用 marker 节点以外，也可以使用单独的标志位来标识 next 指针不能被篡改。 使用标志位这种方法可能需要更多的空间，并且在写入时不仅需要操作节点指针，还需要对标志位进行判断，会引入额外的复杂度。DCAS除了标志位这种方法以外，我们还有另一个思路。既然 CAS 能原子地实现先比较再修改的功能，我们是否也可以有另一个原子操作，保证在前驱节点 b 和待删除指针 n 都没被修改过的情况下，再执行删除操作。在实际应用中，我们将这种操作称为 DCAS（Double Compare And Swap）。 假设我们使用 DCAS 来对链表 b -> n -> f 中的节点 n 执行删除操作，对应的 DCAS 操作如下：DCAS(b.next, n.next, n, f, f);它对应的含义是：当节点 b 的 next 指针指向节点 n，且节点 n 的 next 指针指向节点 f 时，将节点 b 和节点 n 的 next 指针对应的内存空间指向 f。需要注意的是，DCAS 是直接对内存空间进行操作，此时节点 b 和节点 n 的 next 指向同一个内存空间，再使用 DCAS 进行修改时，它们会同时被修改。就可以防止上面的异常情况。我们还可以分析一下 DCAS 是如何避免并发删除导致的丢失问题的。但由于当前主流 CPU 并没有广泛支持 DCAS 操作，并且用 DCAS 操作实现无锁数据结构也不一定比单纯使用 CAS 操作要更加简单，在论文 DCAS is not a silver bullet for nonblocking algorithm design 一文中有更详细的阐述。参考资料Optimistic vs. Pessimistic Locking - Vlad MihalceaJava ConcurrentSkipListMap跳跃表源码深度解析Deletion problem in a lock-free linked listValois, John D. "Lock-free linked lists using compare-and-swap." Proceedings of the fourteenth annual ACM symposium on Principles of distributed computing. 1995.Compare-and-swap - Wikipedia
Apache CalciteApache Calcite 为什么能这么流行 - 知乎 (zhihu.com)简介途中是使用apache calcite或至少关联的项目。那 Apache Calcite 究竟是干嘛的，又为什么能这么流行呢？首先，摆一个应该没多少人会反对的共识：SQL 是编程领域最流行的语言。有 MySQL、Oracle 之类使用 SQL 作为交互语言的数据库然后有 JDBC、ODBC 之类和各种数据库交互的标准接口有大量数据科学家和数据分析师等不太会编程语言但又要使用数据的人第一代大数据计算引擎 MapReduce 被 Hive SQL 很大程度上替代新一代大数据计算引擎 Spark 很快就推出了 Spark SQL最近几年大热的流处理引擎 Flink 很快也推出了 Flink SQL......比如要给MongoDB套上一个SQL的壳子，或者想要直接用SQL查一堆CSV文件，可怕没多少人能顺利实现。Apache Calcite的出现，让你能够很容易的给你的系统套上一个SQL的壳子，并且提供足够高效的查询性能优化。足够简答和focus的定位查询语言查询优化查询执行数据管理数据存储通常我们可以把数据库管理系统分为上面五个组件。Calcite在设计之初九确定了自己只关注和实现图中上面三个部分，而把数据管理和数据存储留给了外部系统、计算引擎。通常数据管理和数据存储，尤其是后者是很复杂的，也会由于数据本身的特性导致是线上的多样性。Calcite抛弃了这俩部分，而是专注于上层更加通用的模块。另一方面，Calcite 没有去重复造各种轮子，在该用现成的东西的时候，就直接用能找到的最合适的。这是个非常好的习惯，但也是非常容易被程序员抵触的方法。比如，作为一个SQL解决方案，关键的SQL解析这一步，Calcite没有选择造轮子，而是直接使用了开源JavaCC，来将SQL语句转换为JAVA代码，然后转换成一颗AST供下一阶段使用。javaCC全称为Java Compiler compiler，它是一个生成器，用于生成词法分析和语法分析器。它可以通过读取一个词法和语法描述文件，来生成一个java程序，这个java程序就包括了词法分析器和语法分析器。接着就可以用生成的词法分析器和语法分析器来对我们的输入进行判断，判断是否符合我们要求的语法规则JAVACC使用 - 知乎 (zhihu.com)所以总的来说JAVACC其实就是一个解释器的生成器，它的关键在于识别模板文件来生成分析程序。另外一个例子，为了支持后面会提到的灵活元数据功能，Calcite需要支持运行时编译java代码。默认的javaC太重，需要一个更轻量级的编译器，Calcite同样没有选择重复造轮子，而是使用了开源的Janino方案。足够简单和focus定位，不重复造轮子，使得Calcite的实现足够简单和稳定。当apache Spark、Flink、Presto为了加速数据处理速度，而使用代码生成技术时，都不约而同的选择了这个开源库JaninoJanino：轻巧的Java动态编译器 - 知乎 (zhihu.com)Jaino主要有三大功能Parser：java代码的解析器。可以解析完整的模块函数、或者是表达式等UnParser是Parser的逆操作。也就是把抽象语法树（AST）变为Java程序。Evaluator：最后，到了用的最多的Evaluator执行环节了。比如：为了加速表达式的执行，那我可以先使用Janino来编译表达式为JVM的bytecode，并在后续多次调用这个编译好的表达式来计算。架构灵活可插拔的架构上面的图是 Calcite 官方给出的架构图。一方面印证了我们上面提到的，Calcite 足够简单，没有做自己不该做的事；另一方面，也是更重要的，Calcite 被设计的足够模块化和可插拔。JDBC Driver 这个模块用来支持使用 JDBC client 的应用；SQL Parser and Validator 模块用来做 SQL 解析和校验；Expressions Builder 用来支持自己做 SQL 解析和校验的框架对接；Operator Expressions 这个模块用来处理关系表达式；Metadata Providers 用来支持外部自定义元数据；Pluggable Rules 用来定义优化规则；最核心的 Query Optimizer 则专注查询优化。功能模块的划分足够合理，足够独立，使得不用完整集成，而是可以只选择其中的一部分使用；而基本上每个模块都支持自定义，也使得用户能更多地定制系统。上图展示里 10多种框架对Calcite的集成情况。我们看一看到像Hive就做了SQL解析，只使用了Calcite的查询优化功能；而Flink则解析到优化都使用了CalciteFlink 对 Calcite 的使用，从这个 Flink 的架构图看的会更清楚。Flink 提供了 Table API 和 SQL API 两种形式来支持对格式化数据的处理。 SQL API 通过 Calite 的 SQL Parser 和 Validator 转成逻辑执行计划，而Table API 直接转换成 Calcite 的逻辑执行计划。二者在这里达到统一，再通过 Calcite 做优化，完了之后再转成对应的物理执行计划，Table API 对应对 DataSet 的操作，SQL API 对应对 DataStream 的操作。以上说的集成方法，都是把 Calcite 的模块当做库使用。如果觉得太重量级，可以选择更简单的 Adapter的方式。通过类似 Spark/Flume 这些框架里自定义 Source/Sink 的方式，来实现和外部系统的数据交互操作。上图列的就是集中典型的 Adapter，比如通过 MongoDB 的 Adapter 就能直接在应用层通过 SQL，而底层自动转换成 Java 和 MongoDB 完成交互。当然如果社区没有现成的实现，也很容易自己按照 API 规范去实现一个。对多种异构数据源的支持除了对标准 SQL 的支持，Calcite 还支持各种丰富的数据源。比如MongoDB和流数据查询优化提到查询优化，不得不提关系代数（Relational Algebra）和关系表达式（Relational Expression），这也是 Calcite 的核心概念之一。我们看一个例子，上图的 SQL 语句通过 SQL 解析和验证后，会先被转换成下面这样的关系表达式：很简单，就是先对两个表做 scan，完了再用 id 字段做 join，再做一个 filter，最后 project 挑出想要的字段。如果要用一句话解释关系代数，就是定义了什么样的关系表达式是等价的。上面的关系表达式会被 Calcite 通过关系代数转换成下面这样：相信大家都看的懂，通过 push down filter 和 push down project，大大减少了数据量，也就是减少了IO，而 IO 通常都是性能瓶颈，自然就大大提升了性能。这样提升性能的等价关系表达式转换，就是查询优化。而类似 push down filter 这样的转换就叫做 planner rule。类似这样的 rule 还有很多，不一一罗列了。知道了怎么优化，怎么样算优化完了呢，不可能一直这么转换下去吧？当然不会，决定什么时候停止优化的是 Planner Engine。目前 Calcite 支持两种 Planner Engine：HepPlanner，优化到关系表达式不再变化为止。VolcanoPlanner，优化到查询成本不再明显变化为止。很显然，HepPlanner 更表象点，不一定就能得到最优解，但优点是速度快；而 VolcanoPlanner 则更准确些，有 cost 来量化做比较，缺点自然是稍慢。用户可以自由选择用哪一种，比如 Hive 就用的 VolcanoPlanner 来做 CBO。接着很自然能想到的问题，就是怎么计算 cost 了。而这里又体现 Calcite 的灵活性了，除了提供默认的 cost function，还允许前面提到的 metadata provider提供自定义的 cost function。通常会被用来计算 cost 的有行数、空间大小、笛卡尔积大小等等，这里不过多展开。
sleep、synchronized、LockSupport的线程阻塞有何区别？让面试官心服口服：Thread.sleep、synchronized、LockSupport.park的线程阻塞有何区别？ - tera - 博客园 (cnblogs.com)前言在日常编码的过程中，我们经常会使用Thread.sleep、LockSupport.park()主动阻塞线程，或者使用synchronized和Object.wait来阻塞线程保证并发安全。此时我们会发现，对于Thread.sleep和Object.wait方法是会抛出InterruptedException，而LockSupport.park()和synchronized则不会。而当我们调用Thread.interrupt方法时，除了synchronized，其他线程阻塞的方式都会被唤醒。于是本文就来探究一下Thread.sleep、LockSupport.park()、synchronized和Object.wait的线程阻塞的原理以及InterruptedException的本质本文主要分为以下几个部分1.Thread.sleep的原理2.LockSupport.park()的原理3.synchronized线程阻塞的原理4.ParkEvent和parker对象的原理5.Thread.interrupt的原理6.对于synchronized打断原理的扩展1.Thread.sleep的原理Thread.java首先还是从java入手，查看sleep方法，可以发现它直接就是一个native方法：public static native void sleep(long millis) throws InterruptedException;为了查看native方法的具体逻辑，我们就需要下载openjdk和hotspot的源码了，下载地址：http://hg.openjdk.java.net/jdk8查看Thread.c：jdk源码目录src/java.base/share/native/libjava可以看到对应的jvm方法是JVM_Sleep：static JNINativeMethod methods[] = { ... {"sleep", "(J)V", (void *)&JVM_Sleep}, ... };查看jvm.cpp，hotspot目录src/share/vm/prims找到JVM_Sleep方法，我们关注其重点逻辑：方法的逻辑中，首先会做2个校验，分别是睡眠时间和线程的打断标记。其实这2个数据的校验都是可以放到java层，不过jvm的设计者将其放到了jvm的逻辑中去判断。如果睡眠的时间为0，那么会调用系统级别的睡眠方法os::sleep()，睡眠时间为最小时间间隔。在睡眠之前会保存线程当前的状态，并将其设置为SLEEPING。在睡眠结束之后恢复线程状态。接着就是sleep方法的重点，如果睡眠时间不为0，同样需要保存和恢复线程的状态，并调用系统级别的睡眠方法os::sleep()。当然睡眠的时间会变成指定的毫秒数。最重要的区别是，此时会判断os::sleep()的返回值，如果是打断状态，那么就会抛出一个InterruptException！这里其实就是InterruptException产生的源头JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis)) JVMWrapper("JVM_Sleep"); //如果睡眠的时间小于0，则抛出异常。这里数据的校验在jvm层逻辑中校验 if (millis < 0) { THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative"); } //如果线程已经被打断了，那么也抛出异常 if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) { THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted"); } ... //这里允许睡眠时间为0 if (millis == 0) { ...{ //获取并保存线程的旧状态 ThreadState old_state = thread->osthread()->get_state(); //将线程的状态设置为SLEEPING thread->osthread()->set_state(SLEEPING); //调用系统级别的sleep方法，此时只会睡眠最小时间间隔 os::sleep(thread, MinSleepInterval, false); //恢复线程的状态 thread->osthread()->set_state(old_state); } } else { //获取并保存线程的旧状态 ThreadState old_state = thread->osthread()->get_state(); //将线程的状态设置为SLEEPING thread->osthread()->set_state(SLEEPING); //睡眠指定的毫秒数，并判断返回值 if (os::sleep(thread, millis, true) == OS_INTRPT) { ... //抛出InterruptedException异常 THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted"); } //恢复线程的状态 thread->osthread()->set_state(old_state); } JVM_END查看os_posix.cpp，hotspot目录src/os/posix/vm我们接着查看os::sleep()方法：首先获取线程的SleepEvent对象，这个是线程睡眠的关键根据是否允许打断分为2个大分支，其中逻辑大部分是相同的，区别在于允许打断的分支中会在循环中额外判断打断标记，如果打断标记为true，则返回打断状态，并在外层方法中抛出InterruptedException最终线程睡眠是调用SleepEvent对象的park方法完成的，该对象内部的原理后面统一说int os::sleep(Thread* thread, jlong millis, bool interruptible) { //获取thread中的_SleepEvent对象 ParkEvent * const slp = thread->_SleepEvent ; ... //如果是允许被打断 if (interruptible) { //记录下当前时间戳，这是时间比较的基准 jlong prevtime = javaTimeNanos(); for (;;) { //检查打断标记，如果打断标记为ture，则直接返回 if (os::is_interrupted(thread, true)) { return OS_INTRPT; } //线程被唤醒后的当前时间戳 jlong newtime = javaTimeNanos(); //睡眠毫秒数减去当前已经经过的毫秒数 millis -= (newtime - prevtime) / NANOSECS_PER_MILLISEC; //如果小于0，那么说明已经睡眠了足够多的时间，直接返回 if (millis <= 0) { return OS_OK; } //更新基准时间 prevtime = newtime; //调用_SleepEvent对象的park方法，阻塞线程 slp->park(millis); } } else { //如果不能打断，除了不再返回OS_INTRPT以外，逻辑是完全相同的 for (;;) { ... slp->park(millis); ... } return OS_OK ; } }所以Thread.sleep的在jvm层面上是调用thread中SleepEvent对象的park()方法实现阻塞线程，在此过程中会通过判断时间戳来决定线程的睡眠时间是否达到了指定的毫秒。而InterruptedException的本质是一个jvm级别对打断标记的判断，并且jvm也提供了不可打断的sleep逻辑。2.LockSupport.park()的原理除了我们经常使用的Thread.sleep，在jdk中还有很多时候需要阻塞线程时使用的是LockSupport.park()方法（例如ReentrantLock），接下去我们同样需要看下LockSupport.park()的底层实现LockSupport.java从java代码入手，查看LockSupport.park()方法，可以看到它直接调用了Usafe类中的park方法：public static void park() { UNSAFE.park(false, 0L); }Unsafe.java查看Unsafe.park，可以看到是一个native方法public native void park(boolean var1, long var2);查看unsafe.cpp，hotspot目录src/share/vm/prims找到park方法，这个方法就比sleep简单粗暴多了，直接调用thread中的parker对象的park()方法阻塞线程UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time)) { ... //简单粗暴，直接调用thread中的parker对象的park方法阻塞线程 thread->parker()->park(isAbsolute != 0, time); ... } UNSAFE_END所以LockSupport.park方法是不会抛出InterruptedException异常的。当一个线程调用LockSupport.park阻塞后，如果被唤醒，那么就直接执行之后的逻辑。而对于打断的响应则需要使用该方法的用户在Java级别的代码上通过调用Thread.interrupted()判断打断标记自行处理。相比而言Thread.sleep则设计更为复杂，除了在jvm级别上对打断作出响应，更提供了不可被打断的逻辑，保证调用该方法的线程一定可以阻塞指定的时间，而这个功能是LockSupport.park所做不到的。3.synchronized线程阻塞的原理再看一下synchronized在线程阻塞上的原理。synchronized本身其实都可写几篇文章来探讨，不过本文仅关注于其线程阻塞部分的逻辑。synchronized的阻塞包括2部分：1.调用synchronized(obj)时，如果没有抢到锁，那么会进入队列等待，并阻塞线程。2.获取到锁之后，调用obj.wait()方法进行等待，此时也会阻塞线程。先来看情况一。因为这种情况并非是调用类中的某个方法，而是一个关键字，因此我们是无法从某个类文件入手。那么我们就需要直接查看字节码了。首先创建一个简单的java类public class Synchronized{ public void test(){ synchronized(this){ } } }编译成.class文件后，再查看其字节码javac Synchronized.java javap -v Synchronized.classsynchronized关键字在字节码上体现为monitorenter和monitorexit指令。public void test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 ... 3: monitorenter 4: aload_1 5: monitorexit ...查看bytecodeInterpreter.cpp，hotspot目录/src/share/vm/interpreter该文件中的方法都是用来解析各种字节码命令的。接着我们找到monitorenter方法：这个方法就是synchronized关键字的具体加锁逻辑，十分复杂，这里只是展示方法的入口在哪里。CASE(_monitorenter): { ... }查看objectMonitor.cpp，hotspot目录/src/share/vm/runtime最终synchronized的线程阻塞逻辑是由objectMonitor对象负责的，所以我们直接查看该对象的相应方法。找到enter方法：跳过其中大部分逻辑，我们看到EnterI方法，正是在该方法中阻塞线程的。void ObjectMonitor::enter(TRAPS) { ... //阻塞线程 EnterI(THREAD); ... }查看EnterI方法这个方法会在一个死循环中尝试获取锁，如果获取失败则调用当前线程的ParkEvent的park()方法阻塞线程，否则就退出循环当然特别注意的是，这个方法是在一个死循环中调用的，因此在java级别来看，synchronized是不可打断的，线程会一直阻塞直到它获取到锁为止。void ObjectMonitor::EnterI(TRAPS) { //获取当前线程对象 Thread * const Self = THREAD; ... for (;;) { //尝试获取锁 if (TryLock(Self) > 0) break; ... //调用ParkEvent的park()方法阻塞线程 if (_Responsible == Self || (SyncFlags & 1)) { Self->_ParkEvent->park((jlong) recheckInterval); } else { Self->_ParkEvent->park(); } ... } ... }接着来看情况二：查看objectMonitor.cpp，hotspot目录/src/share/vm/runtime最终Object.wait()的线程阻塞逻辑也是由objectMonitor对象负责的，所以我们直接查看该对象的相应方法。找到wait方法：可以看到wait()方法中对线程的打断作出了响应，并且会抛出InterruptedException，这也正是java级别的Object.wait()方法会抛出该异常的原因线程阻塞和synchronized一样，是由线程的ParkEvent对象的park()方法完成的void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) { //获取当前线程对象 Thread * const Self = THREAD; //检查是否可以打断 if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) { ... //抛出InterruptedException THROW(vmSymbols::java_lang_InterruptedException()); } if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) { //如果线程被打断了，那就什么都不做 } else if (node._notified == 0) { //调用ParkEvent的park()方法阻塞线程 if (millis <= 0) { Self->_ParkEvent->park(); } else { ret = Self->_ParkEvent->park(millis); } } }所以对于synchronized和Object.wait来说，最终都是调用thread中ParkEvent对象的park()方法实现线程阻塞的而在java层面上synchronized本身是不响应线程打断的，但是Object.wait()方法却是会响应打断的，区别正是在于jvm级别的逻辑处理上有所不同。4.ParkEvent和parker对象的原理Thread.sleep、synchronized和Object.wait底层分别是利用线程SleepEvent和ParkEvent对象的park方法实现线程阻塞的。因为这2个对象实际是一个类型的，因此我们就一起来看一下其park方法究竟做了什么查看thread.cpp，hotspot目录src/share/vm/runtime找到SleepEvent和ParkEvent的定义，从后面的注释就可以发现，ParkEvent就是供synchronized()使用的，而SleepEvent则是供Thread.sleep使用的：ParkEvent * _ParkEvent; // for synchronized() ParkEvent * _SleepEvent; // for Thread.sleep查看park.hpp，hotspot目录src/share/vm/runtime在头文件中能找到ParkEvent类的定义，继承自os::PlatformEvent：class ParkEvent : public os::PlatformEvent { ... }查看os_linux.hpp，hotspot目录src/os/linux/vm以linux系统为例，在头文件中可以看到PlatformEvent的具体定义：我们关注的重点首先是2个private的对象，一个pthread_mutex_t，表示操作系统级别的信号量，一个pthread_cond_t，表示操作系统级别的条件变量其次是定义了3个方法，park()、unpark()、park(jlong millis)，控制线程的阻塞和继续执行class PlatformEvent : public CHeapObj<mtInternal> { private: ... pthread_mutex_t _mutex[1]; pthread_cond_t _cond[1]; ... void park(); void unpark(); int park(jlong millis); // relative timed-wait only ... };查看os_linux.cpp，hotspot目录src/os/linux/vm接着我们就需要去看park方法的具体实现，这里我们主要关注3个系统底层方法的调用pthread_mutex_lock(_mutex)：锁住信号量status = pthread_cond_wait(_cond, _mutex)：释放信号量，并在条件变量上等待status = pthread_mutex_unlock(_mutex)：释放信号量void os::PlatformEvent::park() { ... //锁住信号量 int status = pthread_mutex_lock(_mutex); while (_Event < 0) { //释放信号量，并在条件变量上等待 status = pthread_cond_wait(_cond, _mutex); } //释放信号量 status = pthread_mutex_unlock(_mutex); }可以看到ParkEvent的park()方法底层最终是调用系统函数pthread_cond_wait完成线程阻塞的操作。而线程的parker对象的park()方法本质和ParkEvent是完全一致的，最终也是调用系统函数pthread_cond_wait完成线程阻塞的操作，区别只是在于多了一个绝对时间的判断：查看os_linux.cpp，hotspot目录src/os/linux/vmvoid Parker::park(bool isAbsolute, jlong time) { ... if (time == 0) { //这里是直接长时间等待 _cur_index = REL_INDEX; status = pthread_cond_wait(&_cond[_cur_index], _mutex); } else { //这里会根据时间是否是绝对时间，分别等待在不同的条件上 _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX; status = pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime); } ... }5.Thread.interrupt的原理上面看了3中线程阻塞的原理，那么接着自然是需要看一下线程打断在jvm层面上到底做了什么。我们跳过代码搜寻的过程，直接看最后一步的源码查看os_posix.cpp，hotspot目录src/os/posix/vm找到interrupt方法，这个方法正是打断的重点，其中一共做了2件事情：1.将打断标记置为true2.分别调用thread中的ParkEvent、SleepEvent和Parker对象的unpark()方法void os::interrupt(Thread* thread) { ... //获得c++线程对应的系统线程 OSThread* osthread = thread->osthread(); //如果系统线程的打断标记是false，意味着还未被打断 if (!osthread->interrupted()) { //将系统线程的打断标记设为true osthread->set_interrupted(true); //这个涉及到内存屏障，本文不展开 OrderAccess::fence(); //这里获取一个_SleepEvent，并调用其unpark()方法 ParkEvent * const slp = thread->_SleepEvent ; if (slp != NULL) slp->unpark() ; } //这里依据JSR166标准，即使打断标记为true，依然要调用下面的2个unpark if (thread->is_Java_thread()) //如果是一个java线程，这里获取一个parker对象，并调用其unpark()方法 ((JavaThread*)thread)->parker()->unpark(); ParkEvent * ev = thread->_ParkEvent ; //这里获取一个_ParkEvent，并调用其unpark()方法 if (ev != NULL) ev->unpark() ; }通过对3个park对象park()方法的了解，在unpark中必然是调用系统级别的signal方法：void os::PlatformEvent::unpark() { ... if (AnyWaiters != 0) { //唤醒条件变量 status = pthread_cond_signal(_cond); } ... }所以对于Thread.interrupt来说，它最重要的事情其实是调用3个unpark()方法对象唤醒线程，而我们老生常谈的修改打断标记，反倒是没那么重要。是否响应该标记、是在jvm层上响应还是在java层上响应等等逻辑，都取决于实际需要。6.对于synchronized的扩展在synchronized的原理部分，我们看到线程的阻塞是在一个死循环中执行的，因此在java级别上看来是不可打断的。如果了解synchronized的原理（不了解也没关系，一会儿会有实际示例），可以知道当线程没有抢到锁时会进入一个队列并阻塞，而线程的正常唤醒顺序会按照入队列的顺序依次进行。然而，如果我们仔细看jvm的逻辑，可以发现在循环中，每当线程被唤醒后都会去调用TryLock方法尝试获取锁，那么结合我们对Thread.interrupt方法的了解我们就可以大胆推测，虽然在java级别上synchronized不可打断，但是如果我们不断地调用Thread.interrupt方法就能使得线程直接插队获取锁，而不必按照入队列的顺序了！接下来我们来看示例1.synchronized的顺序性这里我们先让一个线程获取到锁，之后启动3个线程等待在锁上。@Test public void synchronizedTest() throws InterruptedException { int size = 3; Object lock = new Object(); //让第一个线程获取锁后阻塞1秒钟 new Thread(() -> { synchronized (lock) { System.out.println("Thread Lock"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Lock Over"); } }).start(); TimeUnit.MILLISECONDS.sleep(10); //启动3个线程，并等待第一个线程释放锁，每个线程启动间隔10毫秒，保证入队列的顺序性 int count = 1; for (int i = 0; i < size; i++) { int m = count++; TimeUnit.MILLISECONDS.sleep(10); new Thread(() -> { synchronized (lock) { System.out.println(Thread.currentThread().getName()); } }, "thread--" + m).start(); } TimeUnit.SECONDS.sleep(Integer.MAX_VALUE); }最终输出结果，可以看到synchronized的队列遵循先入后出的原则Thread Lock Lock Over thread--3 thread--2 thread--12.线程打断对队列顺序的影响在启动3个线程入队列之前，我们先启动一个单独的线程。并且在主线程的最后，我们在一个死循环中不断调用该单独线程的interrupt方法。@Test public void synchronizedTest() throws InterruptedException { int size = 3; Object lock = new Object(); new Thread(() -> { synchronized (lock) { System.out.println("Thread Lock"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Lock Over"); } }).start(); TimeUnit.MILLISECONDS.sleep(10); //启动一个单独的线程，用来测试synchronized的打断 Thread interruptThread = new Thread(() -> { synchronized (lock) { System.out.println("interruptThread"); } }); interruptThread.start(); TimeUnit.MILLISECONDS.sleep(10); int count = 1; for (int i = 0; i < size; i++) { int m = count++; TimeUnit.MILLISECONDS.sleep(10); new Thread(() -> { synchronized (lock) { System.out.println(Thread.currentThread().getName()); } }, "thread--" + m).start(); } //在主线程中不断打断单独线程 for(;;){ interruptThread.interrupt(); } }输出如下，按照先入后出的原则，这个单独的线程应该是最后一个获取到锁的。然而在主线程不断地打断下，它成功地完成了插队！而其他没有被打断的线程依然按照约定的顺序依次唤醒。有兴趣的同学可以尝试去掉最后的打断，再运行一次。Thread Lock Lock Over interruptThread thread--3 thread--2 thread--1最后总结一下Thread.sleep、LockSupport.park和synchronized线程阻塞方式的区别，这里我分几个层次来总结1.系统级别：这3种方式没有区别，最终都是调用系统的pthread_cond_wait方法2.c++线程级别：Thread.sleep使用的是线程的SleepEvent对象，LockSupport.park使用的是线程的Parker对象，synchronized和Object.wait使用的是线程的ParkEvent对象3.java级别：Thread.sleep可打断并抛出异常；LockSupport.park可打断，且不会抛出异常；synchronized不可打断；Object.wait可打断并抛出异常4.InterruptedException其实仅仅是jvm逻辑上对打断标记的判断而已5.Thread.interrupt的本质在于修改打断标记，并调用3个unpark()方法唤醒线程4.更概括来说，无论是哪种线程阻塞的方式，在系统级别和c++线程级别来说都是可打断的。而jvm通过代码逻辑使得3种线程阻塞的方式在java级别上面对同一个打断方法时会有不同的表现形式
Mybatis工厂模式https://blog.csdn.net/u013412772/article/details/73648537工厂模式概念在Mybatis中⽐如SqlSessionFactory使⽤的是⼯⼚模式，该⼯⼚没有那么复杂的逻辑，是⼀个简单⼯⼚模式。简单工厂模式：该魔术不属于23种经典设计模式，是一种很常见且简单的创建对象的方式。又称为静态工厂方法模式。（Static Factory Methods）例如spring中创建对象可以传入class对象然后按照类型进行注入,这就是简单工厂模式.简单工厂模式的实质是由一个工厂类根据传入的参数，动态决定应该创建哪一个产品类（这些产品类继承自一个父类或接口）的实例。Mybatis的设计mybatis的持久化解决方案是将用户从原始的JDBC访问中解放出来，用户只需要定义需要操作的SQL语句,无须关注底层的JDBC操作,就可以以面向对象的方式来进行持久化层操作.底层数据库连接的获取,数据访问的实现,事务控制等都无须用户关心,从而将应用层从底层的JDBC/JTA API抽取出来.通过配置文件管理JDBC连接,让MyBatis解决持久化的实现.在MyBatis中的常见对象有SqlSessionFactory和SqlSession.本文这种介绍一下两者的概念和使用.SqlSessionFactorySqlSessionFactory是Mybatis的关键对象，它是单个数据映射关系经过编译后的内存镜像。SqlSessionFactory对象实例可以通过SqlSessionFactoryBuilder对象获得，而SqlSessionFactoryBuilder则可以从XML配置文件或一个预先定制的Configuration的实例构建出SqlSessionFactory的实例。每一个Mybatis的应用程序都以一个SqlSessionFacotry对象的实例为核心。同时SqlSessionFacotry也是线程安全的，SqlSessionFacotry一旦被创建，应该在应用执行期间都存在，在应用运行期间不需要多次创建，建议使用单例模式。SqlSessionFactory是创建SqlSession的工厂。//SqlSessionFactory接口源码如下所示: package org.apache.ibatis.session; import java.sql.Connection; public interface SqlSessionFactory { SqlSession openSession();//这个方法最经常用,用来创建SqlSession对象. SqlSession openSession(boolean autoCommit); SqlSession openSession(Connection connection); SqlSession openSession(TransactionIsolationLevel level); SqlSession openSession(ExecutorType execType); SqlSession openSession(ExecutorType execType, boolean autoCommit); SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level); SqlSession openSession(ExecutorType execType, Connection connection); Configuration getConfiguration(); }SqlSessionSqlSession是Mybatis关联对象，是持久化操作的独享，类似于JDBC的Connection。它是应用程序与持久层之间执行交互操作的一个单线程对象，也是Mybatis执行持久化操作的关键对象。SqlSession对象完全包含以数据库为背景的所有执行SQL操作的方法，它底层封装了JDBC连接，可以用SqlSession实例来直接执行被映射的SQL语句。每个线程都有它自己的SqlSession实例。SqlSession的实例不能被共享，同时SqlSession也是线程不安全的，决不能将SqlSession实例的引用放在一个类的静态字段甚至是实例字段中。也决不能将SqlSession实例的引用放在任何类型的管理范围中，比如Servlet当中的HttpSession对象中，使用完SqlSession之后关闭Session很重要。应该确保使用finally块来关闭它。//SqlSession接口源码如下所示: package org.apache.ibatis.session; import java.io.Closeable; import java.sql.Connection; import java.util.List; import java.util.Map; import org.apache.ibatis.executor.BatchResult; public interface SqlSession extends Closeable { <T> T selectOne(String statement); <T> T selectOne(String statement, Object parameter); <E> List<E> selectList(String statement); <E> List<E> selectList(String statement, Object parameter); <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds); <K, V> Map<K, V> selectMap(String statement, String mapKey); <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey); <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds); void select(String statement, Object parameter, ResultHandler handler); void select(String statement, ResultHandler handler); void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler); int insert(String statement); int insert(String statement, Object parameter); int update(String statement); int update(String statement, Object parameter); int delete(String statement); int delete(String statement, Object parameter); void commit(); void commit(boolean force); void rollback(); void rollback(boolean force); List<BatchResult> flushStatements(); void close(); void clearCache(); Configuration getConfiguration(); <T> T getMapper(Class<T> type); Connection getConnection(); } SqlSessionFactory和SqlSession实现过程mybatis框架主要是围绕着SqlSessionFactory进行的，创建过程大概如下：定义一个Configuration对象，其中包含数据源、事务、Mapper文件资源以及影响数据库行为属性设置setting通过配置对象，可以创建一恶搞SqlSessionFacotryBuilder对象通过SqlSessionFacotryBuilder获得SqlSessionFactory的实例SqlSessionFacotry的实例可以获得操作数据的SqlSession实例，通过这个实例对数据库进行操作。并且如果想按照上述方式得到SqlSessionFactory,最好使用下面的mybatis-config.xml类似的配置.在这里mybatis-config.xml配置文件是没有和Spring配置文件整合过得,如果项目中mybaits的配置文件和Spring配置文件整合过了,则下面的代码运行估计会出错,因为一般spring和mybatis整合过之后,mybatis的配置文件基本没有存在的必要了,之前在mybatis中配置的数据源和事务这两个方面,一般的做法都会spring的配置文件,则下面的代码加载mybatis-config.xml的时候,得不到必要的信息,创建的过程中会有问题.所以在这里先给一份mybatis-config.xml单独的配置文件.<?xml version="1.0" encoding="UTF-8"?> <configuration> <properties resource="db.properties"/> <typeAliases> <typeAlias type="cn.itcast.javaee.mybatis.app04.Student" alias="student"/> </typeAliases> <environments default="mysql_developer"> <environment id="mysql_developer"> <transactionManager type="jdbc"/> <dataSource type="pooled"> <property name="driver" value="${mysql.driver}"/> <property name="url" value="${mysql.url}"/> <property name="username" value="${mysql.username}"/> <property name="password" value="${mysql.password}"/> </dataSource> </environment> <environment id="oracle_developer"> <transactionManager type="jdbc"/> <dataSource type="pooled"> <property name="driver" value="${oracle.driver}"/> <property name="url" value="${oracle.url}"/> <property name="username" value="${oracle.username}"/> <property name="password" value="${oracle.password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="cn/itcast/javaee/mybatis/app14/StudentMapper.xml"/> </mappers> </configuration> 下面的这行代码功能是通过配置文件mybatis-config.xml,创建SqlSessionFactory对象,然后产生SqlSession,执行SQL语句.而mybatis的初始化发生在:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);如果是spring和mybaits整合之后的配置文件,一般以这种方式实现,SqlSessionFactory的创建:<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"></property> <property name="mapperLocations" value="classpath:com/cn/mapper/*.xml"></property> </bean>关于SqlSessionFactory和SqlSession两个对象给一个具体的使用过程:package com.cn.testIUserService; import java.io.IOException; import java.io.InputStream; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import com.cn.entity.User; public class MyBatisTest { public static void main(String[] args) { try { //读取mybatis-config.xml文件 InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml"); //初始化mybatis,创建SqlSessionFactory类的实例 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); //创建session实例 SqlSession session = sqlSessionFactory.openSession(); /* * 接下来在这里做很多事情,到目前为止,目的已经达到得到了SqlSession对象.通过调用SqlSession里面的方法, * 可以测试MyBatis和Dao层接口方法之间的正确性,当然也可以做别的很多事情,在这里就不列举了 */ //插入数据 User user = new User(); user.setC_password("123"); user.setC_username("123"); user.setC_salt("123"); //第一个参数为方法的完全限定名:位置信息+映射文件当中的id session.insert("com.cn.dao.UserMapping.insertUserInformation", user); //提交事务 session.commit(); //关闭session session.close(); } catch (IOException e) { e.printStackTrace(); } } }针对上面的代码给出详细的说明关于SqlSessionFactory和SqlSession创建过程涉及的内容.结合上述SqlSessionFactory和SqlSession使用过程和结构图,涉及到的方法为下面步骤,结合源码中的方法为下面的步骤:第一步首先SqlSessionFactoryBuilder去读取mybatis的配置文件，然后build一个DefaultSqlSessionFactory,即得到SqlSessionFactory//源码中涉及的包和具体方法为: //涉及的包为:package org.apache.ibatis.session; //第一个类为:SqlSessionFactoryBuilder,设计到此类的方法为下面部分: public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); } public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { //通过XMLConfigBuilder解析配置文件，解析的配置相关信息都会封装为一个Configuration对象 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); //然后返回一个DefaultSqlSessionFactory return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } //得到DefaultSqlSessionFactory public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); } //第二个类为:DefaultSqlSessionFactory,涉及的方法为: public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } 第二步，获取到SqlSessionFactory之后，就可以利用SqlSessionFactory方法的openSession来获取SqlSession对象了。private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { //通过Confuguration对象去获取Mybatis相关配置信息, Environment对象包含了数据源和事务的配置 // execType为执行器类型，配置文件中定义 // SimpleExecutor -- SIMPLE 就是普通的执行器。 //ReuseExecutor -执行器会重用预处理语句（prepared statements） //BatchExecutor --它是批量执行器 final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); //定义执行器，是对statement的封装 final Executor executor = configuration.newExecutor(tx, execType); //最后返回一个SqlSession return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }得到SqlSession对象之后就可以利用SqlSession内部的方法进行CRUD操作了。注意一点，Connection对象是在SqlSession对象创建之后进行CURD操作中创建的。深入查找之后找到在ManagedTransaction类中找到获取Connection对象的关键代码如下： protected void openConnection() throws SQLException { if (log.isDebugEnabled()) { log.debug("Opening JDBC Connection"); } //dataSource 来源有三种，JndiDatasource，PooledDataSource，UnpooledDataSource，配置文件中定义 this.connection = this.dataSource.getConnection(); if (this.level != null) { this.connection.setTransactionIsolation(this.level.getLevel()); } }PooledDataSource和UnPooledDataSource的区别是PooledDataSource使用了连接池。为什么使用连接池呢？因为创建一个Connection对象的过程，在底层就相当于和数据库建立的通信连接，在建立通信连接的过程，消耗了非常多的时间，而往往我们建立连接后（即创建Connection对象后），就执行一个简单的SQL语句，然后就要抛弃掉，这是一个非常大的资源浪费！mybatis针对这一个问题提出的PooledDataSource使用了连接池。关于数据库连接池的知识点,可以自行百度,在这里就不扩展介绍了.
SOFASOFA-ARKsofastack/sofa-ark: SOFAArk is a light-weight，java based classloader isolation framework. (github.com)背景SOFAArk 最初的场景是解决 Java 开发常常会遇到的包依赖冲突的问题，尤其当工程应用变得臃肿庞大，包冲突的问题也会变得更加棘手，导致各种各样的报错，例如LinkageError, NoSuchMethodError等。实际开发中，可以采用多种方法来解决包冲突问题，比较常见的是类似 SpringBoot 的做法，统一管理应用所有依赖包的版本，保证这些三方包不存在依赖冲突。这种做法只能有效避免包冲突的问题，不能根本上解决包冲突的问题。如果某个应用的确需要在运行时使用两个相互冲突的包，例如 protobuf2 和 protobuf3，那么类似 SpringBoot 的做法依然解决不了问题。为了彻底解决包冲突的问题，我们需要借助类隔离机制，使用不同的ClassLoader加载不同的第三方依赖，进而隔离包冲突的问题。OSGI作为业内最出名的类隔离框架，自然是可以被用于解决上述包冲突的问题，但是OSGI框架太过臃肿，功能繁杂。为了解决包冲突问题，引入OSGI框架有杀鸡用牛刀之嫌，反而使工程变得更加复杂，不利于开发。SOFAArk 则采用较为轻量级的类隔离方案来解决日常经常遇到的包冲突问题，在蚂蚁金服内部服务于整个 SOFABoot 技术体系，弥补 SpringBoot 没有的类隔离能力。实际上，SOFAArk 是一个通用的轻量级类隔离框架，并不限于 SpringBoot 应用，也可以和其他的 Java 开发框架集成。原理SOFAArk 框架包含有三个概念，Ark Container, Ark Plugin 和 Ark Biz; 运行时逻辑结构图如下：在介绍这三个概念之前，为了统一术语，有必要先说一下所谓的Ark包：Ark包是满足特定目录格式要求的Executed Fat Jar，使用官方提供的 Maven 插件 sofa-ark-maven-plugin可以将工程应用打包成一个标准格式的 Ark 包；使用命令 java -jar application.jar即可在 Ark 容器之上启动应用；Ark 包 通常包含 Ark Container、Ark Plugin、 Ark Biz；以下我们针对这三个概念简单做下名词解释：Ark Container: Ark 容器，负责整个运行时的管理；Ark Plugin 和 Ark Biz 运行在 Ark 容器之上；容器具备管理多插件、多应用的功能；容器启动成功后，会自动解析 classpath 包含的 Ark Plugin 和 Ark Biz 依赖，完成隔离加载并按优先级依次启动之；Ark Plugin: Ark 插件，满足特定目录格式要求的 Fat Jar，使用官方提供的 Maven 插件 sofa-ark-plugin-maven-plugin 可以将一个或多个普通的 Java Jar 包打包成一个标准格式的 Ark Plugin； Ark Plugin 会包含一份配置文件，通常包括插件类导入导出配置、插件启动优先级等；运行时，Ark 容器会使用独立的 PluginClassLoader 加载插件，并根据插件配置构建类加载索引表，从而使插件与插件、插件与应用之间相互隔离；Ark Biz: Ark 业务模块，满足特定目录格式要求的 Fat Jar ，使用官方提供的 Maven 插件 sofa-ark-maven-plugin 可以将工程应用打包成一个标准格式的 Ark-Biz 包；是工程应用模块及其依赖包的组织单元，包含应用启动所需的所有依赖和配置；在运行时，Ark Container 优先启动，自动解析 classpath 包含的 Ark Plugin 和 Ark Biz，并读取他们的配置，构建类加载索引关系；然后使用独立的 ClassLoader 加载他们并按优先级配置依次启动；需要指出的是，Ark Plugin 优先 Ark Biz 被加载启动；Ark Plugin 之间是双向类索引关系，即可以相互委托对方加载所需的类；Ark Plugin 和 Ark Biz 是单向类索引关系，即只允许 Ark Biz 索引 Ark Plugin 加载的类，反之则不允许。场景包冲突SOFAArk初衷是为了解决包冲突问题，那什么情况下可以使用 SOFAArk 以及如何使用呢？ 假设如下场景，如果工程需要引入两个三方包：A 和 B，但是 A 需要依赖版本号为 0.1 的 C 包，而恰好 B 需要依赖版本号为 0.2 的 C 包，且 C 包的这两个版本无法兼容:此时，即可使用 SOFAArk 解决该依赖冲突问题；只需要把 A 和版本为 0.1 的 C 包一起打包成一个 Ark Plugin，然后让应用工程引入该插件依赖即可；合并部署SOFAArk 基于类隔离能力，实现了应用的合并部署，可以简单分为静态合并部署和动态合并部署，介绍如下。静态合并部署在实际开发过程中，经常会出现多个团队合作开发同一款产品，他们各自负责不同的功能模块，这些功能模块通常可以独立开发，但是运行时需要作为一个整体的应用运行。在这种情况下，所有团队需要协商统一技术栈及各自的二方包版本，这无疑增加了开发和联调的成本。为了让开发人员专注自身功能业务的开发，理想情况下开发人员希望能像开发独立应用一样，仅定义好对外交互接口，而不用考虑和其他功能模块出现的版本冲突、技术栈不统一等问题。正是基于这种场景，SOFAArk 提供了静态合并部署能力，应用可以依赖其他应用打成的 Biz 包，而当自身被打成 Ark 包时，可以将其他应用 Biz 包一并打入，启动时，则会根据优先级依次启动各应用。由于每个应用使用独立的 BizClassLoader 加载，因此不需要考虑依赖冲突或者技术栈不统一问题。应用之间则通过 SofaService/SofaReference JVM 服务进行交互。动态合并部署动态合并部署区别于静态合并部署最大的一点是，在应用运行时可以通过 API 或者配置中心（Zookeeper）来控制应用的部署和卸载。动态合并部署的设计理念图如下：无论是静态还是动态合并部署都会有宿主应用（master app）的概念, 如果 Ark 包只打包了一个 Biz，则该 Biz 默认成为宿主应用。如果 Ark 包打包了多个 Biz 包，需要配置指定宿主应用。宿主应用不允许被卸载，一般而言，宿主应用会作为流量入口的中台系统，具体的服务实现会放在不同的动态 Biz 中，供宿主应用调用。宿主应用可以使用 SOFAArk 提供的客户端 API 实现动态应用的部署和卸载。除了 API, SOFAArk 提供了 Config Plugin，用于对接配置中心（目前支持 Zookeeper），运行时接受动态配置。Config Plugin 会解析下发的配置，控制动态模块的部署和卸载。随着近几年 Serverless 技术的兴起，蚂蚁集团在 Serverless 领域进行了持续的建设和探索，基于 SOFAArk 动态合并部署技术打造了比较成熟的 SOFAServerless 技术体系，去深入解决企业的研发和运维效率问题。其核心方式是通过快速热部署、动态服务发布等技术，将应用从代码结构和开发者阵型划分为模块和基座。其中基座为业务模块提供计算环境并屏蔽基础设施，让模块开发者不用感知机器和容量等底层设施而专注于某个功能模块的开发迭代来帮助业务快速向前发展。在应用架构领域，不可避免的问题是应用随着业务的复杂度不断增加，研发运维的过程中的问题会不断暴露出来。首先我们看一下普通应用研发和运维过程中的流程是什么样的：如图所示，从需求到设计、开发、线下测试，再到发布线上的研发运维不断反馈、循环迭代的过程。可以简化为开发同学提交代码到代码仓库，在线下做并行的验证测试，测试通过之后在线上发布，发布过程是串行的，只能够有一个发布窗口，这样的过程在应用体量业务还不太复杂的情况下问题，并不是很明显。但当业务复杂度不断增加，普通应用迭代过程在会出现一些新的问题，如下图：管理成本高：需求管理、代码管理、人员管理。时间成本高：线上验证与发布互相阻塞。单次启动慢。变更风险高：一次变更涉及所有代码。一次变更涉及所有机器。另外，由于这些问题是因为多个业务与研发任务耦合在某些单点上导致的，研发运维的成本随着业务的复杂度呈现出指数增长的特点：通过借助 SOFAArk 框架将应用拆分成基座和模块，同时将应用里的接口按场景维度做分组，使得业务可以按一组接口的粒度进行极速发布运维以及资源按需隔离。从这张图里可以看到 Serverless 应用拆分的形态，通过把一个普通的 Java 应用拆出多个模块，进一步对应用进行了拆分：基座和模块，对应的研发人员也划分为基座开发者和模块开发者。基座负责沉淀通用的逻辑，为模块提供计算和环境，并为模块开发者屏蔽基础设施，让模块开发者不需要关心容量和资源等。各个模块则是独立的代码仓库，可以进行独立的研发运维，这样研发运维粒度就得到了精细化，并且由于基座为模块屏蔽了环境与基础设施，模块开发者可以专注于业务开发从而提高了业务创新效率。SOFABootsofa-boot/README_ZH.md at master · sofastack/sofa-boot (github.com)官方介绍SOFABoot 是蚂蚁集团开源的基于 Spring Boot 的研发框架，它在 Spring Boot 的基础上，提供了诸如 Readiness Check，上下文隔离，类隔离，日志空间隔离等等能力。在增强了 Spring Boot 的同时，SOFABoot 提供了让用户可以在 Spring Boot 中非常方便地使用 SOFA 中间件的能力。Spring Boot 是一个非常优秀的开源框架，可以非常方便地就构建出一个基于 Spring 的应用程序，但是在使用过程中，还是会遇到一些问题：Spring Boot 提供了一个基础的健康检查的能力，中间件和应用都可以扩展来实现自己的健康检查逻辑。但是 Spring Boot 的健康检查只有 Liveness Check 的能力，缺少 Readiness Check 的能力，这样会有比较致命的问题。当一个微服务应用启动的时候，必须要先保证启动后应用是健康的，才可以将上游的流量放进来（来自于 RPC，网关，定时任务等等流量），否则就可能会导致一定时间内大量的错误发生。Spring Boot 虽然通过依赖管理（Dependency Management）的方式最大程度的保证了 Spring Boot 管理的 JAR 包之间的兼容性，但是不可避免的，当引入一些其他的 JAR 包的时候，还是可能会遇到冲突，而且很多时候这种冲突解决起来并不是这么容易，一个例子是当冲突的包是序列化相关的类库时，比如说 Hessian，如果应用中的一个组件需要使用 Hessian 3，而另一个则必须要使用 Hessian 4，由于 Hessian 3 和 Hessian 4 之间的不兼容性，并且序列化还涉及到微服务中的上下游服务，要把 Hessian 统一到一个版本绝非易事。在超大规模微服务运维的场景下，运维能力的平台化是一定要解决的问题，而监控又是其中非常主要的一个点，针对于日志监控这种情况，Spring Boot 并没有提供任何解决方案。大部分的开源组件，具体要打印哪些日志，打印到什么路径，什么文件下面，都是由应用的使用者来决定，这样会导致每一个应用的日志配置都各式各样，每一个应用都需要去监控系统中配置自己应用的日志监控，导致关键的监控的实施成本特别高。在企业级应用场景，模块化开发是解决多团队沟通成本的有效解决方案，每个业务团队专注于开发自己的应用模块，每个模块自包含，便于开发及自测，减少团队间的沟通成本。但是 Spring Boot 默认不支持模块化开发，所有 Bean 共用一个 Spring 上下文，在多团队开发时，如果不同团队定义了相同 BeanId，运行时将出现 BeanId 冲突错误。为了解决以上的问题，又因为 SOFA 中间件中的各个组件本身就需要集成 Spring Boot，所以蚂蚁集团基于 Spring Boot 开发并开源了 SOFABoot，来解决以上的问题，也方便使用者在 Spring Boot 中方便地去使用 SOFA 中间件。功能简介增强spring健康检查能力针对Springboot缺少Readiness Check能力的情况，SOFABoot增加了SpringBoot现有的监控检查能力，提供了ReadinessCeck能力。利用Readiness Check的能力，SOFA中间件的各个组件只有在ReadinessCheck通过之后，才将流量引入到应用实例中，比如RP，只有Readiness Check通过之后，才会像注册中心注册，后面来自上游应用流量才会进入。除了中间件可以利用 Readiness Check 的事件来控制流量的进入之外，PAAS 系统也可以通过访问 http://localhost:8080/actuator/readiness 来获取应用的 Readiness Check 的状况，用来控制例如负载均衡设备等等流量的进入。提供类隔离的能力为了解决 Spring Boot 下的类依赖冲突的问题，SOFABoot 基于 SOFAArk 提供了 Spring Boot 上的类隔离的能力，在一个 SOFABoot 的系统中，只要引入 SOFAArk 相关的依赖，就可以将 SOFA 中间件相关的类和应用相关的类的 ClassLoader 进行隔离，防止出现类冲突。当然，用户也可以基于 SOFAArk，将其他的中间件、第三方的依赖和应用的类进行隔离。日志空间隔离能力为了统一大规模微服务场景下的中间件日志的打印，SOFABoot 提供了日志空间隔离的能力给 SOFA 中间件，SOFA 中间件中的各个组件采用日志空间隔离的能力之后，自动就会将本身的日志和应用的普通日志隔离开来，并且打印的日志的路径也是相对固定，非常方便进行统一地监控。SOFA中间件的集成管理基于 Spring Boot 的自动配置能力，SOFABoot 提供了 SOFA 中间件统一易用的编程接口以及 Spring Boot 的 Starter，方便在 Spring Boot 环境下使用 SOFA 中间件，SOFA 中间件中的各个组件都是独立可插拔的，节约开发时间，和后期维护的成本。模块化开发SOFABoot 支持基于 Spring 上下文隔离的模块化开发能力，每个 SOFABoot 模块使用独立的 Spring 上下文，避免不同 SOFABoot 模块间的 BeanId 冲突，有效降低企业级多模块开发时团队间的沟通成本。Kubernetes健康检查健康检查（Health Check）是让系统知道您的应用实例是否正常工作的简单方法。如果您的实例不再工作，则其他服务不应访问该应用或向其发送请求。相反，应该将请求发送到准备好的应用实例，或稍后重试。系统还应该能够使您的应用程序恢复健康状态。强大的自愈能力是Kubernetes这类容器编排引擎的一个重要的特性。自愈的默认实现方式是自动重启发生故障的容器。除此之外用户还可以利用Liveness和Readiness探测机制设置更精细的健康监察，进而实现如下需求：停机部署避免部署无效的镜像更加安全的滚动升级探针可以通过HTTP，Exec，TCP进行探针类型Liveness存活性探针Liveness探针让Kubernetes知道你的应用程序是活着还是死了。 如果你的应用程序还活着，那么Kubernetes就不管它了。 如果你的应用程序已经死了，Kubernetes将删除Pod并启动一个新的替换它。Readiness 就绪性探针只要是探测应用是否准备好接受请求访问，如果检测应用准备好了，就把请求流量放进来；反之，则把应用节点从注册中心拿掉。Startup 启动探针对于旧应用需要更长的启动时间，这时候既不想重启应用也不想让请求访问进来，可以设置启动探测给足够的启动时间保证应用启动成功。
SpringMVC父子容器spring与springmvc父子容器_Spring教程_田守枝Java技术博客 (tianshouzhi.com)父子容器是什么父子容器并非SpringMVC的专利，在普通Spring环境下，Spring就已经设计出具有层次结构的容器了，这种设计方式也并非Spring独创，起工作方式和ClassLoader很相似，每个容器有一个自己容器的父容器，但是与ClassLoader不同的是，通过容器查找bean时是优先从子容器里查找，如果找不到才会从父容器里查找。当应用中存在多个容器时，这种设计方式可以将公共的bean放到父容器中，如果父容器中的bean不适用，之日起还可以覆盖父容器的bean。Spring的容器有俩种，一种是常用的ApplicationContext，父子容器相关的层次结构如下。BeanFactory最终的视线是DefaultListableBeanFactory，其查找bean的部分源码如下，从中可以看出Spring是优先从子容器中查找bean的，如果查找不到会再次从父类容器中找。案例系统中有2个模块：module1和module2，两个模块是独立开发的，module2会使用到module1中的一些类，module1会将自己打包为jar提供给module2使用，我们来看一下这2个模块的代码。放在module1包中，有3个类Service1package com.javacode2018.lesson002.demo17.module1; import org.springframework.stereotype.Component; @Component public class Service1 { public String m1() { return "我是module1中的Servce1中的m1方法"; } } service2package com.javacode2018.lesson002.demo17.module1; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class Service2 { @Autowired private com.javacode2018.lesson002.demo17.module1.Service1 service1; //@1 public String m1() { //@2 return this.service1.m1(); } } 上面2个类，都标注了@Compontent注解，会被spring注册到容器中。@1：Service2中需要用到Service1，标注了@Autowired注解，会通过spring容器注入进来@2：Service2中有个m1方法，内部会调用service的m1方法。再来看模块2放在module2包中，也是有3个类，和模块1中的有点类似。Service1模块2中也定义了一个Service1，内部提供了一个m2方法，如下：package com.javacode2018.lesson002.demo17.module2; import org.springframework.stereotype.Component; @Component public class Service1 { public String m2() { return "我是module2中的Servce1中的m2方法"; } } Service3package com.javacode2018.lesson002.demo17.module2; import com.javacode2018.lesson002.demo17.module1.Service2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class Service3 { //使用模块2中的Service1 @Autowired private com.javacode2018.lesson002.demo17.module2.Service1 service1; //@1 //使用模块1中的Service2 @Autowired private com.javacode2018.lesson002.demo17.module1.Service2 service2; //@2 public String m1() { return this.service2.m1(); } public String m2() { return this.service1.m2(); } } @1：使用module2中的Service1@2：使用module1中的Service2先来思考一个问题上面的这些类使用spring来操作会不会有问题？会有什么问题？这个问题还是比较简单的，大部分人都可以看出来，会报错，因为两个模块中都有Service1，被注册到spring容器的时候，bean名称会冲突，导致注册失败。来个测试类，看一下效果package com.javacode2018.lesson002.demo17; import com.javacode2018.lesson001.demo21.Config; import com.javacode2018.lesson002.demo17.module1.Module1Config; import com.javacode2018.lesson002.demo17.module2.Module2Config; import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class ParentFactoryTest { @Test public void test1() { //定义容器 AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); //注册bean context.register(Module1Config.class, Module2Config.class); //@1 //启动容器 context.refresh(); } } @1：将Module1Config、Module2Config注册到容器，spring内部会自动解析这两个类上面的注解，即：@CompontentScan注解，然后会进行包扫描，将标注了@Compontent的类注册到spring容器。运行test1输出下面是部分输出：Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation- specified bean name 'service1' for bean class [com.javacode2018.lesson002.demo17.module2.Service1] conflicts with existing, non-compatible bean definition of same name and class [com.javacode2018.lesson002.demo17.module1.Service1] service1这个bean的名称冲突了。那么我们如何解决？对module1中的Service1进行修改？这个估计是行不通的，module1是别人以jar的方式提供给我们的，源码我们是无法修改的。而module2是我们自己的开发的，里面的东西我们可以随意调整，那么我们可以去修改一下module2中的Service1，可以修改一下类名，或者修改一下这个bean的名称，此时是可以解决问题的。不过大家有没有想过一个问题：如果我们的模块中有很多类都出现了这种问题，此时我们一个个去重构，还是比较痛苦的，并且代码重构之后，还涉及到重新测试的问题，工作量也是蛮大的，这些都是风险。而spring中的父子容器就可以很好的解决上面这种问题。什么是父子容器创建spring容器的时候，可以给当前容器指定一个父容器。BeanFactory的方式//创建父容器parentFactory DefaultListableBeanFactory parentFactory = new DefaultListableBeanFactory(); //创建一个子容器childFactory DefaultListableBeanFactory childFactory = new DefaultListableBeanFactory(); //调用setParentBeanFactory指定父容器 childFactory.setParentBeanFactory(parentFactory);ApplicationContext//创建父容器 AnnotationConfigApplicationContext parentContext = new AnnotationConfigApplicationContext(); //启动父容器 parentContext.refresh(); //创建子容器 AnnotationConfigApplicationContext childContext = new AnnotationConfigApplicationContext(); //给子容器设置父容器 childContext.setParent(parentContext); //启动子容器 childContext.refresh();上面代码还是比较简单的，大家都可以看懂。我们需要了解父子容器的特点，这些是比较关键的，如下。父子容器特点父容器和子容器是相互隔离的，他们内部可以存在名称相同的bean子容器可以访问父类容器的bean，而父类容器不能访问子类容器的bean调用子类容器的getBean方法获取bean的时候，会沿着当前容器开始向上面的容器进行查找，直到找到对应的bean为止子类容器中可以通过任何方式注入父类容器的bean，而父类容器无法注入子类容器的bean，原因是第二点使用父子容器解决开头的问题关键代码@Test public void test2() { //创建父容器 AnnotationConfigApplicationContext parentContext = new AnnotationConfigApplicationContext(); //向父容器中注册Module1Config配置类 parentContext.register(Module1Config.class); //启动父容器 parentContext.refresh(); //创建子容器 AnnotationConfigApplicationContext childContext = new AnnotationConfigApplicationContext(); //向子容器中注册Module2Config配置类 childContext.register(Module2Config.class); //给子容器设置父容器 childContext.setParent(parentContext); //启动子容器 childContext.refresh(); //从子容器中获取Service3 Service3 service3 = childContext.getBean(Service3.class); System.out.println(service3.m1()); System.out.println(service3.m2()); } 运行输出我是module1中的Servce1中的m1方法 我是module2中的Servce1中的m2方法 注意点我们使用容器的过程中，经常会使用到的一些方法，这些方法通常会在下面的两个接口中org.springframework.beans.factory.BeanFactory org.springframework.beans.factory.ListableBeanFactory 这两个接口中有很多方法，这里就不列出来了，大家可以去看一下源码，这里要说的是使用父子容器的时候，有些需要注意的地方。BeanFactory接口，是spring容器的顶层接口，这个接口中的方法是支持容器嵌套结构查找的，比如我们常用的getBean方法，就是这个接口中定义的，调用getBean方法的时候，会从沿着当前容器向上查找，直到找到满足条件的bean为止。而ListableBeanFactory这个接口中的方法是不支持容器嵌套结构查找的，比如下面这个方法String[] getBeanNamesForType(@Nullable Class<?> type)获取指定类型的所有bean名称，调用这个方法的时候只会返回当前容器中符合条件的bean，而不会去递归查找其父容器中的bean。来看一下案例代码，感受一下：@Test public void test3() { //创建父容器parentFactory DefaultListableBeanFactory parentFactory = new DefaultListableBeanFactory(); //向父容器parentFactory注册一个bean[userName->"路人甲Java"] parentFactory.registerBeanDefinition("userName", BeanDefinitionBuilder. genericBeanDefinition(String.class). addConstructorArgValue("路人甲Java"). getBeanDefinition()); //创建一个子容器childFactory DefaultListableBeanFactory childFactory = new DefaultListableBeanFactory(); //调用setParentBeanFactory指定父容器 childFactory.setParentBeanFactory(parentFactory); //向子容器parentFactory注册一个bean[address->"上海"] childFactory.registerBeanDefinition("address", BeanDefinitionBuilder. genericBeanDefinition(String.class). addConstructorArgValue("上海"). getBeanDefinition()); System.out.println("获取bean【userName】：" + childFactory.getBean("userName"));//@1 System.out.println(Arrays.asList(childFactory.getBeanNamesForType(String.class))); //@2 } 上面定义了2个容器父容器：parentFactory，内部定义了一个String类型的bean：userName->路人甲Java子容器：childFactory，内部也定义了一个String类型的bean：address->上海@1：调用子容器的getBean方法，获取名称为userName的bean，userName这个bean是在父容器中定义的，而getBean方法是BeanFactory接口中定义的，支持容器层次查找，所以getBean是可以找到userName这个bean的@2：调用子容器的getBeanNamesForType方法，获取所有String类型的bean名称，而getBeanNamesForType方法是ListableBeanFactory接口中定义的，这个接口中方法不支持层次查找，只会在当前容器中查找，所以这个方法只会返回子容器的address我们来运行一下看看效果：获取bean【userName】：路人甲Java [address] 结果和分析的一致。那么问题来了：有没有方式解决ListableBeanFactory接口不支持层次查找的问题？spring中有个工具类就是解决这个问题的，如下：org.springframework.beans.factory.BeanFactoryUtils这个类中提供了很多静态方法，有很多支持层次查找的方法，源码你们可以去细看一下，名称中包含有Ancestors的都是支持层次查找的。在test2方法中加入下面的代码：//层次查找所有符合类型的bean名称 String[] beanNamesForTypeIncludingAncestors = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(childFactory, String.class); System.out.println(Arrays.asList(beanNamesForTypeIncludingAncestors)); Map<String, String> beansOfTypeIncludingAncestors = BeanFactoryUtils.beansOfTypeIncludingAncestors(childFactory, String.class); System.out.println(Arrays.asList(beansOfTypeIncludingAncestors)); 输出[address, userName] [{address=上海, userName=路人甲Java}]问题解答springmvc中只使用一个容器是否可以？只使用一个容器是可以正常运行的。那么springmvc中为什么需要用到父子容器？通常我们使用springmvc的时候，采用3层结构，controller层，service层，dao层；父容器中会包含dao层和service层，而子容器中包含的只有controller层；这2个容器组成了父子容器的关系，controller层通常会注入service层的bean。采用父子容器可以避免有些人在service层去注入controller层的bean，导致整个依赖层次是比较混乱的。父容器和子容器的需求也是不一样的，比如父容器中需要有事务的支持，会注入一些支持事务的扩展组件，而子容器中controller完全用不到这些，对这些并不关心，子容器中需要注入一下springmvc相关的bean，而这些bean父容器中同样是不会用到的，也是不关心一些东西，将这些相互不关心的东西隔开，可以有效的避免一些不必要的错误，而父子容器加载的速度也会快一些。父子容器的主要作用应该是划分框架边界。有点单一职责的味道。在J2EE三层架构中，在service层我们一般使用spring框架来管理， 而在web层则有多种选择，如spring mvc、struts等。因此，通常对于web层我们会使用单独的配置文件。例如在上面的案例中，一开始我们使用spring-servlet.xml来配置web层，使用applicationContext.xml来配置service、dao层。如果现在我们想把web层从spring mvc替换成struts，那么只需要将spring-servlet.xml替换成Struts的配置文件struts.xml即可，而applicationContext.xml不需要改变。概念spring和SpringMVC进行整合的时候，一般情况下我们会使用不同的配置文件来配置spring和spirngmvc，因此我们应用中会存在至少2个ApplicationContext实例，由于是在web应用中，最终实例化的是ApplicationContext的子接口WebApplicationContext下面的是父容器，上面的是子容器上图中显示了2个WebApplicationContext实例，为了进行区分，分别称之为：Servlet WebApplicationContext、Root WebApplicationContext。 其中：Servlet WebApplicationContext：这是对J2EE三层架构中的web层进行配置，如控制器(controller)、视图解析器(view resolvers)等相关的bean。通过spring mvc中提供的DispatchServlet来加载配置，通常情况下，配置文件的名称为spring-servlet.xml。Root WebApplicationContext：这是对J2EE三层架构中的service层、dao层进行配置，如业务bean，数据源(DataSource)等。通常情况下，配置文件的名称为applicationContext.xml。在web应用中，其一般通过ContextLoaderListener来加载。springbootspringboot在自动集成了SpringMVC后是否还有父子容器之分？答案是没有为什么springMVC弄了一个父子容器？父子容器的访问规则：子容器可以访问父容器的对象，父容器不能访问子容器的对象spring mvc中父子容器访问规则：子容器可以访问父容器的对象，父容器不能访问子容器的对象1.ContextLoaderListener会被优先初始化时，其会根据元素中contextConfigLocation参数指定的配置文件路径"/resource/applicationContext.xml”，来创建WebApplicationContext实例。 并调用ServletContext的setAttribute方法，将其设置到ServletContext中，key为”org.springframework.web.context.WebApplicationContext.ROOT”，最后的”ROOT"字样表明这是一个 Root WebApplicationContext。2.DispatcherServlet在初始化时调用init方法，会根据元素中contextConfigLocation参数指定的配置文件路径"/resource/spring-servlet.xml”，来创建Servlet WebApplicationContext。同时，其会调用ServletContext的getAttribute方法来判断是否存在Root WebApplicationContext。如果存在，则将其设置为自己的parent。这就是父子上下文(父子容器)的概念。在Spring Boot中，并没有显式的父子容器的概念。Spring Boot使用了自动配置和自动装配的机制，这种机制简化了应用程序的配置和Bean的管理。Spring Boot应用程序通常只有一个应用程序上下文，而不涉及明确的父子容器。Spring Boot的自动配置机制会自动加载和配置所需的Bean，这些Bean通常位于应用程序的类路径上。你可以自定义这些Bean，但通常不需要显式地管理父子容器关系。尽管Spring Boot没有明确的父子容器，但它仍然允许你创建多个配置类和多个包含Bean定义的上下文。这些上下文之间可以有依赖关系，但通常情况下，你不需要显式地管理它们。Spring Boot的目标是简化配置和提供开箱即用的功能，以减少应用程序的复杂性。为什么需要父子容器1、父子容器的主要作用应该是划分框架边界。父子容器的主要作用应该是划分框架边界。有点单一职责的味道。在J2EE三层架构中，在service层我们一般使用spring框架来管理， 而在web层则有多种选择，如spring mvc、struts等。因此，通常对于web层我们会使用单独的配置文件。例如在上面的案例中，一开始我们使用spring-servlet.xml来配置web层，使用applicationContext.xml来配置service、dao层。如果现在我们想把web层从spring mvc替换成struts，那么只需要将spring-servlet.xml替换成Struts的配置文件struts.xml即可，而applicationContext.xml不需要改变。2、是否可以把所有类都通过Spring父容器来管理Spring的applicationContext.xml中配置全局扫描，所有的类都通过父容器来管理的配置就是如下：然后在SpringMvc的配置里面不配置扫描包路径。很显然这种方式是行不通的，这样会导致我们请求接口的时候产生404。因为在解析@ReqestMapping注解的过程中initHandlerMethods()函数只是对Spring MVC 容器中的bean进行处理的，并没有去查找父容器的bean， 因此不会对父容器中含有@RequestMapping注解的函数进行处理，更不会生成相应的handler。所以当请求过来时找不到处理的handler，导致404。3、是否可以把我们所需的类都放入Spring-mvc子容器里面来管理（springmvc的spring-servlet.xml中配置全局扫描）这个是把包的扫描配置spring-servlet.xml中这个是可行的。为什么可行因为无非就是把所有的东西全部交给子容器来管理了，子容器执行了refresh方法，把在它的配置文件里面的东西全部加载管理起来来了。虽然可以这么做不过一般应该是不推荐这么去做的，一般人也不会这么干的。如果你的项目里有用到事物、或者aop记得也需要把这部分配置需要放到Spring-mvc子容器的配置文件来，不然一部分内容在子容器和一部分内容在父容器,可能就会导致你的事物或者AOP不生效。（遇到事物不起作用的时候，其实这也是一种情况）同时通过两个容器同时来管理所有的类？会造成内存的浪费父子容器造成的事务失效问题：比如父容器在service层配置了事务注解，子容器controller在注册的时候就会从父容器拿到事务增强的service方法；但如果子容器中也扫描了service方法，那么controller注入的时候子容器中没有被事务增强的service方法就会被注入到controller方法中，导致事务失效。
java9模块化定义java9模块模块是代码、数据和资源的集合。它是一组相关包和类型（类、抽象类、接口等），包含代码、数据文件和一些静态资源。例如，模块描述符module-info.java是Java9模块中的资源之一。每个模块质保函一组相关代码和数据，以支持单一责任原则（SRP）：一个类改变的原因不应超过一个。简单的说法是模块=代码+数据基本模块和可靠配置目前java9有98个模块，但它仍在继续发展。Oracle将jdk jar和java se规范分为俩组模块。所有JDK和用户定义模块的默认模块都是基本模块java.base文件。它是一个独立的模块，不依赖于任何其他模块。java.base文件被称为java9模块之母该图是一个依赖关系图。每个框表示一个模块，如果一个模块有一个指向另一个模块的箭头，则表示该箭头所指向的模块需要它所指向的模块来执行其功能。正如你在图片中看到的， java.base 文件是其他模块所依赖的模块。这是因为它包含Java对象、整数、字符串等基本类。没有这些类，代码就不能工作。我可以使依赖关系更加明确：java8时的模块rt.jarrt.jar代表runtime JAR，并且包含在引导类（bootstrap classes0）来自Core API Java的所有类。Windows和Linux中，rt.jar位于JRE的lib目录下。JDK1.7之前MacOSX中叫classes.jar。从java7开始也该位rt.jar。很多开发者尝试把自己的类放进rt.jar中，但最好不要这样做，因为rt.jar包含了JVM新人的class文件，jvm加载时不会对其他class文件进行严格的安全检查。rt.jar代表runtime，包含所有核心java运行环境的已编译class文件必须在类路径中包含rt.jar，否则您无权访问核心类，例如 java.lang.String，java.lang.Thread，java.util.ArrayList或java.io.InputStream等，以及所有Java API中的其他类。 可以使用IDE打开并查看rt.jar中的内容， 它不仅包含所有Java API，还包含com包中指定的内部类。在windows中，rt.jar位于$ JAVA_HOME / jre / lib下。 即使没有安装JDK并且只安装JRE，也会在完全相同的位置看到它，在$ JAVA_HOME / lib目录中找不到rt.jar。 顺便提一句，在MacOSX上，它被称为classes.jarand，位于/ System / Library / Frameworks // Classes目录下。关于rt.jar一个很重要的事情就是，JVM知道这个jar文件中的所有类，这意味着JVM在任何位置加载都不会执行检查。之所以这样做是由于各种性能原因，这就是为什么这些class由bootstrap或primodial类加载器加载的原因。所以最好不要尝试将自己类文件包含在rt.jar中，java官方也不建议这样做。核心模块需要知道，核心jar包通常包含了Java平台的基础类库，如java.lang、java.util、java.io等等。这些类库提供了Java程序运行所需要的核心功能。非核心jar包则是指那些不是Java应用程序或框架必需的 jar包，这些jar包提供了一些额外的功能和工具类库，比如数据库驱动、日志管理、ORM框架等等。它们通常是可选的，可以根据需要引入。在Java应用程序或框架中，通常会使用一些第三方依赖库，这些依赖库会以jar包的形式提供。这些jar包可以是核心的，也可以是非核心的。在应用程序或框架的构建和部署过程中，需要将这些jar包一并打包，以便于程序的使用和运行。在安装java包时，会遇到两次路径选择，第一次时选择jdk的路径，第二次是选择JRE的路径，如果所选择的jdk安装路径和jre的安装路径相同，那么jre包中的内容会覆盖掉jdk中的内容，因此，在你安装完成之后，会发现找不到tools.jar和dt.jar包模块化简介什么是模块系统，官方的定义是A uniquely named, reusable group of related packages, as well as resources (such as images and XML files) and a module descriptor.（一组唯一命名、可重复使用的相关包，以及资源（如图像和XML文件）和模块描述符。）如图模块的载体是jar文件，一个模块就是一个jar文件，但相比于传统的jar文件，模块的根目录下多了一个module-info.class文件，也即（module descriptor）包含以下信息模块名称依赖哪些模块导出模块内的哪些包（允许直接import使用）开放模块内的哪些包（允许通过java反射访问）提供哪些服务依赖哪些服务也就是说任意一个jar文件，只要加上一个合法的module descriptor，就可以升级为一个模块。这个看似微小的改变，可以带来哪些好处？原生的依赖管理。有了模块系统，java可以依据module descriptor计算出各个模块之间的依赖关系，一旦发现循环依赖，启动就会终止。同时模块系统不允许模块导出相同的包（即split package分裂包），所以在查找包时，java可以精准定位到一个模块，从而获得更好的性能。精简 JRE。引入模块系统之后，JDK 自身被划分为 94 个模块（参见图-2）。通过 Java 9 新增的 jlink 工具，开发者可以根据实际应用场景随意组合这些模块，去除不需要的模块，生成自定义 JRE，从而有效缩小 JRE 大小。得益于此，JRE 11 的大小仅为 JRE 8 的 53%，从 218.4 MB缩减为 116.3 MB，JRE 中广为诟病的巨型 jar 文件 rt.jar 也被移除。更小的 JRE 意味着更少的内存占用，这让 Java 对嵌入式应用开发变得更友好。更好的兼容性。自打 Java 出生以来，就只有 4 种包可见性，这让 Java 对面向对象的三大特征之一封装的支持大打折扣，类库维护者对此叫苦不迭，只能一遍又一遍的通过各种文档或者奇怪的命名来强调这些或者那些类仅供内部使用，擅自使用后果自负云云。Java 9 之后，利用 module descriptor 中的 exports 关键词，模块维护者就精准控制哪些类可以对外开放使用，哪些类只能内部使用，换句话说就是不再依赖文档，而是由编译器来保证。类可见性的细化，除了带来更好的兼容性，也带来了更好的安全性。提升 Java 语言开发效率。Java 9 之后，Java 像开挂了一般，一改原先一延再延的风格，严格遵循每半年一个大版本的发布策略，从 2017 年 9 月到 2020 年 3 月，从 Java 9 到 Java 14，三年时间相继发布了 6 个版本，无一延期，参见图-4。这无疑跟模块系统的引入有莫大关系。前文提到，Java 9 之后，JDK 被拆分为 94 个模块，每个模块有清晰的边界（module descriptor）和独立的单元测试，对于每个 Java 语言的开发者而言，每个人只需要关注其所负责的模块，开发效率因此大幅提升。这其中的差别，就好比单体应用架构升级到微服务架构一般，版本迭代速度不快也难。模块化将rt.jar做了拆分，导致了ClassLoader的响应调整java9之前的ClassLoader启动类加载器（Bootstrap）:也叫引导类加载器，C++实现,在jav中无法获取,负责加载JAVA_HOME/lib下的类拓展类加载器（Ext）:JAVA实现,在java里获取,负责加载JAVA_HOME/lib/ext下的类应用程序类加载器（Application）：也称为系统类加载器，一般情况下这个就是程序中默认使用的类加载器，我们写的代码默认就是由他来进行加载java9之后的classloaderbootstrap：加载lib/modulesExt:更名为Platform ClassLoader（平台类加载器）加载lib、modulesApplication：加载-cp，-mp指定的类，父类不再是URLClassLoader定制JRE导出可执行 JAR 文件用 Eclipse 打开项目，选择 File - Export - Java - Runnable Jar File。选择要导出的项目，在 Library Handling 一栏，选择“Copy all required lib next to the generated jar”，点击 Finish。注意：设定导出位置时，jar 文件的文件名时不能含空格。假设我们导出 MyApp.jar，在导出目录下会有 MyApp.jar 本身以及 MyApp_lib 文件夹。后者存放了 MyApp.jar 依赖的 JAR 文件。把导出目录作为程序目录，把其他的资源文件也复制进来。定制 JRE转到 JDK 的 bin 目录下，使用 jdeps 命令查看所使用的 jar 包（包括 MyApp_lib 下的 JAR 文件）依赖的模块。假设 MyApp.jar 被导出到了 D:MyApp 下，则查看 MyApp.jar 的依赖的方式为：jdeps "D:\MyApp\MyApp.jar"知道需要的依赖后，使用 jlink 导出你的定制 JRE。jlink 命令的格式为：jlink --output <定制 JRE 的导出路径> --module-path <jmods 文件夹的路径> --add-modules <依赖的模块>假设 jdeps 提示需要的依赖有 java.base、javafx.base、javafx.graphics、javafx.controls，把 jre 导出到 D:MyAppjre 下，则使用命令：jlink --output "D:\MyApp\jre" --module-path ..\jmods --add-modules java.base,javafx.base,javafx.graphics,javafx.controls顺利的话，D:MyApp 下会出现 jre 文件夹，这个文件夹就是我们需要的定制 JRE。注意：jlink 和 jdeps 是 JAVA 9 开始才有的东西。制作 .exe 文件在程序目录下新建一个 .bat 文件，在其中键入命令，使其调用我们定制的 JRE 中的 javaw.exe 运行 jar 文件。例如：start jre/bin/javaw -jar MyApp.jar双击运行，应该可以看到 JAVA 程序启动。最后使用 bat to exe converter 将 bat 文件转为 exe。在 Options 中可以做一些设置，比如设置程序图标，是否要显示控制台等。把 exe 文件放在程序目录下，双击它，就可以运行我们的 JAVA 程序。