带你进入ThreadLocal

1.概览

在这篇文章中,我们将看一下java.lang包下的ThreadLocal类。ThreadLocal能让我们有能力单独地为当前线程(current thread)存储数据-----并且简单地把它包装在一个对象的指定类型中。

2.ThreadLocal API

ThreadLocal的概念允许我们去存储那些只能被一个特定线程访问的数据.例如,我们想有一个和指定线程绑定的Integer值。

      ThreadLocal threadLocalValue = new ThreadLocal<>();

下一步,当我们在一个线程中使用该值得时候,我们只需调用get()或set()方法,更简单地说,就是,我们可以这样认为:ThreadLocal是把数据存储在一个map中----只不过是使用当前线程作为key键。

由于这样的事实,当我们调用get()方法获取一threadLocalValue时,我们得到一个针对当前线程的Integer值:

     threadLocalValue.set(1);

    Integer result=threadLocalValue.get();

通过使用withInitial()静态方法并且传递一个supplier给它,我们可以创建一个ThreadLocal实例:

 ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 1);

要从ThreadLocal中移除值,我们可以使用remove()方法:

  threadLocal.remove();

想看到如何正确地使用ThreadLocal,首先,我们将看一个不适用ThreadLocal的例子,之后,我们将重写这个案例:


3. 把用户数据存储到map中

让我们来设想一下,有一个程序需要为每一个给定的用户id存储该用户特定的上下文Context数据:

publicclassContext {

    privateString userName;

    publicContext(String userName) {

        this.userName = userName;

    }

}

我们想为每一个用户id生成一个thread,我们将创建一个SharedMapWithUserContext类,这个类会实现Runnable接口。 run方法会通过UserReposity类来调用某一个数据库,它会为每一个指定的userId返回一个上下文对象。紧接着,我们把这个context对象存储到ConcurrentHashMap中,并以userId作为key:

publicclassSharedMapWithUserContext implementsRunnable {

    publicstaticMap userContextPerUserId  = newConcurrentHashMap<>();

    privateInteger userId;

    privateUserRepository userRepository = newUserRepository();

    @Override

    publicvoidrun() {

        String userName = userRepository.getUserNameForUserId(userId);

        userContextPerUserId.put(userId, newContext(userName));

    }

    // standard constructor

}

通过针对俩个不同的userId创建并启动俩个线程,我们就能够测试我们的代码,并断言我们在userContextPerUserId 这个map中有俩个入口:

SharedMapWithUserContext firstUser = newSharedMapWithUserContext(1);

SharedMapWithUserContext secondUser = newSharedMapWithUserContext(2);

newThread(firstUser).start();

newThread(secondUser).start();

assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);


4.把用户数据存储到ThreadLocal中

我们可以重写我们的案例,把用户context实例存入ThreadLocal中,每一个线程都将有它自己的ThreadLocal实例。

在使用ThreadLocal时,我们需要格外小心,因为每一个ThreadLocal实例都和一个特定的线程关联。在我们的例子中,针对每一个用户id,我们都有一个专门的线程,并且这个线程是由我们创建的,因此,我们对该线程拥有完全的控制权。

run()方法会抓取用户context并使用set方法把它存储到ThreadLocal变量中:

public  class   ThreadLocalWithUserContext implementsRunnable {

    privatestaticThreadLocal userContext   = newThreadLocal<>();

    privateInteger userId;

    privateUserRepository userRepository = newUserRepository();

    @Override

    publicvoidrun() {

        String userName = userRepository.getUserNameForUserId(userId);

        userContext.set(newContext(userName));

        System.out.println("thread context for given userId: "

          + userId + " is: "+ userContext.get());

    }

    // standard constructor

}

我们可以测试一下,开启俩个线程,它们会为给定的userId执行动作:

ThreadLocalWithUserContext firstUser = newThreadLocalWithUserContext(1);

ThreadLocalWithUserContext secondUser  = newThreadLocalWithUserContext(2);

                             newThread(firstUser).start();

                            newThread(secondUser).start();

在运行完这段代码之后,我们将会看到在控制台看到,针对每一个给定的线程,ThreadLocal都被设置进值了:

thread context forgiven userId: 1is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'}

thread context forgiven userId: 2is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}

根据上面的运行结果,我们能够看到,每一个用户都有它自己的Context。


5.不要把ThreadLocal和ExecutorService一块使用

如果我们想使用ExecutorService并且提交了一个Runnable给它,使用ThreadLocal将会产生非确定的结果---因为我们无法保证,每一个Runnable动作在它每次执行时都能被同一个线程处理。

由于这样的原因,我们的ThreadLocal会被不同的userId共享,这也就是为什么我们不应该把ThreadLocal和ExecutorService一起使用。只有当我们能完全控制让哪个线程执行哪个runnable动作时,我们才能使用ThreadLocal.



6.结论

在这篇文章中,我们看了ThreadLocal的构造,我们实现了使用了在线程间共享的ConcurrentHashMap去存储和指定userId相关联的context。

之后,我们使用ThreadLocal去重写了我们的案例。

上面所有案例的代码都可以在GitHub链接地址上找到-----这是一个maven项目,所以非常容易导入并运行。

你可能感兴趣的:(带你进入ThreadLocal)