多线程中ThreadLocal的使用

前言

多线程是Java的一个重要特性,多线程从某方面可以等价于多任务,当你有多个任务要处理时,多个任务一起做所消耗的时间肯定比任务串行起来做,所消耗的时间短。而对于多线程不熟悉的新手则容易踩到很多坑,最典型的则是变量问题。

概念介绍

下面先用简单粗俗的语言解释一下几个基本概念

线程安全:多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。典型的例子为StringBuffer类。

线程不安全:不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。典型的例子为StringBuilder类。Servlet和SpringMVC采用的是单例设计模式,因此也是线程不安全的。而aop中如果定义了成员变量,也是线程不安全的。

Java内存模型

参考我之前的文章Java内存模型介绍

结合Java内存模型的介绍可知,在单例模式下,多个线程操作同一个变量,会发生线程安全性问题简单来说就是一个变量name在线程A中命名为“李铁蛋”,而线程B将其命名为“李蛋”,此时线程A输出变量name,极有可能输出的是“李蛋”。因此,就需要使用ThreadLocal来给每个线程提供局部变量,解决线程安全问题。

示例

首先我们来看一下关于线程不安全的情况

public class test003 implements Runnable {

    private Res res;

    public test003(Res res) {
        this.res = res;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "," + res.getNumber());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Res res = new Res();
        for (int i = 0; i < 4; i++) {
            new Thread(new test003(res)).start();
        }
    }

}

class Res {
    public Integer count = 0;

    public Integer getNumber() {
        return ++count;
    }

}

程序中的res变量则为主内存中的变量,每个线程都会操作同一个res,获取到的也是同一个count,因此其中一次运行打印出来的结果如下

Thread-0,1
Thread-1,2
Thread-2,3
Thread-3,4
Thread-0,5
Thread-1,6
Thread-2,7
Thread-3,8
Thread-0,9
Thread-1,10
Thread-2,11
。。。。。

程序的本意为打印每个线程从1开始增长,而运行结果中,比如线程0,第一次为1,第二次为5,很明显不符合要求,我们将程序部分代码如下改造:

class Res {
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    // 这里其实可以使用JDK8的Lambda表达式简化代码
    // public static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public Integer getNumber() {
        int count = threadLocal.get() + 1;
        threadLocal.set(count);
        return count;
    }

}

改造后的代码,使用ThreadLocal创建一个成员变量,泛型为Integer,表示这个成员变量为int类型。在getNumber方法中,执行的则是count++操作。threadLocal.get()方法的作用是获取当前线程threadLocal中的值,+1之后获取本次的count,并set回去。我们看一下输出结果。

Thread-0,1
Thread-2,1
Thread-3,1
Thread-1,1
Thread-0,2
Thread-2,2
Thread-3,2
Thread-1,2
Thread-1,3
Thread-0,3
Thread-2,3
Thread-3,3
。。。。

每个线程的结果都是从1开始增长。

总结

ThreadLocal的作用是给每个线程提供局部变量,而这个局部变量就是存储到工作内存中的。线程之间的局部变量互不影响,达到线程安全的目的。ThreadLocal的应用相当广泛,如SpringCloud在网关中获取当前的request,就是使用的ThreadLocal

部分代码如下:

public class RequestContext extends ConcurrentHashMap<String, Object> {

    // ThreadLocal存储RequestContext
    protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
        protected RequestContext initialValue() {
            try {
                return (RequestContext)RequestContext.contextClass.newInstance();
            } catch (Throwable var2) {
                throw new RuntimeException(var2);
            }
        }
    };

    // 获取Request上下文
    public static RequestContext getCurrentContext() {
        if (testContext != null) {
            return testContext;
        } else {
            RequestContext context = (RequestContext)threadLocal.get();
            return context;
        }
    }

    // 获取当前线程的request
    public HttpServletRequest getRequest() {
        return (HttpServletRequest)this.get("request");
    }

}

SpringCloud的源码我还没开始看(这玩意源码太多了估计啃不动),现在在啃Mybatis源码,因此下面对此的分析只是推测,还希望大佬们不要打我。

Zuul在请求进入后,首先会获取到request,并将其存储在RequestContext中,使用threadLocal存储,可以保证每个线程获取到的request都是属于自己的。后续在程序的任意处,都可以使用 RequestContext.getCurrentContext().getRequest() 来获取当前请求的request对象。

错误使用

在web应用中,经常会有人把ThreadLocal作为每个线程的全局变量使用,这种用法是错误的。SpringBoot底层有线程池,对于每一个请求,都会从线程池中随机取出一个线程,因此即使是同一个登录的用户,每一次请求都有可能不是同一个线程,而从ThreadLocal中获取到的值自然也不一样。关于每次请求都不是同一线程的问题,可以自行打印请求的线程id进行证明,这里就不贴代码了。

ThreadLocal在web应用中的使用场景为,为每次请求提供一个全局的值,在这一次请求中,可以在任何地方取出来这个值进行操作。如:在aop中解析token获取登录中的用户信息,存放到ThreadLocal,本次请求需要用到登录用户的信息,就可以取出来。再如:开发者在aop中记录日志,代码全部写到环绕通知中就显得冗余,因此获取ip、参数等内容会写到前置通知中。而对于要存表的日志,参数在前置通知,返回值在后置通知,报错信息在环绕通知中,可能会想到把变量定义到最上面,这种写法也是错误的。在上面说过,aop是单例模式,因此这种写法存在线程安全性问题,在这里就也可以使用ThreadLocal存储日志信息,最后在后置通知中存表。

发布了30 篇原创文章 · 获赞 35 · 访问量 2788

猜你喜欢

转载自blog.csdn.net/qq_36403693/article/details/102539487