又踩坑了。ThreadPoolExecutor?

问题是出现在 24 号的时候,当时有台 weblogic 实例出现阻塞,运维 dump 线程栈后重启了,有个同事进行分析。

该同事分析线程栈后认为问题出在一个被外部系统调用的接口,这个接口收到请求后会从数据库查询数据,然后把数据处理后发提交到线程池,再由线程池异步发送到 MQ 服务器,调用方监听 MQ 进行数据处理,接口代码大致如下:

继续阅读

踩坑 Druid 连接池

这周有个应用的一个实例出现了没有响应,庆幸运维那边在重启前做了线程和内存的 dump 。

线程 dump 文件打开一看,竟然4万多行。。后来发现同事用一个可视化工具来分析线程栈,我也把这个工具加入工具箱:IBM Thread and Monitor Dump Analyzer for Java

下图是这个工具的方法栈分析视图:

可以按线程名词、状态、方法栈的深度来进行排序。

下面说说这次踩的坑。

继续阅读

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

继续阅读