Repository navigation
Expand file tree
/
Copy pathindex.json
More file actions
1 lines (1 loc) · 52.7 KB
/
Copy pathindex.json
File metadata and controls
1 lines (1 loc) · 52.7 KB
1
[{"categories":[],"content":"JAVA并发 ","date":"2022-08-31","objectID":"/java_concurrent/:0:0","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"基础知识 ","date":"2022-08-31","objectID":"/java_concurrent/:1:0","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"并发编程的优缺点 为什么要用到并发(优点) 充分利用多核CPU的运算能力 方便业务拆分 并发编程的缺点 频繁的上下文切换 线程安全(常见的避免死锁的方式) 易混淆的概念 同步 VS 异步 并发 VS 并行 阻塞 VS 非阻塞 临界区资源 ","date":"2022-08-31","objectID":"/java_concurrent/:1:1","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"线程的状态和基本操作 如何新建线程 继承Thread类 实现Runnable接口 实现Callable接口 几种新建线程方式的比较 线程状态的转换 NEW RUNNABLE WAITING TIMED_WAITING TERMINATED BLOCKED 线程的基本操作 interrupt 抛出InterruptedException异常时,会清除中断标志位 interrupt(), interrupted(), isInterrupt() sleep sleep与wait的区别 join yield yield仅仅只会把时间片让给同优先级的线程,而sleep没有这个要求 守护线程Daemon ","date":"2022-08-31","objectID":"/java_concurrent/:1:2","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"并发理论 JMM ","date":"2022-08-31","objectID":"/java_concurrent/:2:0","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"JMM内存模型 哪些是共享数据 实例域 静态域 数组 抽象结构 线程将数据拷贝到工作内存,再刷新到主存。 各个线程通过主存中的数据来完成隐式通信 ","date":"2022-08-31","objectID":"/java_concurrent/:2:1","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"重排序 什么是重排序 为了提高执行性能,编译器和处理器会对指令进行重排序 针对编译器重排序,编译器重排序规则会禁止一些特定类型的编译器重排序 针对处理器重排序,编译器会在生成指令的时候插入内存屏障来禁止特定类型的处理器重排序 数据依赖性 as-if-serial 遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的 ","date":"2022-08-31","objectID":"/java_concurrent/:2:2","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"happens-before规则 定义 如果A happens-before B,则A操作的结果对操作B可见,且A操作在操作B之前执行 如果指令重排序之后的结果,与按照happens-before关系执行的结果一致,则指令可以重排序 理解 站在程序员角度:为编程人员提供了一个类似强内存的内存结构,方便编程 站在编译器和处理器厂商:在不影响正确结果的前提下,可以让编译器和处理器厂商尽情优化 具体规则 程序顺序规则 volatile变量规则 监视器锁规则 传递性 start规则 join规则 线程中断规则 对象finnalize规则 ","date":"2022-08-31","objectID":"/java_concurrent/:2:3","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"并发关键字 ","date":"2022-08-31","objectID":"/java_concurrent/:3:0","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"synchronized 如何使用? 实例方法(锁的是实例对象) 静态方法(锁的是类对象) 代码块根据配置,锁的是实例对象也可以是类对象 moniter机制 字节码中会添加monitorenter和monitorexit指令 锁的重入性:同一个锁程,不需要再次申请获取锁 synchronized的happens-before关系 synchronized的内存语义 共享变量会刷新到主存中,线程每次会从主存中读取最新的值到自身的工作内存中 锁优化 锁状态 无锁状态 偏向锁 轻量级锁 重量级锁 CAS操作 是一种乐观锁策略 利用现代处理器的CMPXCHG 存在ABA的问题;自旋时间可能过长的问题 JAVA对象头 对象的hashcode 对象的分代年龄 是否是偏向锁的标志位 锁标志位 锁升级策略 轻量级锁 加锁:在对象头和栈帧的锁记录中,添加自身的线程ID 锁撤销:在全局安全点上进行 轻量级锁 加锁:Displace mark word,对象头mark word通过CAS指向栈中锁记录 锁撤销:如果CAS替换回对象头失败,则升级成重量级锁 重量级锁 各种锁的比较 Java锁与线程的那些事 Java6及以上版本对synchronized的优化 ","date":"2022-08-31","objectID":"/java_concurrent/:3:1","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"volatile 实现原理 写volatile变量在编译时添加Lock指令 缓存一致性:每个处理器会通过总线嗅探出自己的工作内存中数据是否发生变化 happens-before关系的推导 内存语义 写volatile变量会重新刷新到主存中,其他线程读volatile变量会重新从主存中读取最新值 内存语义的实现 通过在特定位置处插入内存屏障来防止重排序 ","date":"2022-08-31","objectID":"/java_concurrent/:3:2","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"final 如何使用 变量 基本类型 类变量(static变量):只能在声明时赋值或者 在静态代码块中赋值 实例变量:声明时赋值,构造器以及非静态代码块中赋值 局部变量:有且仅有一次赋值机会 引用类型 final修饰的引用类型只保证引用的对象地址不变,其对象 的属性是可以改变的 方法 被final修饰的方法不能被子类重写,但是可以被重载 类 被final修饰的类,不能被子类继承 final的重排序规则 final域为基本类型 禁止对final于的写重排序到构造函数之外 禁止读对象的引用和读该对象包含的final域重排序 final域为引用类型 对一个final修饰的对象的成员域的写入,与随后在构造函数之外 把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的 final实现原理 插入StoreStore和LoadLoad内存屏障 finla引用不能从构造函数函数中“溢出”(this逃逸) ","date":"2022-08-31","objectID":"/java_concurrent/:3:3","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"三大性质 原子性 synchronzied 可见性 synchronized volatile 有序性 synchronized volatile ","date":"2022-08-31","objectID":"/java_concurrent/:3:4","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"Lock体系 ","date":"2022-08-31","objectID":"/java_concurrent/:4:0","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"Lock与synchronized的比较 Lock提供了基于API的可操作性,提供能可响应中断式获取锁,超时获取锁以及非阻塞式获取锁的特性 synchronized执行完同步块以及遇到异常会自动释放锁,而Lock要显示的调用unlock方法释放锁 ","date":"2022-08-31","objectID":"/java_concurrent/:4:1","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"AQS 设计意图(模板方法设计模式) AQS提供给同步组件实现者,为其屏蔽了同步状态的管理,线程排队等底层操作实现者只需要通过AQS提供的模板方法实现同步组件的语义即可 lock(同步组件)是面向使用者的,定义了接口,隐藏了实现细节 如何使用AQS实现自定义同步组件 重写protected方法,告诉AQS如何判断当前同步状态获取是否成功或者失败 同步组件调用AQS的模板方法,实现同步语义。而提供的模板方法又会调用被重写的方法 实现自定义同步组件时,推荐采用继承AQS的静态内部类 可重写的方法 AQS提供的模板方法 ","date":"2022-08-31","objectID":"/java_concurrent/:4:2","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"AQS源码解析 AQS同步队列的数据结构 带头结点的双向链表实现的队列 独占式锁 同步状态获取成功则退出;失败则通过addWaiter方法将当前线程 封装成节点加入同步队列,acquireQueued方法使得当前线程等待获取同步状态 如果获取同步状态并且是同步队列中的头结点,则表明获取锁成功,并唤醒后继结点 可响应中断式获取锁以及超时获取锁特性的实现原理 共享式锁 锁获取原理 锁释放原理 可响应中断式获取锁以及可超时获取锁特性的实现原理 ReentrantLock 重入锁的实现原理 公平锁的实现原理 非公平锁的实现原理 公平锁和非公平锁的比较 ReentrantReadWriteLock 如何表示读写状态的 低16位用来表示写状态,高16位用来表示读状态 WriteLock的获取和释放 当ReadLock已经被其他线程获取或者WriteLock被其他线程获取,当前线程 获取WriteLock失败;否则获取成功。支持重入性 WriteLock释放时将写状态通过CAS操作减一 ReadLock的获取和释放 当WriteLock已经被其他线程获取的话,ReadLock获取失败;否则获取成功。支持重入性 通过CAS操作将读状态减一 锁降级策略 按照WriteLock.lock()–\u003eReadLock.lock()–\u003eWriteLock.unlock()的顺序,WriteLock会降级为ReadLock 生成Condition等待队列 WriteLock可以通过newCondition方法生成Condition等待队列, 而ReadLock无法生成Conditon等待队列 应用场景 适用于读多写少的应用场景,比如缓存设计上 Condition机制 与Object的wait/notify机制相比 具有的特性 Condition能够支持不响应中断,而Object不支持 Lock能够支持多个Condition等待队列,而Object只能支持一个 Condition能够支持设置超时时间的await,而Object不能 与Object的wait/notify相对应的方法 针对Object的wait方法:await, awaitNanos,… 针对Object的notify/notifyAll方法:signal,signalAll方法 底层数据结构 复用AQS的Node类,由不带头结点的链表实现的队列 awiat实现原理 将调用await方法的线程封装成Node,尾插入到同步队列中,并通过LockSupport.park方法 将当前线程置于WAITING状态,直至其他线程通过signal/signalAll方法将其移入到同步队列中, 使其有机会在同步队列中通过自旋获取到Lock,从而当前线程才能从await方法处退出 signal/signalAll实现原理 将等待队列的队头结点移入到同步队列中 await和signal/signalAll的结合使用 ","date":"2022-08-31","objectID":"/java_concurrent/:4:3","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"LockSupport Java并发之AQS详解 Java并发包基石-AQS详解 ","date":"2022-08-31","objectID":"/java_concurrent/:4:4","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"并发容器 ","date":"2022-08-31","objectID":"/java_concurrent/:5:0","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"concurrentHashMap 关键属性 table:元素为Node类的哈希桶数组 nextTable:扩容时的新数组 sizeCtl:控制数组的大小 Unsafe u:提供对哈希桶数组元素的CAS操作 重要内部类 Node:实现Map.entry接口,存放key,value TreeNode:继承Node,会被封装成TreeBin TreeBin:进一步封装TreeNode,链表过长时转换成红黑树时使用 ForwardingNode:扩容时出现的特殊结点 涉及到的CAS操作 tabAt:查询哈希桶数组的元素 casTabAt:设置哈希桶数组中索引为i的元素 setTabAt:设置哈希桶数组中索引为i的元素 构造方法 数组长度总是会保证为2的幂次方 put执行流程 如果当前数组还未初始化,先进行初始化 spread方法重哈希(高16位和低16位异或操作),将哈希值与数组长度与运算,确定待插入结点的索引为i 当前哈希桶中i处为null,直接插入 i处结点不为null的话并且结点hash\u003e0,说明i处为链表头结点。遍历链表,遇到与key相同的结点则覆盖其value, 如果遍历完没有找到,则尾插入新结点 i处结点不为null的话并且结点状态为MOVED,则说明在扩容,帮助扩容 i处结点不为null的话并且结点位TreeBin,则使用红黑树的方式插入结点 插入新结点后,检查链表长度是否大于8,若大于,则转换成红黑树 检测数组长度,若超过临界值,则扩容 get执行流程 扩容机制 用于统计size的方法的执行流程 1.8版本的ConcurrentHashMap与之前版本的比较 减小锁粒度 采用了synchronized而不是lock,大量使用CAS操作 ","date":"2022-08-31","objectID":"/java_concurrent/:5:1","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"CopyOnWriteArrayList 实现原理 利用了读写分离的思想;当写线程写入数据的时候会复制新建一个新容器,当数据更新完成后,再将旧容器引用指向新容器。读线程感知数据更新是延时的,也就是说COW是牺牲了数据实时性而保证数据最终一致性 由于写线程写数据是在新容器写入的,因此读线程不会被阻塞 COW和ReentrantReadWriteLock的区别 相同点 两者都采用了读写分离的思想,并且读和 读线程之间都不会被阻塞 不同点 当写线程在写数据时,ReadWriteLock会阻塞读线程,而 由于COW采用了延时更新的策略,COW并不会阻塞读线程 ReadWriteLock保证了数据实时性而COW保证数据最终一致性 应用场景 适用于读多写少的场景,比如系统的黑名单,白名单设置 为什么具有弱一致性 COW的实现是采用数组,而数组的引用是volatile修饰,但是数组的元素并不是 volatile的。因此数据更新只有当volatile引用指向新数组时才会生效 COW的缺点 由于在写数据时,会复制,因此可能会出现内存使用瞬间增加, 导致minor GC和major GC 只具有数据最终一致性,对数据实时性要求高的场景不合适 ","date":"2022-08-31","objectID":"/java_concurrent/:5:2","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"ThreadLocal 实现思想 采用“空间换时间的”思想,每个线程拥有变量副本,达到 隔离线程的目的,线程间不受影响解决线程安全的问题 操作系统 set方法原理 数据存放在由当前线程Thread维护的ThreadLocalMap中,数据结构为 当前ThreadLocal实例为key,值为value的键值对 get方法原理 以当前ThreadLocal为键,从当前线程Thread维护的ThreadLocalMap中获取value remove方法原理 从当前线程Thread维护的ThreadLocalMap中删除以当前ThreadLocal实例为键的键值对 ThreadLocalMap 底层数据结构 键为ThreadLocal实例,值为value的Entry数组 数组大小为2的幂次方 键ThreadLocal为弱引用 set方法原理 计算ThreadLocal的hashcode 总是加上0x61c88647,这是“Fibonacci Hashing” 计算待插入的索引为i 采用与运算 如何解决hash冲突 当索引为i处有Entry的话(hash冲突),就采用线性探测,进行环形搜素 加载因子 ThreadLocalMap初始大小为16,加载因子为2/3 扩容resize 容量为原数组大小的两倍 getEntry方法原理 根据ThreadLocal的hashcode进行定位,如果所定位的Entry的key与所查找的key相同则直接返回,否则,环形向后继续探测。 remove原理 先找到对应的entry,然后让它的Key为null,之后再对其进行清理 ThreadLocal内存泄漏 造成内存泄露的原因 由于ThreadLocal在Entry中是弱引用,当外部ThreadLocal实例被置为null后,根据可达性分析,堆中ThreadLocal不可达,会被GC掉,因此就存在key为null的entry。无法通过key为null去访问entry,因此,就会存在threadRef-\u003ecurrentThread-\u003ethreadLocalMap-\u003eentry-\u003evalueRef-\u003evalueMemory引用链造成valueMemory不会被GC掉,造成内存泄漏 怎样来解决内存泄漏 关键方法cleanSomeSlots,expungeStaleEntry,replaceStaleEntry 在ThreadLocal的set,getEntry以及remove方法中都利用以上三个关键方法对潜在的内存泄漏进行处理 为什么要使用弱引用 如果使用强引用的话,即使显示对ThreadLocal的实例置为null的话,由于Thread,ThreadLocal以及 ThreadLocalMap引用链关系,ThreadLocal也不会被GC掉,反而会程序员带来困扰; 使用弱引用,尽管存在ThreadLocal内存泄漏的危险,但实际上已经对其进行了处理 ThreadLocal的最佳实践 使用完ThreadLocal后要remove掉 应用场景 hibernate管理seesion,每个线程维护其自身的session,彼此不干扰 用于解决对象不能被多个线程共享的问题 ","date":"2022-08-31","objectID":"/java_concurrent/:5:3","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"BlockingQueue BlockingQueue的基本操作 常用的BlockingQueue ArrayBlockingQueue:由数组实现的有界阻塞队列 LinkedBlockingQueue:由链表实现的有界阻塞队列,可指定长度,如果没有指定则为Integer.MAX_VALUE PriorityBlockingQueue:支持优先级的无界阻塞队列 SynchronousQueue:不存储任何元素阻塞队列 LinkedTransferQueue:由链表实现的无界阻塞队列 LinkedBlockingDeque:由链表实现的无界阻塞队列 DelayQueue:存在实现了Delayed接口的数据的无界阻塞队列 ArrayBlockingQueue与LinkedBlockingQueue实现原理 会有notFull和notEmpty两个等待队列,分别存放 被阻塞的插入数据线程以及被阻塞的消费数据的线程 ArrayBlockingQueue只有一个lock,而LinkedBlockingQueue有两个lock,因此LinkedBlockingQueue的并发度更高,吞吐量更大 通过put和take方法了解生产者-消费者的正确写法 ","date":"2022-08-31","objectID":"/java_concurrent/:5:4","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"ConcurrentLinkedQueue 实现原理 主要采用CAS操作以保证线程安全,并且采用了延时更新的策略,提高吞吐量 数据结构 由Node构成的链式队列 核心方法 offer方法实现原理 poll方法实现原理 HOPS延迟更新的设计意图 尽可能的减少CAS操作,以提升吞吐量和执行效率 ","date":"2022-08-31","objectID":"/java_concurrent/:5:5","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"线程池(Executor体系) ","date":"2022-08-31","objectID":"/java_concurrent/:6:0","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"ThreadPoolExecutor 为什么要使用线程池 降低资源损耗 提升系统响应速度 提高线程的可管理性 执行流程 核心线程corePool,阻塞队列workQueue以及最大线程池maxPool三级缓存的工作方式 构造器各个参数的意义 coolPoolSize:核心线程池的大小 maximumPoolSize:线程池最大容量 keepAliveTime:空闲线程可存活时间 unit:keepAliveTime的时间单位 workQueue:存放任务的阻塞队列 threadFactory:生产线程的工厂类 handler:饱和丢弃策略。共四种: AbortPolicy, CallerRunsPolicy, DiscardPolicy, DiscardOldestPolicy 如何关闭线程池 shutdown:正在执行任务的线程,将任务执行完。空闲线程以中断的方式关闭 shutdownNow:停止所有线程,包括正在执行任务的线程。返回未执行的任务列表 showdown将线程池状态设置为SHUTDOWN,而shutdownNow将线程池状态设置为STOP isTerminated来检查线程池是否已经关闭 如何配置线程池 CPU密集型:Ncpu+1 IO密集型:2Ncpu 任务按照IO密集型和CPU密集型进行拆分 ","date":"2022-08-31","objectID":"/java_concurrent/:6:1","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"ScheduledThreadPoolExecutor UML(类结构) 继承了ThreadPoolExecutor,并实现了ScheduledExecutorService 常用方法 可 延时执行任务:schedule(…..) 可周期执行任务:scheduledAtFixedRate(…)和scheduledWithFixedDelay scheduledAtFixedRate和scheduledWithFixedDelay的区别:…AtFixedRate不要求任务结束了才开始统计延时时间,而….WithFixedDelay要求从任务结束开始统计延时时间 ScheduledFutureTask 可周期执行的异步任务,每一次执行完后会重新设置任务下一次执行的任务, 并且会添加到阻塞队列中 DelayedWorkQueue 按优先级排序的有界阻塞队列,底层数据结构是堆 ","date":"2022-08-31","objectID":"/java_concurrent/:6:2","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"FutureTask FutureTask的几种状态 未启动(还未执行run方法),已启动(已执行run方法),已结束(正常结束,被取消,出现异常) get() 未启动和已启动状态,get方法会阻塞当前线程直到异步任务执行结束 cancel() 未启动状态时,调用cancel方法后该异步任务永远不会再执行 已启动状态,调用cancel方法后根据参数是否中断当前执行任务的线程 已结束状态,调用cancel方法时会返回false 应用场景 当一个线程需要等到另一个任务执行结束后才能继续进行时,可以使用futureTask 实现了Runnable接口 futureTask同样可以交由executor执行,获取直接调用run方法执行 ","date":"2022-08-31","objectID":"/java_concurrent/:6:3","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"原子操作类 ","date":"2022-08-31","objectID":"/java_concurrent/:7:0","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"实现原理 借住与Unsafe类的CAS操作,达到并发安全的目的 ","date":"2022-08-31","objectID":"/java_concurrent/:7:1","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"原子更新基本类型 AtomicInteger, AtomicLong,AtomicBoolean ","date":"2022-08-31","objectID":"/java_concurrent/:7:2","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"原子更新数组类型 AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray ","date":"2022-08-31","objectID":"/java_concurrent/:7:3","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"原子更新引用类型 AtomicReference,AtomicReferenceFieldUpdater,AtomicMarkableReference ","date":"2022-08-31","objectID":"/java_concurrent/:7:4","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"原子更新字段类型 AtomicIntegerFieldUpdater,AtomicLongUpdater,AtomicStampedReference ","date":"2022-08-31","objectID":"/java_concurrent/:7:5","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"并发工具 ","date":"2022-08-31","objectID":"/java_concurrent/:8:0","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"倒计时器CountDownLatch 当CountDownLatch维护的计数器减至为0的时候,调用await方法的线程才会继续往下执行,否则会阻塞等待 适用于一个线程需要等待其他多个线程执行结果的应用场景操作系统 ","date":"2022-08-31","objectID":"/java_concurrent/:8:1","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"循环栅栏CyclicBarrier -当一组线程都达到了“临界点”时,所有的线程才能继续往前执行,否则阻塞等待 ","date":"2022-08-31","objectID":"/java_concurrent/:8:2","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"CountDownLatch和CyclicBarrier的比较 CyclicBarrier能够复用,而CountDownLatch维护的倒计数器不能复用 CyclicBarrier会在await处阻塞等待,而CountDownLatch在await出不会阻塞等待 CyclicBarrier提供了例如isBroken,getNumerWaiting等方法能够 查询当前状态,而CountDownLatch提供的方法较少 ","date":"2022-08-31","objectID":"/java_concurrent/:8:3","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"资源访问控制Semaphore 适用于对特定资源需要控制能够并发访问资源的线程个数。需要先执行acquire方法获取许可证,如果获取成功后线程才能往下继续执行,否则只能阻塞等待;使用完后需要 用release方法归还许可 ","date":"2022-08-31","objectID":"/java_concurrent/:8:4","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"数据交换Exchanger 为两个线程提供了一个同步点,当两个线程都达到了同步点之后就可以使用exchange方法,互相交换数据;如果一个线程先达到了同步点,会在同步点阻塞等待直到另外一个线程也到达同步点 ","date":"2022-08-31","objectID":"/java_concurrent/:8:5","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"并发实践 ","date":"2022-08-31","objectID":"/java_concurrent/:9:0","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"生产者-消费者问题 使用Object的wait/notifyAll方式实现 使用Object的消息通知机制可能存在的问题 notify过早,wait线程无法再获取到通知以至于一直阻塞等待。解决办法:添加状态标志 wait条件变化。解决方法:使用while进行wait条件的判断,而不是在if中进行判断 “假死”状态:使用notifyAll而不是notify 标准范式 永远在while中对wait条件进行判断,而不是在if中进行判断 使用notifyAll进行通知,而不要使用notify进行通知 使用lock的condition的await/signalAll方式实现 使用blockingQueue方式实现 由于BlockingQueue有可阻塞的插入和删除数据的put和take方法,因此,在实现上比使用Object和lock的方式更加简洁 ","date":"2022-08-31","objectID":"/java_concurrent/:9:1","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":[],"content":"相关阅读 tobebetterjavaer并发编程 Java并发专题 \u003c仅供个人收藏学习\u003e ","date":"2022-08-31","objectID":"/java_concurrent/:10:0","tags":["java","concurrent"],"title":"JAVA并发","uri":"/java_concurrent/"},{"categories":null,"content":"背景 随着业务的快速发展,业务复杂度不断增加,线上系统环境有任何细小波动,对整个业务都产生巨大的影响,甚至造成灾难性的雪崩效应,开发人员对此必须立即响应,快速解决问题。 如何提供问题排查效率呢?最有效的方法就是通过系统日志。但是如果保证系统日志全面就必须打印所有系统和业务日志。这就带来另一个问题,日志量暴涨,日志除了能帮助我们解决问题外,同时会造成系统性能下降,在这种背景下,日志级别动态调整组件可以在需要时实时调整线上日志输出级别。 ","date":"2022-08-24","objectID":"/dynamic-log-level/:1:0","tags":["log","java"],"title":"动态日志级别","uri":"/dynamic-log-level/"},{"categories":null,"content":"实现 @GetMapping(value = \"/all-log-level\") public String getAllLogLevel() throws Exception{ return LogLevelDynamicChangeService.getLoggerList(); } @GetMapping(value = \"/change-log-level\") public String changeLogLevel(@RequestParam(LogConstant.LOG_NAME_KEY) String name, @RequestParam(LogConstant.LOG_LEVEL_KEY) String level) { List\u003cLoggerSetting\u003e settings = new ArrayList\u003c\u003e(); LoggerSetting loggerSetting = new LoggerSetting(); loggerSetting.setName(name); loggerSetting.setLevel(level); settings.add(loggerSetting); final boolean ret = LogLevelDynamicChangeService.setLogLevel(settings); return ret ? level : \"error\"; } @Component public class LogLevelDynamicChangeService { private Map\u003cString, Object\u003e LoggerMap = new HashMap\u003c\u003e(); private String logFrameworkType; /** * 1. 初始化:确定所使用的日志框架,获取配置文件中所有的 Logger 内存实例, * 并将它们的引用缓存到 Map 容器中 */ @PostConstruct public void init() { String type = StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr(); if (LogConstant.LOGBACK_LOGGER_FACTORY.equals(type)) { logFrameworkType = LogFrameworkType.LOGBACK; ch.qos.logback.classic.LoggerContext LoggerContext = (ch.qos.logback.classic.LoggerContext) LoggerFactory.getILoggerFactory(); for (ch.qos.logback.classic.Logger Logger : LoggerContext.getLoggerList()) { if (Logger.getLevel() != null) { LoggerMap.put(Logger.getName(), Logger); } } ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); LoggerMap.put(rootLogger.getName(), rootLogger); } // else if (LogConstant.LOG4J_LOGGER_FACTORY.equals(type)) { // logFrameworkType = LogFrameworkType.LOG4J; // Enumeration enumeration = org.apache.log4j.LogManager.getCurrentLoggers(); // while (enumeration.hasMoreElements()) { // Logger Logger = (org.apache.log4j.Logger)enumeration.nextElement(); // if (Logger.getLevel() != null) { // LoggerMap.put(Logger.getName(), Logger); // } // } // org.apache.log4j.Logger rootLogger = org.apache.log4j.LogManager.getRootLogger(); // LoggerMap.put(rootLogger.getName(), rootLogger); // } else if (LogConstant.LOG4J2_LOGGER_FACTORY.equals(type)) { // logFrameworkType = LogFrameworkType.LOG4J2; // org.apache.logging.log4j.core.LoggerContext LoggerContext = (org.apache.logging.log4j.core.LoggerContext) org.apache.logging.log4j.LogManager.getContext(false); // Map\u003cString, org.apache.logging.log4j.core.config.LoggerConfig\u003e map = LoggerContext.getConfiguration().getLoggers(); // for (org.apache.logging.log4j.core.config.LoggerConfig LoggerConfig : map.values()) { // String key = LoggerConfig.getName(); // if (StringUtils.isEmpty(key)) { // key = \"root\"; // } // LoggerMap.put(key, LoggerConfig); // } // } else { logFrameworkType = LogFrameworkType.UNKNOWN; log.error(\"Log 框架无法识别 : type={}\", type); } } /** * 2. 获取 Logger 列表:从本地 Map 容器取出 * @throws JSONException */ public String getLoggerList() throws Exception { JSONObject result = new JSONObject(); result.put(LogConstant.LOG_FRAMEWORK_TYPE_KEY, logFrameworkType); JSONArray LoggerList = new JSONArray(); for (ConcurrentMap.Entry\u003cString, Object\u003e entry : LoggerMap.entrySet()) { JSONObject LoggerJSON = new JSONObject(); LoggerJSON.put(LogConstant.LOG_NAME_KEY, entry.getKey()); if (logFrameworkType == LogFrameworkType.LOGBACK) { ch.qos.logback.classic.Logger targetLogger = (ch.qos.logback.classic.Logger) entry.getValue(); LoggerJSON.put(LogConstant.LOG_LEVEL_KEY, targetLogger.getLevel().toString()); } // else if (logFrameworkType == LogFrameworkType.LOG4J) { // org.apache.log.Logger targetLogger = (org.apache.log4j.Logger) entry.getValue(); // LoggerJSON.put(LogConstant.LOG_LEVEL_NAME, targetLogger.getLevel().toString()); // } else if (logFrameworkType == LogFrameworkType.LOG4J2) { // org.apache.logging.log4j.core.config.LoggerConfig targetLogger = (org.apache.logging.log4j.core.config.LoggerConfig) entry.getValue(); // LoggerJSON.put(LogConstant.LOG_LEVEL_NAME, targetLogger.getLevel().toString()); // } else { LoggerJSON.put(LogConstant.LOG_LEVEL_KEY, \"Logger 的类型未知 , 无法处理 !\"); } ","date":"2022-08-24","objectID":"/dynamic-log-level/:2:0","tags":["log","java"],"title":"动态日志级别","uri":"/dynamic-log-level/"},{"categories":null,"content":"前言 至少 30 年以前,一些软件设计人员就已经意识到领域建模和设计的重要性,并形成一种思潮,Eric Evans 将其定义为领域驱动设计(Domain-Driven Design,简称 DDD)。在互联网开发“小步快跑,迭代试错”的大环境下,DDD 似乎是一种比较“古老而缓慢”的思想。然而,由于互联网公司也逐渐深入实体经济,业务日益复杂,我们在开发中也越来越多地遇到传统行业软件开发中所面临的问题。本文就先来讲一下这些问题,然后再尝试在实践中用 DDD 的思想来解决这些问题。 ","date":"2022-08-24","objectID":"/ddd/:0:1","tags":["architectures","java"],"title":"DDD领域驱动设计","uri":"/ddd/"},{"categories":null,"content":"问题 过度耦合 业务初期,我们的功能大都非常简单,普通的 CRUD 就能满足,此时系统是清晰的。随着迭代的不断演化,业务逻辑变得越来越复杂,我们的系统也越来越冗杂。模块彼此关联,谁都很难说清模块的具体功能意图是啥。修改一个功能时,往往光回溯该功能需要的修改点就需要很长时间,更别提修改带来的不可预知的影响面。 下图是一个常见的系统耦合病例。 订单服务接口中提供了查询、创建订单相关的接口,也提供了订单评价、支付、保险的接口。同时我们的表也是一个订单大表,包含了非常多字段。在我们维护代码 时,牵一发而动全身,很可能只是想改下评价相关的功能,却影响到了创单核心路径。虽然我们可以通过测试保证功能完备性,但当我们在订单领域有大量需求同时并行开发时,改动重叠、恶性循环、疲于奔命修改各种问题。 上述问题,归根到底在于系统架构不清晰,划分出来的模块内聚度低、高耦合。 有一种解决方案,按照演进式设计的理论,让系统的设计随着系统实现的增长而增长。我们不需要作提前设计,就让系统伴随业务成长而演进。这当然是可行的,敏捷实践中的重构、测试驱动设计及持续集成可以对付各种混乱问题。重构——保持行为不变的代码改善清除了不协调的局部设计,测试驱动设计确保对系统的更改不会导致系统丢失或破坏现有功能,持续集成则为团队提供了同一代码库。 在这三种实践中,重构是克服演进式设计中大杂烩问题的主力,通过在单独的类及方法级别上做一系列小步重构来完成。我们可以很容易重构出一个独立的类来放某些通用的逻辑,但是你会发现你很难给它一个业务上的含义,只能给予一个技术维度描绘的含义。这会带来什么问题呢?新同学并不总是知道对通用逻辑的改动或获取来自该类。显然,制定项目规范并不是好的 idea。我们又闻到了代码即将腐败的味道。 事实上,你可能意识到问题之所在。在解决现实问题时,我们会将问题映射到脑海中的概念模型,在模型中解决问题,再将解决方案转换为实际的代码。上述问题在于我们解决了设计到代码之间的重构,但提炼出来的设计模型,并不具有实际的业务含义,这就导致在开发新需求时,其他同学并不能很自然地将业务问题映射到该设计模型。设计似乎变成了重构者的自娱自乐,代码继续腐败,重新重构……无休止的循环。 用 DDD 则可以很好地解决领域模型到设计模型的同步、演化,最后再将反映了领域的设计模型转为实际的代码。 注:模型是我们解决实际问题所抽象出来的概念模型,领域模型则表达与业务相 关的事实;设计模型则描述了所要构建的系统。 贫血症和失忆症 贫血领域对象 贫血领域对象(Anemic Domain Object)是指仅用作数据载体,而没有行为和动作的领域对象。 在我们习惯了 J2EE 的开发模式后,Action/Service/DAO 这种分层模式,会很自然地写出过程式代码,而学到的很多关于 OO 理论的也毫无用武之地。使用这种开发方式,对象只是数据的载体,没有行为。以数据为中心,以数据库 ER 设计作驱动。分层架构在这种开发模式下,可以理解为是对数据移动、处理和实现的过程。以笔者最近开发的系统抽奖平台为例: 场景需求 奖池里配置了很多奖项,我们需要按运营预先配置的概率抽中一个奖项。 实现非常简单,生成一个随机数,匹配符合该随机数生成概率的奖项即可。 贫血模型实现方案 先设计奖池和奖项的库表配置。 设计 AwardPool 和 Award 两个对象,只有简单的 get 和 set 属性的方法 class AwardPool { int awardPoolId; List\u003cAward\u003e awards; public List\u003cAward\u003e getAwards() { return awards; } public void setAwards(List\u003cAward\u003e awards) { this.awards = awards; } ...... } class Award { int awardId; int probability;// 概率 ...... } Service 代码实现 设计一个 LotteryService,在其中的 drawLottery() 方法写服务逻辑 AwardPool awardPool = awardPoolDao.getAwardPool(poolId);//sql 查询,将数据映射到 AwardPool 对象 for (Award award : awardPool.getAwards()) { // 寻找到符合 award.getProbability() 概率的 award } 按照我们通常思路实现,可以发现:在业务领域里非常重要的抽奖,我的业务逻辑都是写在 Service 中的,Award 充其量只是个数据载体,没有任何行为。简单的业务系统采用这种贫血模型和过程化设计是没有问题的,但在业务逻辑复杂了,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症。更好的是采用领域模型的开发方式,将数据和行为封装在一起,并与现实世界中的业务对象相映射。各类具备明确的职责划分,将领域逻辑分散到领域对象中。继续举我们上述抽奖的例子,使用概率选择对应的奖品就应当放到 AwardPool 类中。 ","date":"2022-08-24","objectID":"/ddd/:0:2","tags":["architectures","java"],"title":"DDD领域驱动设计","uri":"/ddd/"},{"categories":null,"content":"为什么选择 DDD ? 软件系统复杂性应对 解决复杂和大规模软件的武器可以被粗略地归为三类:抽象、分治和知识。 分治 把问题空间分割为规模更小且易于处理的若干子问题。分割后的问题需要足够小,以便一个人单枪匹马就能够解决他们;其次,必须考虑如何将分割后的各个部分装配为整体。分割得越合理越易于理解,在装配成整体时,所需跟踪的细节也就越少。即更容易设计各部分的协作方式。评判什么是分治得好,即高内聚低耦合。 抽象 使用抽象能够精简问题空间,而且问题越小越容易理解。举个例子,从北京到上海出差,可以先理解为使用交通工具前往,但不需要一开始就想清楚到底是高铁还是飞机,以及乘坐他们需要注意什么。 知识 顾名思义,DDD 可以认为是知识的一种。 DDD 提供了这样的知识手段,让我们知道如何抽象出限界上下文以及如何去分治。 与微服务架构相得益彰 微服务架构众所周知,此处不做赘述。我们创建微服务时,需要创建一个高内聚、低耦合的微服务。而 DDD 中的限界上下文则完美匹配微服务要求,可以将该限界上下文理解为一个微服务进程。 上述是从更直观的角度来描述两者的相似处。 在系统复杂之后,我们都需要用分治来拆解问题。一般有两种方式,技术维度和业务维度。技术维度是类似 MVC 这样,业务维度则是指按业务领域来划分系统。 微服务架构更强调从业务维度去做分治来应对系统复杂度,而 DDD 也是同样的 着重业务视角。 如果两者在追求的目标(业务维度)达到了上下文的统一,那么在具体做法上有 什么联系和不同呢? 我们将架构设计活动精简为以下三个层面: 业务架构——根据业务需求设计业务模块及其关系 系统架构——设计系统和子系统的模块 技术架构——决定采用的技术及框架 以上三种活动在实际开发中是有先后顺序的,但不一定孰先孰后。在我们解决常规套路问题时,我们会很自然地往熟悉的分层架构套(先确定系统架构),或者用PHP 开发很快(先确定技术架构),在业务不复杂时,这样是合理的。 跳过业务架构设计出来的架构关注点不在业务响应上,可能就是个大泥球,在面临需求迭代或响应市场变化时就很痛苦。 DDD 的核心诉求就是将业务架构映射到系统架构上,在响应业务变化调整业务架构时,也随之变化系统架构。而微服务追求业务层面的复用,设计出来的系统架构和业务一致;在技术架构上则系统模块之间充分解耦,可以自由地选择合适的技术架构,去中心化地治理技术和数据。 可以参见下图来更好地理解双方之间的协作关系: ","date":"2022-08-24","objectID":"/ddd/:0:3","tags":["architectures","java"],"title":"DDD领域驱动设计","uri":"/ddd/"},{"categories":null,"content":"如何实践 DDD 我们将通过上文提到的抽奖平台,来详细介绍我们如何通过 DDD 来解构一个中 型的基于微服务架构的系统,从而做到系统的高内聚、低耦合。 首先看下抽奖系统的大致需求: 运营——可以配置一个抽奖活动,该活动面向一个特定的用户群体,并针对一个 用户群体发放一批不同类型的奖品(优惠券,激活码,实物奖品等)。 用户 - 通过活动页面参与不同类型的抽奖活动。 设计领域模型的一般步骤如下: 根据需求划分出初步的领域和限界上下文,以及上下文之间的关系; 进一步分析每个上下文内部,识别出哪些是实体,哪些是值对象; 对实体、值对象进行关联和聚合,划分出聚合的范畴和聚合根; 为聚合根设计仓储,并思考实体或值对象的创建方式; 在工程中实践领域模型,并在实践中检验模型的合理性,倒推模型中不足的 地方并重构。 战略建模 战略和战术设计是站在 DDD 的角度进行划分。战略设计侧重于高层次、宏观上 去划分和集成限界上下文,而战术设计则关注更具体使用建模工具来细化上下文。 领域 现实世界中,领域包含了问题域和解系统。一般认为软件是对现实世界的部分模 拟。在 DDD 中,解系统可以映射为一个个限界上下文,限界上下文就是软件对于问 题域的一个特定的、有限的解决方案。 限界上下文 限界上下文 一个由显示边界限定的特定职责。领域模型便存在于这个边界之内。在边界内, 每一个模型概念,包括它的属性和操作,都具有特殊的含义。 一个给定的业务领域会包含多个限界上下文,想与一个限界上下文沟通,则需要 通过显示边界进行通信。系统通过确定的限界上下文来进行解耦,而每一个上下文内 部紧密组织,职责明确,具有较高的内聚性。 一个很形象的隐喻:细胞质所以能够存在,是因为细胞膜限定了什么在细胞内, 什么在细胞外,并且确定了什么物质可以通过细胞膜。 划分限界上下文 划分限界上下文,不管是 Eric Evans 还是 Vaughn Vernon,在他们的大作里 都没有怎么提及。 显然我们不应该按技术架构或者开发任务来创建限界上下文,应该按照语义的边 界来考虑。 我们的实践是,考虑产品所讲的通用语言,从中提取一些术语称之为概念对象, 寻找对象之间的联系;或者从需求里提取一些动词,观察动词和对象之间的关系;我 们将紧耦合的各自圈在一起,观察他们内在的联系,从而形成对应的界限上下文。形 成之后,我们可以尝试用语言来描述下界限上下文的职责,看它是否清晰、准确、简 洁和完整。简言之,限界上下文应该从需求出发,按领域划分。 前文提到,我们的用户划分为运营和用户。其中,运营对抽奖活动的配置十分复 杂但相对低频。用户对这些抽奖活动配置的使用是高频次且无感知的。根据这样的业 务特点,我们首先将抽奖平台划分为 C 端抽奖和 M 端抽奖管理平台两个子域,让两 者完全解耦。 在确认了 M 端领域和 C 端的限界上下文后,我们再对各自上下文内部进行限界 上下文的划分。下面我们用 C 端进行举例。 产品的需求概述如下: 抽奖活动有活动限制,例如用户的抽奖次数限制,抽奖的开始和结束的时间等; 一个抽奖活动包含多个奖品,可以针对一个或多个用户群体; 奖品有自身的奖品配置,例如库存量,被抽中的概率等,最多被一个用户抽中的次数等等; 用户群体有多种区别方式,如按照用户所在城市区分,按照新老客区分等; 活动具有风控配置,能够限制用户参与抽奖的频率。 根据产品的需求,我们提取了一些关键性的概念作为子域,形成我们的限界上下文。 首先,抽奖上下文作为整个领域的核心,承担着用户抽奖的核心业务,抽奖中包 含了奖品和用户群体的概念。 在设计初期,我们曾经考虑划分出抽奖和发奖两个领域,前者负责选奖,后者 负责将选中的奖品发放出去。但在实际开发过程中,我们发现这两部分的逻辑 紧密连接,难以拆分。并且单纯的发奖逻辑足够简单,仅仅是调用第三方服务 进行发奖,不足以独立出来成为一个领域。 对于活动的限制,我们定义了活动准入的通用语言,将活动开始 / 结束时间,活 动可参与次数等限制条件都收拢到活动准入上下文中。 对于抽奖的奖品库存量,由于库存的行为与奖品本身相对解耦,库存关注点更多 是库存内容的核销,且库存本身具备通用性,可以被奖品之外的内容使用,因此我们 定义了独立的库存上下文。 由于 C 端存在一些刷单行为,我们根据产品需求定义了风控上下文,用于对活 动进行风控。 最后,活动准入、风控、抽奖等领域都涉及到一些次数的限制,因此我们定义了 计数上下文。 可以看到,通过 DDD 的限界上下文划分,我们界定出抽奖、活动准入、风控、 计数、库存等五个上下文,每个上下文在系统中都高度内聚。 上下文映射图 在进行上下文划分之后,我们还需要进一步梳理上下文之间的关系。 康威(梅尔·康威)定律 任何组织在设计一套系统(广义概念上的系统)时,所交付的设计方案在结构上 都与该组织的沟通结构保持一致。 康威定律告诉我们,系统结构应尽量的与组织结构保持一致。这里,我们认为团 队结构(无论是内部组织还是团队间组织)就是组织结构,限界上下文就是系统的业 务结构。因此,团队结构应该和限界上下文保持一致。 梳理清楚上下文之间的关系,从团队内部的关系来看,有如下好处: 1. 任务更好拆分,一个开发人员可以全身心的投入到相关的一个单独的上下 文中; 2. 沟通更加顺畅,一个上下文可以明确自己对其他上下文的依赖关系,从而使 得团队内开发直接更好的对接。 从团队间的关系来看,明确的上下文关系能够带来如下帮助: 222 \u003e 2017 年美团点评技术文章精选 1. 每个团队在它的上下文中能够更加明确自己领域内的概念,因为上下文是领 域的解系统; 2. 对于限界上下文之间发生交互,团队与上下文的一致性,能够保证我们明确 对接的团队和依赖的上下游。 限界上下文之间的映射关系 合作关系(Partnership):两个上下文紧密合作的关系,一荣俱荣,一损俱损。 共享内核(Shared Kernel):两个上下文依赖部分共享的模型。 客户方 - 供应方开发(Customer-Supplier Development):上下文之间有组 织的上下游依赖。 遵奉者(Conformist):下游上下文只能盲目依赖上游上下文。 防腐层(Anticorruption Layer):一个上下文通过一些适配和转换与另一个上 下文交互。 开放主机服务(Open Host Service):定义一种协议来让其他上下文来对本上 下文进行访问。 发布语言(Published Language):通常与 OHS 一起使用,用于定义开放主 机的协议。 大泥球(Big Ball of Mud):混杂在一起的上下文关系,边界不清晰。 另谋他路(SeparateWay):两个完全没有任何联系的上下文。 上文定义了上下文映射间的关系,经过我们的反复斟酌,抽奖平台上下文的映射 关系图如下: 由于抽奖,风控,活动准入,库存,计数五个上下文都处在抽奖领域的内部,所 以它们之间符合“一荣俱荣,一损俱损”的合作关系(PartnerShip,简称 PS)。 同时,抽奖上下文在进行发券动作时,会依赖券码、平台券、外卖券三个上 下文。抽奖上下文通过防腐层(Anticorruption Layer,ACL)对三个上下文进行 了隔离,而三个券上下文通过开放主机服务(Open Host Service)作为发布语言 (Published Language)对抽奖上下文提供访问机制。 通过上下文映射关系,我们明确的限制了限界上下文的耦合性,即在抽奖平台 中,无论是上下文内部交互(合作关系)还是与外部上下文交互(防腐层),耦合度都 限定在数据耦合(Data Coupling)的层级。 战术建模——细化上下文 梳理清楚上下文之间的关系后,我们需要从战术层面上剖析上下文内部的组织关 系。首先看下 DDD 中的一些定义。 实体 当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。 例:最简单的,公安系统的身份信息录入,对于人的模拟,即认为是实体,因为 每个人是独一无二的,且其具有唯一标识(如公安系统分发的身份证号码)。 在实践上建议将属性的验证放到实体中。 值对象 当一个对象用于对事务进行描述而没有唯一标识时,它被称作值对象(Value Object)。 例:比如颜色信息,我们只需要知道 {“name”:\" 黑色 “,“css”:\"#000000”} 这样 的值信息就能够满足要求了,这避免了我们对标识追踪带来的系统复杂性。 值对象很重要,在习惯了使用数据库的数据建模后,很容易将所有对象看作实 体。使用值对象,可以更好地做系统优化、精简设计。 它具有不变性、相等性和可替换性。 在实践中,需要保证值对象创建后就不能被修改,即不允许外部再修改其属性。 在不同上下文集成时,会出现模型概念的公用,如商品模型会存在于电商的各个上下 文中。在订单上下文中如果你只关注下单时商品信息快照,那么将商品对象视为值对 象是很好的选择。 聚合根 Aggregate( 聚合)是一组相关对象的集合,作为一个整体被外界访问,聚合根 (Aggregate Root)是这个聚合的根节点。 聚合是一个非常重要的概念,核心领域往往都需要用聚合来表达。其次,聚合在 技术上有非常高的价值,可以指导详细设计。 聚合由根实体,值对象和实体组成。 如何创建好的聚合? 边界内的内容具有一致性:在一个事务中只修改一个聚合实例。如果你发现边 界内很难接受强一致,不管是出于性能或产品需求的考虑,应该考虑剥离出独 立的聚合,采用最终一致的方式。 设计小聚合:大部分的聚合都可以只包含根实体,而无需包含其他实体。即使","date":"2022-08-24","objectID":"/ddd/:0:4","tags":["architectures","java"],"title":"DDD领域驱动设计","uri":"/ddd/"},{"categories":null,"content":"参考书籍 Eric Evans. 领域驱动设计 . 赵俐 盛海艳 刘霞等译 . 人民邮电出版社,2016. Vaughn Vernon. 实现领域驱动设计 . 滕云译 . 电子工业出版社,2014. ","date":"2022-08-24","objectID":"/ddd/:0:5","tags":["architectures","java"],"title":"DDD领域驱动设计","uri":"/ddd/"},{"categories":null,"content":"作者简介 文彬、子维,美团点评资深研发工程师,毕业于南京大学,现从事美团外卖营销相关的研发工作。 最后打波硬广,美团外卖上海研发中心长期招聘前端、客户端、后端、数据仓库和数据挖掘相关 的工程师,欢迎有兴趣的同学发送简历到 wenbin.lu@dianping.com ","date":"2022-08-24","objectID":"/ddd/:0:6","tags":["architectures","java"],"title":"DDD领域驱动设计","uri":"/ddd/"}]