ThreadLocal深入理解 修订版

本文是传智博客多线程视频的学习笔记。

原版本见

深入理解ThreadLocal_查看所以threadlocal的值_程序员小董的博客-CSDN博客

ThreadLocal是一个和线程安全相关的类。

它能干什么?
能保证在一个线程内,某个变量的全局共享。

说的很模糊,咱们看一个图


线程1里面的数据,应该在线程1范围内的模块a,b,c都能访问。
线程2里面的数据,应该在线程3范围内的模块a,b,c都能访问。
且线程1,2之间数据不会混淆。
那它有什么用呢?
举个例子,银行的转账包含两步,存款和取款,时候如果在存款取款中间出了问题,就得回滚;如果一切正常等整个交易完成了再commit,而调用commit的对象是Connection。那你说,如果多个线程共用一个Connection会发生什么问题?

一个非线程安全的例子

在我们讲述ThreadLocal之前,我们先看一个例子。

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadScopeDataShare {
    static private int data = 0;

    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 2; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    int data2= new Random().nextInt(100);
                    System.out.println(Thread.currentThread().getName()+" put "+data2);
                    data=data2;
                    try {
                        Thread.sleep(1000); //为什么要睡1秒 大家懂吗?
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    new ModuleA().get();
                    new ModuleB().get();

                }
            });
        }
        threadPool.shutdown();
    }

    public static int getData() {
        return data;
    }

}

class ModuleA {
    public int get() {
        int data = ThreadScopeDataShare.getData();
        System.out
            .println("ModuleA  "+Thread.currentThread().getName() + " getdata " + data);
        return data;
    }
}

class ModuleB {
    public int get() {
        int data = ThreadScopeDataShare.getData();
        System.out
            .println("ModuleB  "+Thread.currentThread().getName() + " getdata " + data);
        return data;
    }
}



在我们设想中,应该是线程1放的数据,在线程1中,模块A与模块B取得的数据应该是一致的。同理,线程2里面放的数据,工作再线程2下的模块A模块B取得的数据也应该是一致的。
可是上面的代码的运行结果却是:
pool-1-thread-1 put 90
pool-1-thread-2 put 78
ModuleA  pool-1-thread-2 getdata 78
ModuleB  pool-1-thread-2 getdata 78
ModuleA  pool-1-thread-1 getdata 78
ModuleB pool-1-thread-1 getdata 78

改进版

我们新建一个map,key是当前线程,value是我们要保存的数据。
那么就可以保证每个线程的各个模块取得的数据都是一致的。

public class ThreadScopeShareData3 {

    private static Map<Thread, Integer> threadData = new HashMap<Thread, Integer>();
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 2; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    int data2= new Random().nextInt(100);
                    System.out.println(Thread.currentThread().getName()+" put "+data2);
                    threadData.put(Thread.currentThread(),data2);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    new A().get();
                    new B().get();

                }
            });
        }
        threadPool.shutdown();
    }
    
    static class A{
        public void get(){
            int data = threadData.get(Thread.currentThread());
            System.out.println("A from " + Thread.currentThread().getName()
                    + " get data :" + data);
        }
    }
    //省略class B
}

运行结果

pool-1-thread-1 put 2
pool-1-thread-2 put 99
A from pool-1-thread-2 get data :99
A from pool-1-thread-1 get data :2
B from pool-1-thread-1 get data :2
B from pool-1-thread-2 get data :99

ThreadLocal的简单介绍

   早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
  当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

我们先看应用再讲原理,然后再讲一个实际的应用。
第一个应用

public class ThreadLocalTest {

    private static ThreadLocal<Integer> x = new ThreadLocal<Integer>();
    public static void main(String[] args) {
        for(int i=0;i<2;i++){
            new Thread(new Runnable(){
                @Override
                public void run() {
                    int data = new Random().nextInt(500);
                    System.out.println(Thread.currentThread().getName()
                            + " has put data :" + data);
                    x.set(data);
                    new A().get();
                    new B().get();
                }
            }).start();
        }
    }
    
    static class A{
        public void get(){
            int data = x.get();
            System.out.println("A from " + Thread.currentThread().getName()
                    + " get data :" + data);
        }
    }
    
    static class B{
        public void get(){
            int data = x.get();            
            System.out.println("B from " + Thread.currentThread().getName()
                    + " get data :" + data);        
        }        
    }
}



Thread-0 has put data :67
A from Thread-0 get data :67
B from Thread-0 get data :67
Thread-1 has put data :221
A from Thread-1 get data :221
B from Thread-1 get data :221
完全符合我们的要求。
这里有个问题:
如果一个线程能要共享多个变量怎么做?
private static ThreadLocal<Integer> x = new ThreadLocal<Integer>();
private static ThreadLocal<Integer> y = new ThreadLocal<Integer>();
不嫌麻烦吗?
ThreadLocal里可以放Interger,也可以放Objcet么。
如果几个变量有关系,如name,age我们就把它们包装成User;

如果变量没有关系,那就包装成一个map。

(当然一个线程如果要共享多个变量,那么分别设置为x,y也是可以的)

这样可以不?

import java.util.Random;

public class ThreadLocalTest3 {

    private static ThreadLocal<MyThreadScopeData2> myThreadScopeData = new ThreadLocal<MyThreadScopeData2>();

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int data = new Random().nextInt(500);

                    MyThreadScopeData2 myData = new MyThreadScopeData2();
                    myData.setName("name" + data);
                    myData.setAge(data);
                    myThreadScopeData.set(myData);

                    new A().get();
                    new B().get();
                }
            }).start();
        }
    }

    static class A {
        public void get() {
            MyThreadScopeData2 myData = myThreadScopeData.get();

            System.out
                    .println("A from " + Thread.currentThread().getName()
                            + " getMyData: " + myData.getName() + ","
                            + myData.getAge());
        }
    }

        //省略class B
}

class MyThreadScopeData2 {

    private static ThreadLocal<MyThreadScopeData2> map = new ThreadLocal<MyThreadScopeData2>();

    private String name;
    private int age;

    //省略get set
}


可以,不过对用户来说暴露了ThreadLocal的应用,我们希望在调用的时候,ThreadLocal对用户是透明的。
换句话说,我们得把ThreadLocal包装起来。

package com.alibaba;

/**
 * @program: parent_pro
 * @description:
 * @author: 渭水
 * @create: 2023/06/26
 */
public class UserDataContextHolder {
    private static final ThreadLocal<UserData> CONTEXT_THREAD_LOCAL =
        new ThreadLocal<>();

    /**
     * 设置上下文
     *
     * @param context context
     */
    public static void set(UserData context) {
        CONTEXT_THREAD_LOCAL.set(context);
    }

    public static UserData get() {
        return CONTEXT_THREAD_LOCAL.get();
    }

    /**
     * 初始化上下文
     */
    public static UserData initContext(Long userId) {
        UserDataContextHolder.clear();
        UserData context = new UserData(userId);
        CONTEXT_THREAD_LOCAL.set(context);
        return context;
    }

    /**
     * 清理上下文
     */
    public static void clear() {
        CONTEXT_THREAD_LOCAL.remove();
    }
}

上面的UserData是什么东西呢?它就是一个单纯的POJO,保存了这个用户的一些信息,也就是你想在多个模块间传递的东西!!

另外得记得,当业务逻辑完成之后,得调用clear方法清理掉这个ThreadLocal信息


现在重头戏来了,看看ThreadLocal实现的原理。

参见:

深入理解ThreadLocal_查看所以threadlocal的值_程序员小董的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值