[Java学习笔记]ThreadLocal实现不同线程的数据隔离,同一线程的数据共享

本文介绍了Java的ThreadLocal,用于在多线程环境中实现线程内数据共享和不同线程间的数据隔离。ThreadLocal为每个线程提供独立的变量副本,避免并发问题。文章详细阐述了ThreadLocal的用法,包括同一线程内的数据共享和不同线程的数据隔离,并探讨了其内部实现机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ThreadLocal是什么?

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

对比Synchronized,我们就能更好的理解——Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离

ThreadLocal的用法(存在意义)

一、同一线程内共享数据

一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图所示:
在这里插入图片描述

同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。

例如下面这个例子:
我需要在执行完毕的时候对事物提交,发生异常的时候对事务进行回滚,那么就要求我们进行提交/回滚的Connection连接和执行增删查改的连接是同一个——即一个事务一个连接,问题在于,我们获取连接的方式如果是通过连接池(DataSource)来完成,那么每次获取的连接都不一定是相同的!

    @Before
    public void before(){
        ad = new AccountDaoImp();
    }
    @Test
    public void transfer(){
        Connection conn = DataSourceUtils.getConnection();
        try {
            conn.setAutoCommit(false);
            ad.decreaMoney(2, 2000);
            int a = 1/0;
            ad.increaMoney(1,2000);
            conn.commit();
        } catch (SQLException e) {
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }
    }

这个时候我们就可以借助ThreadLocal将连接嵌在这个线程中, 因为即使是数据库和业务两层不相干的情况下,整个项目的执行还是依赖于同一条线程的——主线程,那么我们将事务开始时就直接将这个连接钉死在这个主线程上,那么之后回滚/提交用的连接都会是同一个了!

public class DataSourceUtils {
    private static DataSource ds = null;
    private static ThreadLocal<Connection> tl = new ThreadLocal<>();
    static {
        ds = new ComboPooledDataSource();
    }
    public static DataSource getDataSource(){
        return ds;
    }
    public static Connection getConnection(){
        Connection conn = tl.get();//后面获取连接都是获取到同线程对应的同一个connection
        if(conn == null) {
            try {
                tl.set(ds.getConnection());//首次将连接借tl置入到当前线程的ThreadLocalMap中
                conn = tl.get();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return conn;
    }
}

二、不同线程实现数据隔离

public class TestNum {  
        // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值  
        private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {  
            public Integer initialValue() {  
                return 0;  
            }  
        };  
      
        // ②获取下一个序列值  
        public int getNextNum() {  
            seqNum.set(seqNum.get() + 1);  
            return seqNum.get();  
        }  
      
        public static void main(String[] args) {  
            TestNum sn = new TestNum();  
            // ③ 3个线程共享sn,各自产生序列号  
            TestClient t1 = new TestClient(sn);  
            TestClient t2 = new TestClient(sn);  
            TestClient t3 = new TestClient(sn);  
            t1.start();  
            t2.start();  
            t3.start();  
        }  
      
        private static class TestClient extends Thread {  
            private TestNum sn;  
      
            public TestClient(TestNum sn) {  
                this.sn = sn;  
            }  
      
            public void run() {  
                for (int i = 0; i < 3; i++) {  
                    // ④每个线程打出3个序列值  
                    System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["  
                             + sn.getNextNum() + "]");  
                }  
            }  
        }  
    }  

通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个TestNum实例。运行以上代码,在控制台上输出以下的结果:

thread[Thread-0] --> sn[1]
thread[Thread-1] --> sn[1]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-0] --> sn[2]
thread[Thread-1] --> sn[3]
thread[Thread-2] --> sn[2]
thread[Thread-0]—>sn[3]
thread[Thread-2] --> sn[3]

注意:我们使用ThreadLocal为不同线程分配副本,副本并不是指同一个对象,因此倘若为不同的线程set()同一个对象的引用,还是无法避免导致不同线程都对该对象进行了操作,因此我们分配的副本就是不同的对象。ThreadLocal的最大意义就在于它实现了将副本写进了Thread对象内部的Threadlocals 这个映射对象(它是一个ThreadLocal.ThreadLocalMap类对象),我们的set、get方法都只能从当前线程中设置或者获得副本。

ThreadLocal实现机制

那么到底ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”的呢?先来看一下ThreadLocal的set()方法的源码是如何实现的:

/** 
    * Sets the current thread's copy of this thread-local variable 
    * to the specified value.  Most subclasses will have no need to 
    * override this method, relying solely on the {@link #initialValue} 
    * method to set the values of thread-locals. 
    * 
    * @param value the value to be stored in the current thread's copy of 
    *        this thread-local. 
    */  
   public void set(T value) {  
       Thread t = Thread.currentThread();  
       ThreadLocalMap map = getMap(t);  
       if (map != null)  
           map.set(this, value);  
       else  
           createMap(t, value);  
   } 

ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。

  1. 获取此时当前线程对象
  2. 尝试获取该线程内部的ThreadLocalMap对象
  3. 若为空(线程内ThreadLocals默认是null),创建一个map给这个线程t
    否则不为空的情况说明该线程内已经有了对应的键值对(this–xxx),而此时做的就是替换xxx

我们接着看上面代码中出现的getMap和createMap方法的实现:

/** 
 * @param  t the current thread 
 * @return the map 
 */  
ThreadLocalMap getMap(Thread t) {  
    return t.threadLocals;  
}  
  
/** 
 * @param t the current thread 
 * @param firstValue value for the initial entry of the map 
 * @param map the map to store. 
 */  
void createMap(Thread t, T firstValue) {  
    t.threadLocals = new ThreadLocalMap(this, firstValue);  

接下来再看一下ThreadLocal类中的get()方法:

/** 
 * 
 * @return the current thread's value of this thread-local 
 */  
public T get() {  
    Thread t = Thread.currentThread();  
    ThreadLocalMap map = getMap(t);  
    if (map != null) {  
        ThreadLocalMap.Entry e = map.getEntry(this);  
        if (e != null)  
            return (T)e.value;  
    }  
    return setInitialValue();  
} 

get方法也是如此,我们只能获取到当前线程的ThreadLocalMap对象,若这个Map是空的那么我们就设定初始值并返回,否则返回这个Map查询的结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值