[java并发] java高并发系列 - 第24天:ThreadLocal、InheritableThreadLocal(通俗易懂)

原文链接:查看原文

感谢公众号“ 路人甲Java”的分享,如有冒犯,请联系删除,快去关注他吧
在这里插入图片描述

本文内容

  1. 需要解决的问题
  2. 介绍ThreadLocal
  3. 介绍InheritableThreadLocal

需要解决的问题:

我们还是以解决问题的方式引出 ThreadLocalInheritableThreadLocal

目前java开发web系统一般为3层,controller、service、dao,请求到达controller、controller调用service、service调用dao,然后进行处理。

写一个简单的例子,有三个方法分别模拟controler、service、dao

代码如下:

package aboutThread.Concurrent.Day24;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

 

public class Demo1 {
    static AtomicInteger threadIndex = new AtomicInteger(1);
    
    //创建处理请求的例子
    static ThreadPoolExecutor disposeRequestExecutor = new ThreadPoolExecutor(3,
    3,
    60,
    TimeUnit.SECONDS,
    new LinkedBlockingDeque<>(),
    r ->{
        Thread thread = new Thread(r);
        thread.setName("disposeRequestThread-" + threadIndex.getAndIncrement());
        return thread;
    });

    //记录日志
    public static void log(String msg){
        StackTraceElement stack[] = (new Throwable()).getStackTrace();
        System.out.println("****" + System.currentTimeMillis() + ",[线程" + Thread.currentThread().getName() + "]," + stack[1] + ":" + msg);
    }

    //模拟ctroller
    public static void controller(List<String> dataList){
        log("接受请求");
        service(dataList);
    }

    //模拟service
    public static void service(List<String> dList){
        log("执行业务!");
        dao(dList);
    }

    public static void dao(List<String> dataList){
        log("执行数据库操作成功!");
        //模拟插入数据
        for(String s : dataList){
            log("插入:" + s + "成功!");
        }
    }

    public static void main(String[] args){
        //需要插入的数据
        List<String> dataList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            dataList.add("数据" + i);
        }

        //模拟5个请求

        int requestCount = 5;
        for (int i = 0; i < requestCount; i++) {
            disposeRequestExecutor.execute(() -> {
                controller(dataList);
            });
        }

        disposeRequestExecutor.shutdown();
    }
}

输出:

****1592019276284,[线程disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo1.controller(Demo1.java:35):接受请求
****1592019276284,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.controller(Demo1.java:35):接受请求
****1592019276284,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.controller(Demo1.java:35):接受请求
****1592019276285,[线程disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo1.service(Demo1.java:41):执行业务!
****1592019276285,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.service(Demo1.java:41):执行业务!
****1592019276285,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.service(Demo1.java:41):执行业务!
****1592019276285,[线程disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:46):执行数据库操作成功!
****1592019276285,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:46):执行数据库操作成功!
****1592019276285,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:46):执行数据库操作成功!
****1592019276286,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据0成功!
****1592019276286,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据0成功!
****1592019276286,[线程disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据0成功!
****1592019276286,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据1成功!
****1592019276286,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据1成功!
****1592019276286,[线程disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据1成功!
****1592019276286,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据2成功!
****1592019276286,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据2成功!
****1592019276286,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.controller(Demo1.java:35):接受请求
****1592019276286,[线程disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据2成功!
****1592019276289,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.controller(Demo1.java:35):接受请求
****1592019276289,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.service(Demo1.java:41):执行业务!
****1592019276289,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.service(Demo1.java:41):执行业务!
****1592019276289,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:46):执行数据库操作成功!
****1592019276289,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:46):执行数据库操作成功!
****1592019276289,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据0成功!
****1592019276289,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据0成功!
****1592019276290,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据1成功!
****1592019276290,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据1成功!
****1592019276290,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据2成功!
****1592019276291,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据2成功!

代码中调用controller、service、dao 3个方法时,来模拟处理一个请求。main方法中循环了5次,模拟发起5次请求,然后交个线程去处理请求,dao中模拟循环插入传入dataList数据

问题来了:开发者想看一下哪些地方耗时比较多,想过日志来分析耗时情况,向追踪某个请求的完整日志,怎么处理?

上面的请求采用线程池的方式处理的,多个请求可能会被一个线程处理,通过日志很难看出哪些日志是同一个请求,我们能不能给请求加一个唯一标志,日志中输出这个唯一标志,当然可以!

如果我们的代码就只有上面示例代码那么简单,我想还是很容易的,上面就3个方法,给每个方法加个traceId参数,log方法也加个traceId参数,就解决了,代码如下:

package aboutThread.Concurrent.Day24;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;


public class Demo2 {
    static AtomicInteger threadIndex = new AtomicInteger(1);
    //创建处理请求的线程池子
    static ThreadPoolExecutor disposeRequestExecutor = new ThreadPoolExecutor(3,
    3,
    60,
    TimeUnit.SECONDS,
    new LinkedBlockingDeque<>(),
    r ->{
        Thread thread = new Thread(r);
        thread.setName("dispostRequestThread-" + threadIndex.getAndIncrement());
        return thread;
    });


    //记录日志
    public static void log(String msg,String traceId){
        StackTraceElement stack[] = (new Throwable()).getStackTrace();
        System.out.println("********" + System.currentTimeMillis() + "[traceId:"+ traceId +"],线程[" + Thread.currentThread().getName() +"]," + stack[1] + ":" + msg);
    }

    //模拟controller
    public static void controller(List<String> dataList,String traceId){
        log("接受请求",traceId);
        service(dataList,traceId);
    }

    //模拟service
    public static void service(List<String> dataList,String traceId){
        log("执行业务",traceId);
        dao(dataList,traceId);
    }

    //模拟dao
    public static void dao(List<String> dataList,String traceId){
        log("执行数据库操作",traceId);
        //模拟插入数据
        for(String s: dataList){
            log("插入数据:" + s + "成功",traceId);
        }
    }

    public static void main(String[] args){
        //需要插入的数据
        List<String> dataList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            dataList.add("数据" + i);
        }

        //模拟5个请求
        int reqeustCount = 5;
        for (int i = 0; i < reqeustCount; i++) {
            String traceId = String.valueOf(i);
            disposeRequestExecutor.execute(() ->{
                controller(dataList, traceId);
            });
        }
        disposeRequestExecutor.shutdown();
    }
}

输出:

****1592019276284,[线程disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo1.controller(Demo1.java:35):接受请求
****1592019276284,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.controller(Demo1.java:35):接受请求
****1592019276284,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.controller(Demo1.java:35):接受请求
****1592019276285,[线程disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo1.service(Demo1.java:41):执行业务!
****1592019276285,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.service(Demo1.java:41):执行业务!
****1592019276285,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.service(Demo1.java:41):执行业务!
****1592019276285,[线程disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:46):执行数据库操作成功!
****1592019276285,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:46):执行数据库操作成功!
****1592019276285,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:46):执行数据库操作成功!
****1592019276286,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据0成功!
****1592019276286,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据0成功!
****1592019276286,[线程disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据0成功!
****1592019276286,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据1成功!
****1592019276286,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据1成功!
****1592019276286,[线程disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据1成功!
****1592019276286,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据2成功!
****1592019276286,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据2成功!
****1592019276286,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.controller(Demo1.java:35):接受请求
****1592019276286,[线程disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据2成功!
****1592019276289,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.controller(Demo1.java:35):接受请求
****1592019276289,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.service(Demo1.java:41):执行业务!
****1592019276289,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.service(Demo1.java:41):执行业务!
****1592019276289,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:46):执行数据库操作成功!
****1592019276289,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:46):执行数据库操作成功!
****1592019276289,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据0成功!
****1592019276289,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据0成功!
****1592019276290,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据1成功!
****1592019276290,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据1成功!
****1592019276290,[线程disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据2成功!
****1592019276291,[线程disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo1.dao(Demo1.java:49):插入:数据2成功!
{20-06-13 11:34}Arans-Mac:~/JavaPrjs_VS_CODE/JavaThread@master✗✗✗✗✗✗ aran%  cd /Users/aran/JavaPrjs_VS_CODE/JavaThread ; /Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=localhost:51822 -Dfile.encoding=UTF-8 -cp "/Users/aran/Library/Application Support/Code/User/workspaceStorage/0f284770f0218fea9250fa3f3328bca2/redhat.java/jdt_ws/JavaThread_f39215e3/bin" aboutThread.Concurrent.Day24.Demo2 
Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
        ArrayList cannot be resolved to a type

        at aboutThread.Concurrent.Day24.Demo2.main(Demo2.java:55)
{20-06-13 12:29}Arans-Mac:~/JavaPrjs_VS_CODE/JavaThread@master✗✗✗✗✗✗ aran%  cd /Users/aran/JavaPrjs_VS_CODE/JavaThread ; /Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=localhost:51826 -Dfile.encoding=UTF-8 -cp "/Users/aran/Library/Application Support/Code/User/workspaceStorage/0f284770f0218fea9250fa3f3328bca2/redhat.java/jdt_ws/JavaThread_f39215e3/bin" aboutThread.Concurrent.Day24.Demo2 
Exception in thread "main" java.lang.Error: Unresolved compilation problem: 

        at aboutThread.Concurrent.Day24.Demo2.main(Demo2.java:54)
{20-06-13 12:29}Arans-Mac:~/JavaPrjs_VS_CODE/JavaThread@master✗✗✗✗✗✗ aran%  cd /Users/aran/JavaPrjs_VS_CODE/JavaThread ; /Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=localhost:51838 -Dfile.encoding=UTF-8 -cp "/Users/aran/Library/Application Support/Code/User/workspaceStorage/0f284770f0218fea9250fa3f3328bca2/redhat.java/jdt_ws/JavaThread_f39215e3/bin" aboutThread.Concurrent.Day24.Demo2 
********1592022666542[traceId:0],线程[dispostRequestThread-1],aboutThread.Concurrent.Day24.Demo2.controller(Demo2.java:34):接受请求
********1592022666543[traceId:1],线程[dispostRequestThread-2],aboutThread.Concurrent.Day24.Demo2.controller(Demo2.java:34):接受请求
********1592022666542[traceId:2],线程[dispostRequestThread-3],aboutThread.Concurrent.Day24.Demo2.controller(Demo2.java:34):接受请求
********1592022666543[traceId:0],线程[dispostRequestThread-1],aboutThread.Concurrent.Day24.Demo2.service(Demo2.java:40):执行业务
********1592022666543[traceId:2],线程[dispostRequestThread-3],aboutThread.Concurrent.Day24.Demo2.service(Demo2.java:40):执行业务
********1592022666543[traceId:0],线程[dispostRequestThread-1],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:46):执行数据库操作
********1592022666543[traceId:2],线程[dispostRequestThread-3],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:46):执行数据库操作
********1592022666543[traceId:1],线程[dispostRequestThread-2],aboutThread.Concurrent.Day24.Demo2.service(Demo2.java:40):执行业务
********1592022666544[traceId:1],线程[dispostRequestThread-2],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:46):执行数据库操作
********1592022666544[traceId:2],线程[dispostRequestThread-3],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据0成功
********1592022666544[traceId:0],线程[dispostRequestThread-1],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据0成功
********1592022666544[traceId:1],线程[dispostRequestThread-2],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据0成功
********1592022666544[traceId:2],线程[dispostRequestThread-3],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据1成功
********1592022666544[traceId:1],线程[dispostRequestThread-2],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据1成功
********1592022666544[traceId:2],线程[dispostRequestThread-3],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据2成功
********1592022666544[traceId:0],线程[dispostRequestThread-1],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据1成功
********1592022666544[traceId:1],线程[dispostRequestThread-2],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据2成功
********1592022666544[traceId:0],线程[dispostRequestThread-1],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据2成功
********1592022666544[traceId:3],线程[dispostRequestThread-3],aboutThread.Concurrent.Day24.Demo2.controller(Demo2.java:34):接受请求
********1592022666545[traceId:4],线程[dispostRequestThread-2],aboutThread.Concurrent.Day24.Demo2.controller(Demo2.java:34):接受请求
********1592022666545[traceId:3],线程[dispostRequestThread-3],aboutThread.Concurrent.Day24.Demo2.service(Demo2.java:40):执行业务
********1592022666545[traceId:3],线程[dispostRequestThread-3],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:46):执行数据库操作
********1592022666545[traceId:4],线程[dispostRequestThread-2],aboutThread.Concurrent.Day24.Demo2.service(Demo2.java:40):执行业务
********1592022666546[traceId:4],线程[dispostRequestThread-2],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:46):执行数据库操作
********1592022666545[traceId:3],线程[dispostRequestThread-3],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据0成功
********1592022666546[traceId:4],线程[dispostRequestThread-2],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据0成功
********1592022666546[traceId:3],线程[dispostRequestThread-3],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据1成功
********1592022666546[traceId:4],线程[dispostRequestThread-2],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据1成功
********1592022666546[traceId:3],线程[dispostRequestThread-3],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据2成功
********1592022666546[traceId:4],线程[dispostRequestThread-2],aboutThread.Concurrent.Day24.Demo2.dao(Demo2.java:49):插入数据:数据2成功

上面我们通过修改代码的方式,把问题解决了,但前提是你们的系统都想上面这么简单,功能很少,需要改的地方很少,可以这么去改。但事与愿违,我们的系统一般功能都比较多的,如果我们都一个个去改,岂不是要疯掉,改代码还涉及到重新测试,风险也不可控,那有什么好办法么?


ThreadLocal

还是拿上面的问题,我们来分析一下,每个请求都是由一个线程处理的,线程就相当于一个人一样,每个请求相当于一个任务,任务来了,人来处理,处理完毕之后,再处理下一个请求任务。人身上是不是有很多口袋,人刚开始准备处理任务的时候,我们把任务的编号放在口袋中,然后处理中一路携带者,处理过程中如果需要用到这个编号,直接从口袋中取就可以了。那么刚好java中线程设计的时候也考虑到了这些问题,Thread对象中就有很多口袋,用来放东西。Thread类中有这么一个变量:

ThreadLocal.ThreadLocalMap threadLocals = null;

这个就是用来操作Thread中所有口袋的东西, ThreadLocalMap 源码中有一个数组(有兴趣可以看下源码),对应处理者身上的很多口袋一样,数组中的每个元素对应一个口袋。

如果来操作Thread中的这些口袋呢,java为我们提供了一个类 ThreadLocal,ThreadLocal对象用来操作Thread中的某一个口袋,可以向这个口袋中放东西、获取里面的东西,清除里面的东西,这个口袋一次性只能放一个东西,重复放东西会将里面已经存在的东西覆盖掉。

常用的3个方法:

//向Thread中某个口袋放东西
public void set(T value);
//获取这个口袋中目前放的东西
public T get();
//清空这个口袋中放的东西
public void remove();

我们使用ThreadLocal来改造一下上面的代码。如下:

package aboutThread.Concurrent.Day24;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;


public class Demo3 {
    //创建一个操作Thread中存放请求任务追踪id口袋的对象
    static ThreadLocal<String> traceIdKD = new ThreadLocal<>();

    static AtomicInteger threadIndex = new AtomicInteger(1);

    //创建处理请求的线程池
    static ThreadPoolExecutor disposeRequestExecutor = new ThreadPoolExecutor(3,
    3,
    60,
    TimeUnit.SECONDS,
    new LinkedBlockingDeque<>(),
    r->{
        Thread thread = new Thread(r);
        thread.setName("disposeRequestThread-" + threadIndex.getAndIncrement());
        return thread;
    });

    //记录日志
    public static void log(String msg){
            StackTraceElement stack[] = (new Throwable()).getStackTrace();

            //获取当前线程存放tranceId口袋中的内容
            String traceId = traceIdKD.get();
            System.out.println("********" + System.currentTimeMillis() + "[traceId:"+ traceId +"],[线程:"+Thread.currentThread().getName()+"],"+stack[1]+":" + msg);


        }

    public static void controller(List<String> dataList){
        log("接受请求");
        service(dataList);
    }

    public static void service(List<String> dataList)
    {
        log("执行业务");
        dao(dataList);
    }

    public static void dao(List<String> dataList){
        log("执行数据库操作!");
        //模拟插入数据
        for(String s : dataList){
            log("插入数据["+s+"]成功");
        }
    }
public static void main(String[] args){
    //要插入的数据
    List<String> dataList = new ArrayList<>();
    for (int i = 0; i < 3; i++) {
        dataList.add("数据"+i);
    }

    //模拟5个请求
    int requestCount = 5;
    for (int i = 0; i < requestCount; i++) {
        String traceId = String.valueOf(i);
        disposeRequestExecutor.execute(() ->{
            //把traceId放入口袋中
            traceIdKD.set(traceId);
            try {
                controller(dataList);
            } finally{
                //把traceId从口袋里删除
                traceIdKD.remove();
            }
        });
    }
    disposeRequestExecutor.shutdown();
}
}

输出:

********1592118731195[traceId:0],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo3.controller(Demo3.java:41):接受请求
********1592118731195[traceId:1],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo3.controller(Demo3.java:41):接受请求
********1592118731196[traceId:0],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo3.service(Demo3.java:47):执行业务
********1592118731196[traceId:1],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo3.service(Demo3.java:47):执行业务
********1592118731195[traceId:2],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo3.controller(Demo3.java:41):接受请求
********1592118731197[traceId:1],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:52):执行数据库操作!
********1592118731196[traceId:0],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:52):执行数据库操作!
********1592118731197[traceId:2],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo3.service(Demo3.java:47):执行业务
********1592118731197[traceId:1],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据0]成功
********1592118731197[traceId:2],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:52):执行数据库操作!
********1592118731197[traceId:0],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据0]成功
********1592118731197[traceId:1],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据1]成功
********1592118731197[traceId:0],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据1]成功
********1592118731197[traceId:1],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据2]成功
********1592118731197[traceId:2],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据0]成功
********1592118731197[traceId:0],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据2]成功
********1592118731198[traceId:3],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo3.controller(Demo3.java:41):接受请求
********1592118731198[traceId:4],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo3.controller(Demo3.java:41):接受请求
********1592118731198[traceId:3],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo3.service(Demo3.java:47):执行业务
********1592118731198[traceId:2],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据1]成功
********1592118731198[traceId:4],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo3.service(Demo3.java:47):执行业务
********1592118731198[traceId:3],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:52):执行数据库操作!
********1592118731208[traceId:4],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:52):执行数据库操作!
********1592118731208[traceId:3],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据0]成功
********1592118731208[traceId:2],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据2]成功
********1592118731208[traceId:4],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据0]成功
********1592118731208[traceId:3],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据1]成功
********1592118731208[traceId:4],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据1]成功
********1592118731209[traceId:3],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据2]成功
********1592118731209[traceId:4],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo3.dao(Demo3.java:55):插入数据[数据2]成功

可以看出输出和刚才使用traceId参数的方式结果一致,但是却简单了很多。不用去修改controller、service、dao的代码了,风险也减少了很多。

代码中创建了一个 ThreadLocal traceIdkD ,这个对象用来操作Thread中的一个口袋,用这个口袋开存放traceId。在main方法中通过 traceIdKD.set(traceId) 方法将traceId 放入口袋,log方法中通过 traceIdKD.get() 获取口袋中的traceId,最后任务处理之后,main方法中的finally中调用 traceIdKD.remove(); 将口袋的中traceId清除。

ThreadLocal的官方解释为:

“该类提供了线程局部(thread-local)变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其get或set方法) 的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的private static 字段,它们希望将状态与某一个线程(例如,用户ID或事务ID)相关联”


InheritableThreadLocal

继续上面的实例,dao中循环处理dataList的内容,假如dataList处理比较耗时,我们想加快处理上的速度有什么办法么?大家已经想到了,用多线程并行处理dataList,那么我们把代码改一下:

package aboutThread.Concurrent.Day24;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Demo4 {
        //创建一个操作Thread中存放请求任务追踪id口袋的对象
        static ThreadLocal<String> traceIdKD = new ThreadLocal<>();

        static AtomicInteger threadIndex = new AtomicInteger(1);
    
        //创建处理请求的线程池
        static ThreadPoolExecutor disposeRequestExecutor = new ThreadPoolExecutor(3,
        3,
        60,
        TimeUnit.SECONDS,
        new LinkedBlockingDeque<>(),
        r->{
            Thread thread = new Thread(r);
            thread.setName("disposeRequestThread-" + threadIndex.getAndIncrement());
            return thread;
        });
    
        //记录日志
        public static void log(String msg){
                StackTraceElement stack[] = (new Throwable()).getStackTrace();
    
                //获取当前线程存放tranceId口袋中的内容
                String traceId = traceIdKD.get();
                System.out.println("********" + System.currentTimeMillis() + "[traceId:"+ traceId +"],[线程:"+Thread.currentThread().getName()+"],"+stack[1]+":" + msg);
    
    
            }
    
        public static void controller(List<String> dataList){
            log("接受请求");
            service(dataList);
        }
    
        public static void service(List<String> dataList)
        {
            log("执行业务");
            dao(dataList);
        }
    
        public static void dao(List<String> dataList){

            CountDownLatch countDownLatch = new CountDownLatch(dataList.size());
            log("执行数据库操作!");
            String threadName = Thread.currentThread().getName();
            //模拟插入数据
            for(String s : dataList){
                new Thread(() ->{
                try{
                   TimeUnit.MILLISECONDS.sleep(5);
                   log("插入数据["+s+"]成功");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally{
                    countDownLatch.countDown();
                }
            }).start();
            }
            //等待上面的dataList处理完毕
            try {
                countDownLatch.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    public static void main(String[] args){
        //要插入的数据
        List<String> dataList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            dataList.add("数据"+i);
        }
    
        //模拟5个请求
        int requestCount = 5;
        for (int i = 0; i < requestCount; i++) {
            String traceId = String.valueOf(i);
            disposeRequestExecutor.execute(() ->{
                //把traceId放入口袋中
                traceIdKD.set(traceId);
                try {
                    controller(dataList);
                } finally{
                    //把traceId从口袋里删除
                    traceIdKD.remove();
                }
            });
        }
        disposeRequestExecutor.shutdown();
    }
}

输出:

********1592119912566[traceId:1],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo4.controller(Demo4.java:41):接受请求
********1592119912566[traceId:0],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo4.controller(Demo4.java:41):接受请求
********1592119912566[traceId:2],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo4.controller(Demo4.java:41):接受请求
********1592119912566[traceId:0],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo4.service(Demo4.java:47):执行业务
********1592119912566[traceId:1],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo4.service(Demo4.java:47):执行业务
********1592119912567[traceId:2],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo4.service(Demo4.java:47):执行业务
********1592119912567[traceId:0],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo4.dao(Demo4.java:54):执行数据库操作!
********1592119912567[traceId:1],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo4.dao(Demo4.java:54):执行数据库操作!
********1592119912567[traceId:2],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo4.dao(Demo4.java:54):执行数据库操作!
********1592119912573[traceId:null],[线程:Thread-4],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据0]成功
********1592119912573[traceId:null],[线程:Thread-3],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据0]成功
********1592119912574[traceId:null],[线程:Thread-8],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据2]成功
********1592119912574[traceId:null],[线程:Thread-7],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据1]成功
********1592119912575[traceId:null],[线程:Thread-5],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据1]成功
********1592119912575[traceId:null],[线程:Thread-9],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据1]成功
********1592119912575[traceId:null],[线程:Thread-6],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据0]成功
********1592119912575[traceId:3],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo4.controller(Demo4.java:41):接受请求
********1592119912575[traceId:null],[线程:Thread-10],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据2]成功
********1592119912575[traceId:3],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo4.service(Demo4.java:47):执行业务
********1592119912576[traceId:null],[线程:Thread-11],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据2]成功
********1592119912576[traceId:3],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo4.dao(Demo4.java:54):执行数据库操作!
********1592119912576[traceId:4],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo4.controller(Demo4.java:41):接受请求
********1592119912576[traceId:4],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo4.service(Demo4.java:47):执行业务
********1592119912582[traceId:null],[线程:Thread-13],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据1]成功
********1592119912582[traceId:null],[线程:Thread-12],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据0]成功
********1592119912582[traceId:null],[线程:Thread-14],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据2]成功
********1592119912587[traceId:4],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo4.dao(Demo4.java:54):执行数据库操作!
********1592119912594[traceId:null],[线程:Thread-16],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据1]成功
********1592119912594[traceId:null],[线程:Thread-15],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据0]成功
********1592119912595[traceId:null],[线程:Thread-17],aboutThread.Concurrent.Day24.Demo4.lambda$1(Demo4.java:61):插入数据[数据2]成功

看一下上面的输出,有些traceId为null,这是为什么呢?这是因为dao为了提升处理速度,创建了子线程来并行处理,子线程调用log的时候,去自己的存放traceId的口袋中拿去东西,肯定是空的了。

那有什么办法么?可不可以不这样?

父线程相当于主管,子线程相当于干活的小弟,主管让小弟们干活的时候,将自己兜里的东西复制一份给小弟们使用,主管兜里的东西可能有很多牛逼的工具,为了提升小弟们的工作效率,给小弟们都复制一个,丢到小弟们的兜里,然后小弟就可以从自己的兜里拿去这些东西使用了,也可以清空自己兜里的东西。

Thread 对象中有个inheritableThreadLocals 变量,代码如下:

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

inheritableThreadLocal相当于线程中另外一种兜,这种兜有什么特征呢?当创建子线程时,子线程会将父线程这种类型的兜里面的东西全部赋值一份放到自己的 inheritableThreadLocals 兜中,使用InheribtaleThreadLocal 对象可以操作线程中的inheritableThreadLocals兜。

InheritableThreadLocal常用的方法也有3个:

//向Thread中的某个兜中放东西
public void set(T value);
//获取这个口袋中目前放的东西
public T get();
//清空这个口袋中放的东西
public void remove();

使用InheritableThreadLocal 解决上面的子线程无法输出traceId问题,只需将ThreadLocal改成InheritableThreadLocal即可,代码如下:

package aboutThread.Concurrent.Day24;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Demo5 {
      //创建一个操作Thread中存放请求任务追踪id口袋的对象
      static InheritableThreadLocal<String> traceIdKD = new InheritableThreadLocal<>();

      static AtomicInteger threadIndex = new AtomicInteger(1);
  
      //创建处理请求的线程池
      static ThreadPoolExecutor disposeRequestExecutor = new ThreadPoolExecutor(3,
      3,
      60,
      TimeUnit.SECONDS,
      new LinkedBlockingDeque<>(),
      r->{
          Thread thread = new Thread(r);
          thread.setName("disposeRequestThread-" + threadIndex.getAndIncrement());
          return thread;
      });
  
      //记录日志
      public static void log(String msg){
              StackTraceElement stack[] = (new Throwable()).getStackTrace();
  
              //获取当前线程存放tranceId口袋中的内容
              String traceId = traceIdKD.get();
              System.out.println("********" + System.currentTimeMillis() + "[traceId:"+ traceId +"],[线程:"+Thread.currentThread().getName()+"],"+stack[1]+":" + msg);
  
  
          }
  
      public static void controller(List<String> dataList){
          log("接受请求");
          service(dataList);
      }
  
      public static void service(List<String> dataList)
      {
          log("执行业务");
          dao(dataList);
      }
  
      public static void dao(List<String> dataList){

          CountDownLatch countDownLatch = new CountDownLatch(dataList.size());
          log("执行数据库操作!");
          String threadName = Thread.currentThread().getName();
          //模拟插入数据
          for(String s : dataList){
              new Thread(() ->{
              try{
                 TimeUnit.MILLISECONDS.sleep(5);
                 log("插入数据["+s+"]成功");
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } finally{
                  countDownLatch.countDown();
              }
          }).start();
          }
          //等待上面的dataList处理完毕
          try {
              countDownLatch.await();
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
  public static void main(String[] args){
      //要插入的数据
      List<String> dataList = new ArrayList<>();
      for (int i = 0; i < 3; i++) {
          dataList.add("数据"+i);
      }
  
      //模拟5个请求
      int requestCount = 5;
      for (int i = 0; i < requestCount; i++) {
          String traceId = String.valueOf(i);
          disposeRequestExecutor.execute(() ->{
              //把traceId放入口袋中
              traceIdKD.set(traceId);
              try {
                  controller(dataList);
              } finally{
                  //把traceId从口袋里删除
                  traceIdKD.remove();
              }
          });
      }
      disposeRequestExecutor.shutdown();
  }   
}

输出:

********1592120471992[traceId:1],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo5.controller(Demo5.java:41):接受请求
********1592120471992[traceId:0],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo5.controller(Demo5.java:41):接受请求
********1592120471993[traceId:1],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo5.service(Demo5.java:47):执行业务
********1592120471993[traceId:0],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo5.service(Demo5.java:47):执行业务
********1592120471992[traceId:2],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo5.controller(Demo5.java:41):接受请求
********1592120471993[traceId:2],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo5.service(Demo5.java:47):执行业务
********1592120471993[traceId:1],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo5.dao(Demo5.java:54):执行数据库操作!
********1592120471993[traceId:0],[线程:disposeRequestThread-1],aboutThread.Concurrent.Day24.Demo5.dao(Demo5.java:54):执行数据库操作!
********1592120471993[traceId:2],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo5.dao(Demo5.java:54):执行数据库操作!
********1592120472000[traceId:1],[线程:Thread-4],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据0]成功
********1592120472000[traceId:2],[线程:Thread-3],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据0]成功
********1592120472000[traceId:2],[线程:Thread-6],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据1]成功
********1592120472000[traceId:0],[线程:Thread-5],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据0]成功
********1592120472001[traceId:2],[线程:Thread-9],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据2]成功
********1592120472001[traceId:1],[线程:Thread-7],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据1]成功
********1592120472001[traceId:1],[线程:Thread-10],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据2]成功
********1592120472001[traceId:3],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo5.controller(Demo5.java:41):接受请求
********1592120472001[traceId:4],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo5.controller(Demo5.java:41):接受请求
********1592120472001[traceId:4],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo5.service(Demo5.java:47):执行业务
********1592120472002[traceId:4],[线程:disposeRequestThread-2],aboutThread.Concurrent.Day24.Demo5.dao(Demo5.java:54):执行数据库操作!
********1592120472001[traceId:3],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo5.service(Demo5.java:47):执行业务
********1592120472002[traceId:0],[线程:Thread-8],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据1]成功
********1592120472002[traceId:0],[线程:Thread-11],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据2]成功
********1592120472002[traceId:3],[线程:disposeRequestThread-3],aboutThread.Concurrent.Day24.Demo5.dao(Demo5.java:54):执行数据库操作!
********1592120472008[traceId:4],[线程:Thread-15],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据2]成功
********1592120472008[traceId:4],[线程:Thread-12],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据0]成功
********1592120472008[traceId:4],[线程:Thread-13],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据1]成功
********1592120472008[traceId:3],[线程:Thread-14],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据0]成功
********1592120472009[traceId:3],[线程:Thread-17],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据2]成功
********1592120472010[traceId:3],[线程:Thread-16],aboutThread.Concurrent.Day24.Demo5.lambda$1(Demo5.java:61):插入数据[数据1]成功

输出中都有个traceId了,和期望的结果一致。


这是学习java并发的第24天,加油!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值