杂记

解决 tail -f 碰到日志绕接时,停止工作的问题

日志绕接: 将零散日志整合, 重命名的过程

  • 为啥会停止工作:

  • tail -f是以文件描述符为标志来监测文件变化,当出现日志绕接时,会有重命名、创建文件的动作,这会促使文件描述符发生变化,至此tail -f 工作失效。

  • 解决办法:

使用tail -F命令,该模式是以文件名来监测日志,而且文件不可访问时,还会重试,正好满足日志绕接的场景。

反射修改常量池里的String

  • 为了安全, 很多规范要求敏感字符串用完后手动清空
// 通过反射来填充String字符串持有的char数组value,实现从内在中抹除敏感数据的目标。
public static void clear(String s) {
    if (StringUtils.isEmpty(s)) {
        return ;
    }

    Class<? extends String> clazz = s.getClass();
    try {
        Field field = clazz.getDeclaredField("value");
        field.setAccessible(true);
        Object obj = field.get(s); // 获取字符串的char[]数组
        if (obj instanceof char[]) {
            clear((char[]) obj); // 方法重载, 置为"000"
        }
    } catch (...) {
        ...
    }
}
  • 我们知道, 堆上池外String类底层是char[]. 那如果这个字符串是属于字符串常量池,会发生什么?
String test = "123";
clear(test);
sout(test); // 打印"000"
sout("123"); // 打印"000"
sout("123".equals("000")); // true

String test2 = new String(char[]{'4', '5', '6'});
clear(test2);
sout(test2); // 打印"456"
sout(new String(char[]{'4', '5', '6'})); // 打印"456"
sout(new String(char[]{'4', '5', '6'}).equals("000")); // false

修改字符串池之外的字符串是不会发生上一步的奇怪BUG

  • 如何保证入参都是字符串池之外的字符串呢?
// 添加一个地址比较的逻辑,用于检查该入参是否属于字符串池
// 此方法适用于jdk7以前, jdk8+的intern方法不一定拷贝到常量池, 即使s不在池中, s == s.intern()也可以为true
public static void clear(String s) {
    if (StringUtils.isEmpty(s) || s == s.intern()) {
        return ;
    }

    Class<? extends String> clazz = s.getClass();
    try {
        Field field = clazz.getDeclaredField("value");
        field.setAccessible(true);
        Object obj = field.get(s); // 获取字符串的char[]数组
        if (obj instanceof char[]) {
            clear((char[]) obj); // 方法重载, 置为"000"
        }
    } catch (...) {
        ...
    }
}

Map的红黑树体现在哪?

  • java.util.TreeMap#fixAfterDeletion
  • java.util.TreeMap#fixAfterInsertion
  • java.util.HashMap#rotateXXX

JVM类型推导

Integer i = null;
String.valueOf(i); // "null"
String.valueOf((Integer)null); // "null"
String.valueOf(null); // NPE

前两个传参有具体类型, 所以命中具体方法

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

最后一个传参null无法推导, 同时命中上方法和

public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}

JVM会倾向于选择形参类型更具体的public String(char value[])方法, 故NPE

spring aop针对同一个方法有多个切面,默认优先级是怎么样的?

  • 在同一切面类内, 按照切入点的定义顺序来织入
  • 在不同的切面类内, 都实现了Ordered接口, 按切入点的Order数值从小到达织入.
  • 在不同的切面类内, 存在没实现Ordered接口的类, 则切入点的顺序不确定.

mybatis执行SQL查询列表,得不到结果时,返回为null还是空list/set?

为了接口幂等性, 返回空集合

spring中如何有两个相同beanId的bean声明,哪个生效?

  • bean不存在覆盖一说,发现有重复beanId的bean时则不加载
  • 如果明确要做spring bean覆盖,则应该使用spring父子容器, 这时应考虑加载顺序
    • spring bean的加载顺序首先是根据依赖关系,生成DAG(有向无环图),然后依次加载。
    • 通过指定@Order/@Primary来主动干涉spring bean的创建优先级。
    • 无任何扰动的情况下,spring加载路径中,越靠后的越先加载

使用并行流处理数据时,出现异常,在主线程会收到异常吗?

  • 只要有一条数据出现异常,主线程就会收到异常。
  • 不论哪一条数据执行有异常,都不影响其他数据的执行。
List<Integer> result = 
    List.of(1, 1, 2, 3, 4, 5, 6, 7, 8, 91)
        .parallelStream()
        .map(item -> {
            System.out.println("map item [" + item + "]...");
            if (item < 10) {
                throw new RuntimeException(String.valueOf(item));
            }
            return item * 10;}
        ).toList();

如果线程里没有捕获异常,这个异常会被怎么处理?

  • java.lang.Thread类提供方法setUncaughtExceptionHandler,用于指定未被捕获的异常的回调处理类,可使用它做一个兜底操作。但是前提是初始化Thread时需要设置该回调,如果没有设置的话,那未捕获的异常将会丢失。

最佳实践:
在线程的边界,捕获所有异常(Throwable),保证出了异常后系统能有兜底,这样也不用依赖于线程创建时是否添加异常处理回调

现在有100万条黑名单ip列表, 当客户端携带ip地址进行访问时, 需要快速判断是否匹配, 怎么设计?

  • 如果要求严格匹配, 则构造字典树, 时间复杂度O(1), 空间复杂度O(n)
  • 如果允许一定误判率, 则构造布隆过滤器, 时间复杂度O(1), 空间复杂度O(n/8)

curl命令

在linux命令行, 使用curl命令调试某接口, 需要将url用引号包装, 防止linux将路径参数中的&解释为命令串行

# 错误, 会解释为
# curl -X POST http://localhost:3306/book?name=somebook和price=10两条命令
curl -X POST http://localhost:3306/book?name=somebook&price=10
# 正确
curl -X POST 'http://localhost:3306/book?name=somebook&price=10'

常用List的实现与选择

ArrayList

  • 使用数组作为基础数据结构,实现了RandomAccess接口
  • 大小调整,正常默认扩展原来大小的一半与需要添加的大小(针对addAll)的较大值, 使用System.arraycopy方法将原来的数组内容拷贝到新数组中
  • 无参构造初始化为空数组,当第一次添加元素时默认初始化大小10
  • 数组最大大小为Integer.MAX_SIZE-8, 减8是因为在有的JVM实现中可能会在数组中添加一个头,超过大小会造成内存溢出,但是如果添加元素超过Interger.MAX_SIZE-8而小于Interger.MAX_SIZE时,数组的大小也会进行扩展,但是具体是否会引起异常就会据不同的JVM实现可能有所不同
  • 保存了一个修改次数的计数modCount,每次添加删除元素都会增加该计数, 在使用迭代器遍历的时候,如果两次迭代modCount发生变化则会抛出ConcurrentModificationException,所以不能在迭代中修改列表的数据
  • 线程不安全

LinkedList

  • 双向链表作为基础数据结构, 实现了Queue接口,所以也是一个双向队列
  • 使用下标访问的时候,LinkedList根据待访问的index确定从头指针开始查找还是从尾指针开始查找,保证了至多O(n/2)的时间复杂度
  • 保存了一个修改次数的计数modCount,每次添加删除元素都会增加该计数, 在使用迭代器遍历的时候,如果两次迭代modCount发生变化则会抛出ConcurrentModificationException,所以不能在迭代中修改列表的数据
  • 线程不安全

Vector

  • 与ArrayList不同的是Vector几乎所有的方法都添加了synchronized保护来保证线程安全,但是这样大大降低了访问的性能
  • Vector构造方法提供了内部数组的capacityIncrement,如果不设置默认是倍增的
  • Vector这种对所有方法的加锁行为是无效而且没有用的, 因为大多数时候我们对List的操作可能不会只是一个操作,会是一组操作, 需要事务型API

CopyOnWriteArrayList

  • 当数据发生变化的时候会重新生成一个新的数组,并将原来数组的内容拷贝到新数组中,并将内部的数组引用改为新数组
  • 使用ReentrantLock来保证多个线程的并行修改
  • 迭代器遍历时可以对列表进行修改,不会抛出ConcurrentModificationException,因为修改的时候数组数据会变成新的数组,而遍历的仍然是老数组, 不是同一个modCount
  • 修改添加操作会非常耗时,所以只有当至少90%以上的操作都是读操作,很少有写操作的时候才会使用

代码重构

  • 处理逻辑不变, 待处理数据可变 -> 抽取数据
  • 待处理数据不变, 处理逻辑可变 -> 用函数式接口抽取出算子, 将算子作为参数
class A {
    public Object foo1(String param1, String param2) {
        if (Collection.size() == 1) {
            Collection.get(0).util1(param1, param2);
        }
        for(var unit : Collection.get()) {
            unit.util1(param1, param2);
        }
        return ...;
    }

    public Object foo2(Integer param1, String param2) {
        if (Collection.size() == 1) {
            Collection.get(0).util2(param1, param2);
        }
        for(var unit : Collection.get()) {
            unit.util2(param1, param2);
        }
        return ...;
    }

    public Object foo3() {
        if (Collection.size() == 1) {
            Collection.get(0).util3();
        }
        for(var unit : Collection.get()) {
            unit.util3();
        }
        return ...;
    }
}
class B {
    public Object foo1(String param1, String param2) {
        return bar(unit.getClass()::util1);
    }

    public Object foo2(Integer param1, String param2) {
        return bar(unit.getClass()::util2);
    }

    public Object foo3() {
        return bar(unit.getClass()::util3);
    }

    private <T> T bar(Function<unit.getClass(), T> func) {
        if (Collection.size() == 1) {
            func.apply(Collection.get(0));
        }
        for(var unit : Collection.get()) {
            func.apply(unit);
        }
        return T;
    }
}

java基本类型占空间大小

  • 对于局部变量的基本类型, 因为使用栈帧里的局部变量表的slot存储, 所以long, double占 2slot(8字节), 其余类型(包括引用)都 1slot(4字节, 32位机|8字节, 64位机)
  • 对于成员变量的基本类型, 因为是在堆上动态分配空间, 所以boolean/byte 1字节, char/short 2字节, int/float 4字节, double/long 8字节

新建对象的方法

  • new
  • 反射
  • clone
  • 反序列化
  • Unsafe.allocateInstance

volatile原理?Happens-before内存模型?

如果操作 X happens-before 操作 Y,那么 X 的结果对于 Y 可见。

  • happens-before模型包括如下几种:

    • 线程内部字节码顺序, 注意如果操作没有依赖关系, jvm在编译阶段可能已将字节码重排
    • unlock -> lock
    • volatile写 -> volatile读
    • Thread.start() -> run()的首个操作操作
    • run()的末尾操作 -> Thread.isAlive()/Thread.join()
    • Thread.interrupt() -> catch InterruptedException/Thread.interrupted()
    • 构造器的末尾操作 -> finalizer的首个操作
  • 如下示例代码, 由于方法局部变量之间没有依赖性, 所以在多线程情况下的重排可能引发意外结果

int a=0, b=0;
 
public void method1() {
  int r2 = a;
  b = 1;
}

public void method2() {
  int r1 = b;
  a = 2;
}
  • 使用volatile修饰b, 强制规定b的写操作 先于 b的读操作, 结果一定为r1 = 1, r2 = 0;
    • 本质上是利用happens-before规定了多线程的运行顺序
int a=0;
volatile int b=0;
 
public void method1() {
  int r2 = a; // 1. read a
  b = 1; // 2. write b
}
 
public void method2() {
  int r1 = b; // 3. read b
  a = 2; // 4. write a
}
  • happens-before底层实现原理: 内存屏障
  • 比如规则: volatile写 -> volatile读, 那么volatile写读之间的内存访问应该被屏蔽, 即禁止jvm将 访问内存字节码 重排到volatile写读之间
    • 实现上, 并没有真正禁止重排, 而是遇到volatile写就强制刷新缓存, 将当前volatile值刷回内存, 同时使其他线程持有的同一缓存行失效(类似于先写DB后删redis缓存的强一致性策略), 保证了即使重排字节码, 也能去内存读到最新值的效果
    • volatile缺点: 不保证原子性, 如果频繁刷新缓存性能会退化(伪共享问题), 因此只适用于读多写少, 单线程写场景

synchronized锁升级

每个对象都拥有字段: markword对象头, 其末尾3位标识该对象的锁状态

  • 无锁(001)表示所有线程都能访问
  • 偏向锁(101, 默认)针对的是锁仅会被同一线程持有的情况
    • 只会在第一次请求时采用 CAS 操作,在锁对象的markword记录下当前线程的地址
    • 即使持有线程已经结束访问, 也不会主动清除标记, 方便这个线程重复加锁时直接放行
    • 如果其他线程申请加锁, 则判断markword记录的当前持有线程是否存活
      • 如果持有线程已终止, 则改线程地址为新的申请线程
      • 否则, 表明出现了多线程竞争, 针对单线程的偏向锁已经不能满足, 升级为轻量级锁
  • 轻量级锁/自旋锁(00)针对的是多个线程在不同时间段申请同一把锁, 且每个线程持有时间较短的情况
    • 为了减少阻塞操作带来的上下文切换, 竞争线程通过自旋操作保持在用户态, 并不断CAS尝试修改标志为00
    • 这在持有时间较短的情况下是可以接受的(比如本地加锁), 但如果持有时间很长(比如加锁后RPC), 空转自旋会消耗CPU, 影响吞吐
    • 因此, 当竞争线程自旋超过一定次数(根据历史自旋次数决定), 或竞争线程很多时, 表示持有时间会较长, 让竞争者都阻塞, 减少CPU消耗, 升级为重量级锁
  • 重量级锁(10)会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。
    • 类似threadlocal, 每个对象都拥有字段: 锁计数器, 持有者指针, 阻塞队列
    • 当进入monitorEnter, 如果计数器=0, 表明可以加锁; 如果计数器>0, 则继续判断锁持有者是否就是访问者(可重入锁), 如果是则可以加锁, 否则进入阻塞队列
    • 当进入monitorExit, 计数器--, 如果计数器减到0, 表示锁释放了, 从阻塞队列中取出线程尝试加锁

泛型 & 类型擦除

  • 对于List<? extends XXX>类型的list, 其add和get操作的对象都是XXX类型, 同时单独保存?的类型, 需要用到时再强转为?
posted @   Blazer96  阅读(190)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示