封装ThreadLocal

news2024/11/23 22:18:10
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

为什么要封装ThreadLocal?

原因有两点:

1、对于Thread,如果希望在Interceptor中存入UserInfo并在Service层通过ThreadLocal把UserInfo出来,必须保证Interceptor和Service此时用的是同一个ThreadLocal。

但是一个对象如何同时出现在Interceptor和Service呢?各自new一个ThreadLocal可不行,因为此时是两个对象了。比如,在Interceptor创建的对象是紫霞,而Service创建的是青霞,紫霞在至尊宝存入的东西,后面的青霞可没办法出来,因为Thread内部的ThreadLocalMap是以ThreadLocal作为key的(看上面爱心的key)。

但如果我们在ThreadLocalUtil中new一个ThreadLocal对象作为成员变量,就可以在Service中取出来了:

即:把ThreadLocal对象封装在ThreadLocalUtil中,分别在Interceptor和Service中使用它。

2、原生的ThreadLocal无法满足复杂的业务场景。

比如现在我封装了一个最简单的ThreadLocal(装饰者模式,为的是解决第一个问题):

/**
 * @author mx
 */
public class MyThreadLocal {

    private MyThreadLocal() {
    }

    private static final ThreadLocal<Object> THREAD_CONTEXT = new ThreadLocal<>();

    public static void put(Object obj) {
        THREAD_CONTEXT.set(obj);
    }

    public static Object get() {
        return THREAD_CONTEXT.get();
    }

    public static void remove() {
        THREAD_CONTEXT.remove();
    }
}

MyThreadLocal确实解决了第一个问题,复用了ThreadLocal,保证了Interceptor和Service用到的ThreadLocal是同一个对象。

但是,有两个缺陷:

  • 无法存取多个不同的值
  • 语意不明

比如,Service层希望往ThreadLocal里再添加一个Score对象,好让DAO层能获取到。你要怎么做?

另外,MyThreadLocal.get()其实很突兀,语意不明,光看代码你根本不知道get出来的是什么东西。

基于以上两个原因,我们必须封装ThreadLocal。对于第二个问题,其实可以考虑把原先的value改为Map类型。比如原本是 threadLocal1:User或者threadLocal1:Score,确实只能存一个值,而且很容易发生覆盖。但是如果把Map作为value存进去,形成双层Map就灵活多了:

{
	"threadLocal1" : {
        "USER_INFO" : User,
        "SCORE" : Score
    }
}

思路分析到这,我们开始写代码。一般情况下,我们只需要考虑一个ThreadLocal和多个Thread,这也是实际编程最常见的方式,所以下面的代码只会封装一个ThreadLocalUtil,里面也只有一个ThreadLocal。

ThreadLocalUtil第一版

/**
 * @author mx
 */
public class ThreadLocalUtil {

    private ThreadLocalUtil() {
    }

    /**
     * ThreadLocal是紫霞仙子,至尊宝是Thread
     * ThreadLocal的泛型规定了紫霞仙子劈开至尊宝时,能给他心里塞的东西的类型。
     * <p>
     * 比如
     * 将ThreadLocal泛型指定为String,那么造了一个ThreadLocalMap后,这个map只能存 threadLocal:"这是字符串" 这样的键值对
     * 将ThreadLocal泛型指定为Integer,那么造了一个ThreadLocalMap后,这个map只能存 threadLocal:1111111111 这样的键值对
     *
     * 由于单纯的value会发生值覆盖,所以我们使用Map<String, Object>作为value
     */
    private static final ThreadLocal<Map<String, Object>> THREAD_CONTEXT = new ThreadLocal<>();


    /**
     * 存入线程变量
     *
     * @param key
     * @param object
     */
    public static void put(String key, Object object) {
        /**
         * 至尊宝(一个Thread)经过这段代码,遇到了紫霞(THREAD_CONTEXT)。大家可以点进get()看看,内部操作是:
         * 1.把至尊宝的心取出来(从Thread中取出ThreadLocalMap)
         *
         * ThreadLocalMap的构造类似于这样
         * {
         * ...THREAD_CONTEXT: {
         * ........."USER_INFO":"{'name':'bravo', 'age':18}",
         * ........."SCORE":"{'Math':99, 'English': 97}"
         * ......}
         * }
         *
         * 2.ThreadLocalMap.Entry e = map.getEntry(this); 把自己(THREAD_CONTEXT)作为key,取出属于自己的value,此时value是一个Map<String, Object>。
         * 3.所以最终THREAD_CONTEXT.get()返回的Map<String, Object> map
         *
         */
        Map<String, Object> map = THREAD_CONTEXT.get();
        // 第一次从ThreadLocalMap中根据threadLocal取出的value可能是null
        if (map == null) {
            map = new HashMap<>();
            // 把map作为value放进去
            THREAD_CONTEXT.set(map);
        }
        /**
         * 假设本次存的是 USER_INFO:{"name":"bravo", "age":18}
         * 此时ThreadLocalMap中的结构是
         * {
         * ...THREAD_CONTEXT: {
         * ........."USER_INFO":"{'name':'bravo', 'age':18}",
         * ......}
         * }
         *
         */
        map.put(key, object);
    }

    /**
     * 取出线程变量
     *
     * @param key
     * @return
     */
    public static Object get(String key) {
        // 先获取Map
        Map<String, Object> map = THREAD_CONTEXT.get();
        // 从Map中得到USER_INFO
        return map != null ? map.get(key) : null;
    }

    /**
     * 移除当前线程的指定变量
     * 比如把
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ......}
     * }
     * 变成
     * {
     * ...THREAD_CONTEXT: {
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ......}
     * }
     * 并不是移除所有,而是只移除USER_INFO
     *
     * @param key
     */
    public static void remove(String key) {
        Map<String, Object> map = THREAD_CONTEXT.get();
        map.remove(key);
    }

    /**
     * 移除当前线程的所有变量
     * 比如把
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ......}
     * }
     * 变成
     * {
     * }
     */
    public static void clear() {
        THREAD_CONTEXT.remove();
    }
}

建议大家从上面的MyThreadLocal开始,尝试自己一步步封装,ThreadLocalUtil第一步也不难,完全可以自己写。

ThreadLocalUtil第二版

上面的版本其实马马虎虎能用了,就是看起来不是特别优雅,很多地方需要判断null。如果你希望自己的工具类优雅些,逼格高一点,可以尝试下面这种:

/**
 * @author mx
 */
public class ThreadLocalUtil {

    private ThreadLocalUtil() {
    }

    /**
     * 注意右边new的不是原生的ThreadLocal,而是我自定义的MapThreadLocal,它继承自ThreadLocal
     *
     * @see MapThreadLocal
     */
    private final static ThreadLocal<Map<String, Object>> THREAD_CONTEXT = new MapThreadLocal();

    /**
     * 根据key获取value
     * 比如key为USER_INFO,则返回"{'name':'bravo', 'age':18}"
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     *
     * @param key
     * @return
     */
    public static Object get(String key) {
        // getContextMap()表示要先获取THREAD_CONTEXT的value,也就是Map<String, Object>。然后再从Map<String, Object>中根据key获取
        return getContextMap().get(key);
    }

    /**
     * put操作,原理同上
     *
     * @param key
     * @param value
     */
    public static void put(String key, Object value) {
        getContextMap().put(key, value);
    }

    /**
     * 清除map里的某个值
     * 比如把
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     * 变成
     * {
     * ...THREAD_CONTEXT: {
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     *
     * @param key
     * @return
     */
    public static Object remove(String key) {
        return getContextMap().remove(key);
    }

    /**
     * 清除整个Map<String, Object>
     * 比如把
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     * 变成
     * {
     * ...THREAD_CONTEXT: {}
     * }
     */
    public static void remove() {
        getContextMap().clear();
    }

    /**
     * 从ThreadLocalMap中清除当前ThreadLocal存储的内容
     * 比如把
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     * 变成
     * {
     * }
     */
    public static void clear() {
        THREAD_CONTEXT.remove();
    }

    /**
     * 从ThreadLocalMap
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     * 中获取Map<String, Object>
     * {
     * ..."USER_INFO":"{'name':'bravo', 'age':18}",
     * ..."SCORE":"{'Math':99, 'English': 97}"
     * }
     *
     * @return
     */
    private static Map<String, Object> getContextMap() {
        return THREAD_CONTEXT.get();
    }

    /**
     * 内部类,继承自ThreadLocal,和第一版一样,仍旧指定value为Map<String, Object>
     * 之所以要自定义MapThreadLocal,是为了重写原生ThreadLocal的initialValue()
     * 把ThreadLocal第一版中判断null的操作隐藏掉,让代码优雅一些(但对于初学者来说,理解难度也提升了)
     */
    private static class MapThreadLocal extends ThreadLocal<Map<String, Object>> {

        @Override
        protected Map<String, Object> initialValue() {
            return new HashMap<String, Object>(8) {

                private static final long serialVersionUID = 3637958959138295593L;

                @Override
                public Object put(String key, Object value) {
                    return super.put(key, value);
                }
            };
        }
    }
}

第二版的难点有两个:

  • 多了一个getContextMap(),部分人会晕。其实这个操作就是得到当前ThreadLocal对应Map<String, Object>
  • 为什么重写initialValue()可以避免判断null?

另外,不用担心每次都会创建新的Map覆盖原有的,get()方法内部本身会判断,如果已经有ThreadLocalMap其实是直接取值返回的。

如果还是觉得难理解,我建议取消getContextMap(),把里面的代码拷贝到各个方法中,好理解些。

最后的最后,不要因为这个工具类是自己封装的就怀疑是不是会重新导致线程安全问题。

只有同时满足下面3个条件,才有可能发生线程安全问题:

  • 多线程环境
  • 有共享数据
  • 有多条语句操作共享数据/单条语句本身非原子操作

但实际上,ThreadLocal的机制本身就避免了资源共享...因为每个线程内部都有自己的ThreadLocalMap(每个线程都有自己的资源,相互独立)。

所以记住,ThreadLocal本身和线程安全没啥关系,但你可以用它来解决线程安全问题,而且它的解决办法很粗暴,就是从根源上杜绝了资源共享。

之前说最后应该调用threadLocal.remove(),而对应ThreadLocalUtil,应该调用clear(),它对应的才是threadLocal.remove()。

ThreadLocalUtil第三版

之前封装ThreadLocal时一直在解决两个问题:

  • 原生的ThreadLocal对每个Thread的操作是基于单值的Key-Value,而我们期望基于Key-MapValue的操作
  • 如果不重写initValue(),需要在外部处理Map的初始化问题

对于initValue()的重写,其实不需要专门写一个内部类(很多人不习惯内部类),有两种替代方式:

  • 给THREAD_CONTEXT赋值时,直接new ThreadLocal()并用匿名类方式重写initValue()
  • 让ThreadLocalUtil继承ThreadLocal,然后重写initValue()

第一种方式最简单,这里演示第二种。

另外,之前在知乎专栏讨论过,ThreadLocalMap是定义在ThreadLocal内部的,由于包权限问题,我们无法直接使用。而我们的ThreadLocalUtil其实本质是就像个Map,所以第三版我改了名字,干脆咱也叫ThreadLocalMap,就当Map使用,只不过是线程内共享的。

当然,你也可以有更好的封装,可以下方留言:

/**
 * @author mx
 */
public class ThreadLocalMap extends ThreadLocal<Map<String, Object>> {

    private ThreadLocalMap() {
    }

    private final static ThreadLocal<Map<String, Object>> THREAD_CONTEXT = new ThreadLocalMap();

    /**
     * 根据key获取value
     * 比如key为USER_INFO,则返回"{'name':'bravo', 'age':18}"
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     *
     * @param key
     * @return
     */
    public static Object get(String key) {
        // getContextMap()表示要先获取THREAD_CONTEXT的value,也就是Map<String, Object>。然后再从Map<String, Object>中根据key获取
        return THREAD_CONTEXT.get().get(key);
    }

    /**
     * put操作,原理同上
     *
     * @param key
     * @param value
     */
    public static void put(String key, Object value) {
        THREAD_CONTEXT.get().put(key, value);
    }

    /**
     * 清除map里的某个值
     * 比如把
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     * 变成
     * {
     * ...THREAD_CONTEXT: {
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     *
     * @param key
     * @return
     */
    public static Object remove(String key) {
        return THREAD_CONTEXT.get().remove(key);
    }

    /**
     * 清除整个Map<String, Object>
     * 比如把
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     * 变成
     * {
     * ...THREAD_CONTEXT: {}
     * }
     */
    public static void clear() {
        THREAD_CONTEXT.get().clear();
    }

    /**
     * 从ThreadLocalMap中清除当前ThreadLocal存储的内容
     * 比如把
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     * 变成
     * {
     * }
     */
    public static void clearAll() {
        THREAD_CONTEXT.remove();
    }

    @Override
    protected Map<String, Object> initialValue() {
        return new HashMap<String, Object>(8) {

            private static final long serialVersionUID = 3637958959138295593L;

            @Override
            public Object put(String key, Object value) {
                return super.put(key, value);
            }
        };
    }

}

测试用例:

/**
 * @author mx
 */
public class ThreadLocalMapTest {

    public static void main(String[] args) {

        ThreadLocalMap.put("mainKey", "mainValue");

        new Thread(()->{
            ThreadLocalMap.put("threadKey", "threadValue");

            System.out.println("get main value in thread:" + ThreadLocalMap.get("mainKey"));
            System.out.println("get thread value in thread:" + ThreadLocalMap.get("threadKey"));
        }).start();

        System.out.println("get thread value in main:" + ThreadLocalMap.get("threadKey"));
        System.out.println("get main value in main:" + ThreadLocalMap.get("mainKey"));

    }

}

ThreadLocalUtil第四版

public class ThreadLocalMap {

    private ThreadLocalMap() {
    }

    /**
     * ThreadLocal的静态方法withInitial()会返回一个SuppliedThreadLocal对象
     * 而SuppliedThreadLocal<T> extends ThreadLocal<T>
     * 我们存进去的Map会作为的返回值:
     * protected T initialValue() {
     *    return supplier.get();
     * }
     * 
     * 所以也相当于重写了initialValue()
     * 
     */
    private final static ThreadLocal<Map<String, Object>> THREAD_CONTEXT = ThreadLocal.withInitial(
            () -> new HashMap<>(8)
    );

    /**
     * 根据key获取value
     * 比如key为USER_INFO,则返回"{'name':'bravo', 'age':18}"
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     *
     * @param key
     * @return
     */
    public static Object get(String key) {
        // getContextMap()表示要先获取THREAD_CONTEXT的value,也就是Map<String, Object>。然后再从Map<String, Object>中根据key获取
        return THREAD_CONTEXT.get().get(key);
    }

    /**
     * put操作,原理同上
     *
     * @param key
     * @param value
     */
    public static void put(String key, Object value) {
        THREAD_CONTEXT.get().put(key, value);
    }

    /**
     * 清除map里的某个值
     * 比如把
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     * 变成
     * {
     * ...THREAD_CONTEXT: {
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     *
     * @param key
     * @return
     */
    public static Object remove(String key) {
        return THREAD_CONTEXT.get().remove(key);
    }

    /**
     * 清除整个Map<String, Object>
     * 比如把
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     * 变成
     * {
     * ...THREAD_CONTEXT: {}
     * }
     */
    public static void clear() {
        THREAD_CONTEXT.get().clear();
    }

    /**
     * 从ThreadLocalMap中清除当前ThreadLocal存储的内容
     * 比如把
     * {
     * ...THREAD_CONTEXT: {
     * ........."USER_INFO":"{'name':'bravo', 'age':18}",
     * ........."SCORE":"{'Math':99, 'English': 97}"
     * ...}
     * }
     * 变成
     * {
     * }
     */
    public static void clearAll() {
        THREAD_CONTEXT.remove();
    }

}

Spring对ThreadLocal的封装

比如编写AOP日志时,经常会用到的RequestContextHolder,其实内部也维护了ThreadLocal。

那么Spring是如何做到remove的呢?使用过滤器(我们使用了拦截器)。

对于ThreadLocal的应用还有很多很多,这里就举这么一个例子叭~

补充:线程重用导致用户信息错乱的Bug

虽然小册两篇ThreadLocal相关的文章都反复强调用完之后最好及时remove(),但似乎都没有给出特别具有说服力的案例。最近在看极客时间朱晔老师的《Java业务开发常见错误100例》时,发现一个很不错的案例,这里特别拿来补充。

贴一段里面的代码:


private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);

@GetMapping("wrong")
public Map wrong(@RequestParam("userId") Integer userId) {
    //设置用户信息之前先查询一次ThreadLocal中的用户信息
    String before  = Thread.currentThread().getName() + ":" + currentUser.get();
    //设置用户信息到ThreadLocal
    currentUser.set(userId);
    //设置用户信息之后再查询一次ThreadLocal中的用户信息
    String after  = Thread.currentThread().getName() + ":" + currentUser.get();
    //汇总输出两次查询结果
    Map result = new HashMap();
    result.put("before", before);
    result.put("after", after);
    return result;
}

为了更明显地看到这个BUG,可以将Tomcat线程池的最大连接数设置为1:

server.tomcat.max-threads=1

分别请求两次:

也就是说,由于Tomcat连接池的线程数有限(比如极端情况下max-thread=1),所以必然存在线程复用。如果两个请求复用一个Thread且ThreadLocal没有及时remove,那么上一个请求设置在Thread.ThreadLocalMap中的值就会污染本次请求。

所以应该保证每次使用后及时remove():


@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {
    String before  = Thread.currentThread().getName() + ":" + currentUser.get();
    currentUser.set(userId);
    try {
        String after = Thread.currentThread().getName() + ":" + currentUser.get();
        Map result = new HashMap();
        result.put("before", before);
        result.put("after", after);
        return result;
    } finally {
        //在finally代码块中删除ThreadLocal中的数据,确保数据不串
        currentUser.remove();
    }
}

或者像Spring一样放在filter或者interceptor中remove()。

说一个小插曲:

之前有一次面试时,我提到项目中使用了BaseController,里面封装了ThreadLocal,可以获取Interceptor中存入的用户信息,然后面试官问我是否了解分布式场景下ThreadLocal导致的用户信息混乱的问题。我当时有点懵逼,关注点全在分布式场景上是否会产生这种BUG。其实不论是单体应用还是分布式应用,都有可能出现这个BUG。但为什么我们没遇到呢?不是因为项目小、并发低,而是我们根本不会像上面demo那样,上来就获取ThreadLocal里的内容,我们通常是在Interceptor中先设置值,然后Controller/Service中获取值,也就是说每次都是先覆盖、再取值,此时上次的value早就没了。

当然,还是推荐本次请求的值在响应时就remove,不要留到下次请求去覆盖,很容易出错,也容易造成内存泄漏。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1288258.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

geemap学习笔记020:如何搜索Earth Engine Python脚本

前言 本节内容比较简单&#xff0c;但是对于自主学习比较重要&#xff0c;JavaScript提供了很多的示例代码&#xff0c;为了便于学习&#xff0c;geemap将其转为了Python代码。 Earth Engine Python脚本 import ee import geemapee.Initialize()geemap.ee_search() #搜索Ear…

Vue3网站用户引导功能【Intro.js】

一、介绍 Intro.js 是一个用于创建网站用户引导、功能介绍和教程的 JavaScript 库。它允许开发者通过步骤和提示突出显示网站上的特定元素&#xff0c;以帮助用户更好地了解和使用网站的功能。以下是 Intro.js 的一些关键特点和用法介绍&#xff1a; 更多Intro.js 功能网址&a…

图扑数字孪生压缩空气储能管控平台

压缩空气储能在解决可再生能源不稳定性和提供可靠能源供应方面具有重要的优势。压缩空气储能&#xff0c;是指在电网负荷低谷期将电能用于压缩空气&#xff0c;在电网负荷高峰期释放压缩空气推动汽轮机发电的储能方式。通过提高能量转换效率、增加储能密度、快速启动和调节能力…

电子编曲软件FL Studio2024汉化中文免费版下载

电子编曲需要什么软件&#xff1f;市面上的宿主软件都可以完成电子编曲的工作&#xff0c;主要适用电子音乐风格编曲的宿主软件有FL Studio、Ableton Live等。电子编曲需要什么基础&#xff1f;需要对于电子音乐足够熟悉、掌握基础乐理知识以及宿主软件的使用方法。 就我个人的…

Linux cgroup技术

cgroup 全称是 control group&#xff0c;顾名思义&#xff0c;它是用来做“控制”的。控制什么东西呢&#xff1f;当然是资源的使用了。 cgroup 定义了下面的一系列子系统&#xff0c;每个子系统用于控制某一类资源。 CPU 子系统&#xff0c;主要限制进程的 CPU 使用率。cpu…

王道数据结构课后代码题p175 06.已知一棵树的层次序列及每个结点的度,编写算法构造此树的孩子-兄弟链表。(c语言代码实现)

/* 此树为 A B C D E F G 孩子-兄弟链表为 A B E C F G D */ 本题代码如下 void createtree(tree* t, char a[], int degree[], int n) {// 为B数组分配内存tree* B (tree*)malloc(sizeof(tree) * n);int i 0;i…

CENTOS 7 添加黑名单禁止IP访问服务器

一、通过 firewall 添加单个黑名单 只需要把ip添加到 /etc/hosts.deny 文件即可&#xff0c;格式 sshd:$IP:deny vim /etc/hosts.deny# 禁止访问sshd:*.*.*.*:deny# 允许的访问sshd:.*.*.*:allowsshd:.*.*.*:allow 二、多次失败登录即封掉IP&#xff0c;防止暴力破解的脚本…

搞程序权益系统v1.1

继1.0出来后我就把antdui换成elem 新增号卡功能现在只支持对接号氪系统 大家问我这个程序到底有什么用&#xff0c;我这边已经在写和WordPress对接文件&#xff0c;到时候在WordPress网站打开该程序就可以把订单同步到你的程序里面去&#xff0c;当然自己有集成能力也可以到小…

FairGuard无缝兼容小米澎湃OS、ColorOS 14 、鸿蒙4!

随着移动互联网时代的发展&#xff0c;各大手机厂商为打造生态系统、构建自身的技术壁垒&#xff0c;纷纷投身自研操作系统。 而对于一款游戏安全产品&#xff0c;在不同操作系统下&#xff0c;是否能够无缝兼容并且提供稳定的、高强度的加密保护&#xff0c;成了行业的一大痛…

python笔记:dtaidistance

1 介绍 用于DTW的库纯Python实现和更快的C语言实现 2 DTW举例 2.1 绘制warping 路径 from dtaidistance import dtw from dtaidistance import dtw_visualisation as dtwvis import numpy as np import matplotlib.pyplot as plts1 np.array([0., 0, 1, 2, 1, 0, 1, 0, 0…

Redis 命令全解析之 String类型

文章目录 ⛄String 介绍⛄命令⛄对应 RedisTemplate API⛄应用场景 ⛄String 介绍 String 类型&#xff0c;也就是字符串类型&#xff0c;是Redis中最简单的存储类型。 其value是字符串&#xff0c;不过根据字符串的格式不同&#xff0c;又可以分为3类&#xff1a; ● string&…

javaScript(四):函数和常用对象

文章目录 1、函数介绍2、函数的作用3、函数语法4、常用对象&#xff1a;数组5、常用对象&#xff1a;String6、常用对象&#xff1a;自定义对象 1、函数介绍 函数是一段可重复使用的代码块&#xff0c;用于执行特定任务或计算并返回结果。 函数由以下几个要素组成&#xff1a; …

2024最新电脑系统清理软件哪个好用?

基本上&#xff0c;不管是win版还是Mac版的电脑&#xff0c;其装机必备就是一款电脑系统清理软件&#xff0c;就比如Mac&#xff0c;目前在市面上&#xff0c;电脑系统清理软件是非常多的。 对于不熟悉系统的用户来说&#xff0c;使用一些小众工具&#xff0c;往往很多用户都不…

Flask项目Day1,Flask常见第三方拓展包

拉项目 git clone https://gitee.com/hahaguai007/python-flask-mysql.git git clone 项目地址运行后即可获取项目 2.创建数据库 在MySQL中创建一个数据库&#xff0c;名字自己定&#xff0c;然后修改RealProject\settings.py里的SQLALCHEMY_DATABASE_URI&#xff0c;格式为 …

一部,即全部,十年超越之作一加12售价4299元起

2023 年 12 月 5 日&#xff0c;一加正式发布十年旗舰一加 12。作为一加十年超越之作&#xff0c;一加 12 秉持「产品力优先」理念&#xff0c;带来多项领先行业的首创技术。一加 12 全球首发拥有医疗级护眼方案和行业第一 4500nit 峰值亮度的 2K 东方屏&#xff0c;完整搭载 F…

【Intel/Altera】 全系列FPGA最新汇总说明,持续更新中

前言 2023年11月14日英特尔 FPGA中国技术日&#xff0c;Intel刚发布了新的FPGA系列&#xff0c;官网信息太多&#xff0c;我这里结合以前的信息&#xff0c;简单汇总更新一下&#xff0c;方便大家快速了解Intel/Altera FPGA家族。 目录 前言 Altera和Intel 型号汇总 1. Agi…

【五分钟】学会利用cv2.resize()函数实现图像缩放

引言 在numpy知识库&#xff1a;深入理解numpy.resize函数和数组的resize方法中&#xff0c;小编较为详细地探讨了numpy的resize函数背后的机理。从结果来看&#xff0c;numpy.resize函数并不适合对图像进行缩放操作。而opencv中的resize函数虽然和numpy的resize函数同名&…

Python爬虫技术:如何利用ip地址爬取动态网页

目录 一、引言 二、Python爬虫基础 三、动态网页结构分析 四、利用ip地址爬取动态网页 1、找到需要爬取的动态网页的URL结构 2、构造请求参数 3、发送请求并获取响应 4、解析响应内容 五、实例代码 六、注意事项 七、总结 一、引言 随着互联网的快速发展&#xff0…

调研37位程序员后,我不再因为AI而焦虑 feat.脑放电波

点击文末“阅读原文”即可参与节目互动 剪辑、音频 / 杜1- 运营 / SandLiu 卷圈 监制 / 姝琦 封面 / 姝琦Midjourney 产品统筹 / bobo Nixon 的毕业论文研究了37位程序员使用GPT 写代码的状态&#xff0c;我们邀请了懂编程 且 具备一定技术团队管理经验的科技乱炖朱峰、小…

数据结构第二次作业——递归、树、图【考点罗列//错题正解//题目解析】

目录 一、选择题 ——递归—— 1.【单选题】 ——递归的相关知识点 2.【单选题】——递归的应用 3.【单选题】——递归的实现结构 4.【单选题】——递归的执行与实现 5.【单选题】 ——递归算法 ——树—— 6.【单选题】 ——树的结构 *7.【单选题】——树的知识点 …