Spring 事务原理与集成 MyBatis 事务管理

1. 事务管理器抽象

一个事务管理器只需要三个基本的能力:获取一个事务、提交事务、回滚事务。

public interface PlatformTransactionManager extends TransactionManager {
    // 获取一个事务
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
            throws TransactionException;

    // 提交事务
    void commit(TransactionStatus status) throws TransactionException;

    // 回滚事务
    void rollback(TransactionStatus status) throws TransactionException;
}

DataSourceTransactionManagerAutoConfiguration 配置类导入了数据库事务管理器 DataSourceTransactionManager

2. 事务同步回调钩子

事务同步回调钩子让我们有机会在事务的各个阶段加入一些协调的动作。

public interface TransactionSynchronization extends Flushable {
    /** Completion status in case of proper commit. */
    int STATUS_COMMITTED = 0;

    /** Completion status in case of proper rollback. */
    int STATUS_ROLLED_BACK = 1;

    /** Completion status in case of heuristic mixed completion or system errors. */
    int STATUS_UNKNOWN = 2;

    default void suspend() {}
    default void resume() {}
    default void flush() {}
    default void beforeCommit(boolean readOnly) {}
    default void beforeCompletion() {}
    default void afterCommit() {}
    default void afterCompletion(int status) {}
}

继续阅读

新一代 GC 神器 ZGC

0. 标记-复制算法

标记-复制算法分三个阶段:

  • 标记阶段:从 GC Roots 出发,标记存活对象。
  • 转移阶段:把存活对象复制到新的内存地址,原来的内存空间变成可回收的。
  • 重定位阶段:存活对象被复制到其他地方后,所有指向对象旧地址的指针都要调整到对象新的地址上。

1. 概述

Z Garbage Collector,即ZGC,是一个可伸缩的、低延迟的垃圾收集器。只支持 64 位的系统。

ZGC 未分代,每次 GC 都是 FullGC 。

1.1 ZGC 的设计目标

  • 亚毫秒级的最大暂停时间。
  • 暂停时间不随堆、存活对象数量、根集合大小的增长而增长。
  • 可处理的堆大小从 8MB 到 16TB。

总的来说,ZGC 具有如下特性:

  • 并发
  • 基于 Region
  • 压缩
  • NUMA 友好
  • 使用着色指针
  • 使用读屏障 load barriers

截至 JDK 14,ZGC 已在主流的操作系统上获得支持。

继续阅读

ConcurrentHashMap

1. 为什么 key 和 value 不允许为 null

HashMap 中允许 key、value 为 null,key 为 null 时哈希值为 0 。

ConcurrentHashMap 中都不能为 null 是因为作者 Doug Lea 认为:在并发编程中,null 值容易引来歧义,当调用 get(key) 返回 null 时,无法确定是 key 对应的 value 就是 null ,还是说这个 key 不存在。
非并发编程中可以通过调用 containsKey 方法来判断,但并发编程中无法保证这两个方法之间没有其他线程来修改 key 值。

2. 并行度 concurrencyLevel

ConcurrentHashMap 构造函数里有个参数 concurrencyLevel 提供了建议支持的并行度。

在 JDK 1.7 的实现里,分段锁段数组的大小由 concurrencyLevel 决定,为大于 concurrencyLevel 的最小的 2 次幂值,但不能超过 2^16,初始化后不能修改。

在 JDK 1.8 里,concurrencyLevel 会影响 Node 数组的初始容量,由于并发粒度是数组的元素,从而影响并发度。

concurrencyLevel 并不是指定了精确的并发度。

继续阅读

HashMap

本文源码基于 JDK 1.8.0_101 。

在 JDK 1.8 之前,HashMap 采用 槽数组 + 单链表 来实现;

在 JDK 1.8 开始采用 槽数组 + 单链表 + 红黑树 来实现。

1. 为什么引入红黑树?

解决哈希冲突时、链表过长导致访问效率低下的问题。

为什么是红黑树不是其他树:
二叉排序树在极端情况下会退化成线性结构。
平衡二叉树(AVL树)是严格平衡树,在增加或删除节点时,旋转次数比红黑树要多。红黑树的统计性能高于 AVL 树。

红黑树特性,RBT树上的每个节点,都要遵循下面的规则:
① 每个节点都是红色或者黑色;
② 根节点必须始终是黑色;
③ 没有两个相邻的红色节点;
④ 对每个结点,从该结点到其子孙节点的所有路径上包含相同数目的黑结点。

继续阅读

HashTable 有什么奇怪的知识?

本文涉及的源码基于 JDK 1.8.0_101 。

1. HashTable

  1. 采用数组 + 链表(表头插入)方式解决哈希冲突。
  2. 所有的 public 方法都用 synchronized 修饰以保证线程安全。
  3. 在构造时就初始化槽数组(默认大小为 11)。
  4. 键、值 都不能为 null
  5. 指定 key 的目标槽的定位逻辑:(key.hashCode() & 0x7FFFFFFF) % table.length,掩码+求模。
  6. 槽数组的最大尺寸为 MAX_ARRAY_SIZE: Integer.MAX_VALUE - 8(减 8 是因为一些 JVM 会在数组里保留一些 header words)。
  7. 扩容逻辑为 两倍 + 1。
  8. 阈值为: (int)(capacity * loadFactor),但不能超过 MAX_ARRAY_SIZE + 1

继续阅读

双重检查加锁 与 volatile

一个问题:双重检查加锁为什么用了 volatile 就可以正确工作?

反过来问:不用 volatile 修饰 resource 属性有什么问题?

双重检查加锁的简单示例:

public class DoubleCheckedLocking {
    private static Resource resource;

    public static Resource getInstance() {
        if (resource == null) {     // LL01
            synchronized (DoubleCheckedLocking.class) {
                if (resource == null) {
                    resource = new Resource();
                }
            }
        }
        return resource;
    }
}

继续阅读

FutureTask (JDK1.8)

本文基于 JDK 1.8.0_101 对应的源码。

FutureTask 表示一个可取消的异步计算的任务,其构造器接收一个 Runnable/Callable 表示异步计算的任务。虽然说 FutureTask 支持取消,但任务一旦开始执行,执行过程就可能不受取消操作的影响。

当任务没有执行结束时,所有获取结果的操作都会阻塞等待。

以栈的方式维护等待获取结果的线程,因为不用考虑公平性的问题,用栈更简单。

FutureTask 在 JDK 1.7 的时候是基于 AQS 实现的,可以看这里

总体上:

  • volatile int state 控制执行状态,
  • volatile Thread runner 防止重复执行,
  • volatile WaitNode waiters 以栈形式维护等待线程。

继续阅读

再读 AQS

基于 JDK 1.8.0_101 。

AQS 的核心:两种模式、一个状态、一个同步等待队列、条件对象。

  • 两种模式:是指支持独占模式或共享模式,两者不能并存,要么独占、要么共享。
  • 一个状态:状态是 AQS 的灵魂,根据所处的模式来定义其值的含义。
  • 一个同步等待队列:是线程为进入指定的模式、但还不能进入时,需要在同步等待队列上进行等待。该队列是双向链表,以方便支持取消操作。
  • 条件对象:只能在独占模式下使用,线程可以在条件对象上进行等待、由其他线程在满足条件时唤醒。
    > 每个条件对象其实是一个单向链表实现的条件等待队列,在给定条件对象上等待的线程会进入条件等待队列,等待其他线程来唤醒。
    > 进入等待之前,线程会先进入条件等待队列、保存自己持有时的状态值,然后释放独占模式,进入挂起线程、等待唤醒,唤醒后重新尝试进入独占模式,进入后再继续执行。

继续阅读

Disruptor 为什么那么快?

一个简短的笔记。

Disruptor 快的核心秘诀是:基于数组、空间局部性良好、消除伪共享、无锁、支持批量消费。

1. 基于数组的内存局部性

Disruptor 底层是一个固定大小的环形数组,初始化的时候会顺序创建与数组长度一样数量的对象,以便让这些对象在内存上尽量挨着的。

顺序创建与随机创建在内存上的差别可以对比下面两张图(来自极客时间专栏《Java并发编程实战》):

往队列里写入事件时,需要把事件的属性拷贝到这些预创建好的对象里,以保持内存的局部性。

这里的一个隐性要求是:事件的属性应该尽量是基本类型,如果是对象类型,存的是引用,访问引用指向的值时可能就是随机的内存访问。

继续阅读

RateLimiter 浅析

本文基于 Guava-18.0.jar 。

0. 概述

RateLimiter 是令牌桶思想的一个实现,可实现流量整形、资源访问速率控制。

与信号量对比:

  • 一旦从 RateLimiter 获得许可,不需要释放。信号量分配后必须释放。
  • RateLimiter 控制的是速率,以配置的速率分发许可,速率不变时单位时间内分发的许可量是恒定的。信号量控制的是并发访问的数量,单位时间内分配的次数跟使用许可的时长有关,每次申请使用的时间越短,则单位时间内能分配的次数就越多。

RateLimiter 请求许可的数量不会对请求本身产生抑制影响,但会对下一个请求产生抑制。例如一个请求很大数量许可的请求到达空闲 RateLimiter 时,它将马上获得许可,但下一个请求会被抑制,从而为前面昂贵的请求付出代价。

RateLimiter 申请许可时,可能会阻塞、也可能不会,是否阻塞取决于上一次分配许可的时间和当前请求的许可数量。

RateLimiter 可以配置一个 warnup 热身周期,在这个周期内每秒发出的许可数量稳步增长直至达到稳定的速率。简单说是慢启动吧。

1. 使用示例:

// 每秒2个的速率提交任务
final RateLimiter rateLimiter = RateLimiter.create(2.0);
void submitTasks(List tasks, Executor executor) {
    for (Runnable task : tasks) {
        rateLimiter.acquire(); // may wait
        executor.execute(task);
    }
}

继续阅读