另一种线程安全机制:在事务管理中起到巨大作用的 ThreadLocal

ThreadLocal 是什么

ThreadLocal 不是一个线程,而是保存线程本地化对象的容器。

当运行于多线程环境的某个对象使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程分配一个独立的变量副本。

所以每个线程可以独立地改变自己的副本,而不会影响其他线程所对应的变量副本。

ThreadLocal 的接口方法

  • void set(Object value) :设置当前线程的线程局部变量值。
  • public Object get() :返回当前线程所对应的线程局部变量。
  • public void remove() :将当前线程的局部变量值删除。目的是为了减少内存的占用。
  • protected Object initialValue() :返回该线程局部变量的初始值。该方法是一个protected 的方法,显然是为了让之类覆盖而设计的。这个方法是延迟调用方法,在线程第一次调用 get() 或者 set(Object) 时才执行,并且仅执行一次。

ThreadLocal 维护一份独立变量副本的思很简单:在 ThreadLocal 类中有一个 Map,用于存储每个线程的变量副本,Map 中元素的键为线程对象,值为对应线程的变量副本。

ThreadLocal 实例

public class SequenceNumber {

    // 通过匿名内部类覆盖 initialValue() 方法,指定初始值
    private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
        @Override
        public Integer initialValue() {
            return 0;
        }
    };

    public int getNextNum() {
        seqNum.set(seqNum.get() + 1); // 获得下一个序列值
        return seqNum.get();
    }

    public static void main(String[] args) {
        SequenceNumber sn = new SequenceNumber();

        TestClient t1 = new TestClient(sn);	// 3个线程共享sn,各自产生序列号
        TestClient t2 = new TestClient(sn);
        TestClient t3 = new TestClient(sn);
        t1.start();
        t2.start();
        t3.start();

    }

    private static class TestClient extends Thread {
        private SequenceNumber sn;
        
        public TestClient(SequenceNumber sn){
            this.sn = sn;
        }

        @Override
        public void run(){
            for (int i = 0; i < 3; i++) {	//每个线程打印3个序列号
                System.out.println("thread[" + Thread.currentThread().getName() +
                        "] sn[" + sn.getNextNum() + "]");
            }
        }
    }

}

运行结果:

可以发现每个线程所产生的序列号都共享于同一个实例 SequenceNumber,但它们并没有互相干扰,而是各自产生独立的序列号。

与 Thread 同步机制比较

在同步机制中,通过对象的锁机制保证同一个时间只有一个线程访问变量。

而 ThreadLocal 是从另一个角度来解决并发问题,ThreadLocal 为没一个线程提供独立的变量副本,从而隔离了多个线程对访问数据冲突。

简单来说,Thread 机制是采用“以时间换空间” 的方式:访问串行化,对象共享化;而 ThreadLocal 采用了 “以空间换时间” 的方式:访问并行化,对象独享化。

Spring 使用 ThreadLocal 解决线程安全问题

在一般情况下,只有无状态的 Bean 才可以在多线程环境下共享。在 Spring 中,绝大部分 Bean 都可以生命为 singleton 作用域。

正是因为 Spring 对一些 Bean 中非线程安全的 ”状态性对象“ 采用 ThreadLocal 进行封装,让他们也成为线程安全的 “状态性对象”,因此,有状态的 Bean 就能过以 singleton 的方式在多线程中正常工作。

举个获取数据库连接的例子:

非线程安全

public class TopicDao {
    // 1.一个非线程安全变量
    private Connetion conn;
    
    public void addTopic() {
        // 2.引用非线程安全变量
        Statement stat = getConnection().createStatement();
    }
}

使用 ThreadLocal 进行改进,变成线程安全的状态

public class TopicDao {
    // 1. 使用 ThreadLocal 保存 Connection 变量
    private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<>();

    public static Connection getConnection() {

        // 2. 如果 connThreadLocal没有,则创建一个新的 Connection,并保存到线程本地变量中
        if (connThreadLocal.get() == null) {
            Connection conn = ConnectionManager.getConnection();
            connThreadLocal.set(conn);
            return conn;
        }else {
            // 3. 直接返回线程本地变量
            return connThreadLocal.get();
        }
    }

    public void addTopic() {
        // 4. 从 ThreadLocal 中获取线程对应的 Connection
        Statement stat = getConnection().createStatement();
    }

}

这个例子本身很粗糙,将 Connection 的 ThreadLocal 直接放在 DAO 中只能做到本 DAO 的多个方法共享 Connection 时不发生线程安全问题,而无法和其他 DAO 共用一个Connection 。要做到同一个事务多DAO 共享同一个 Connection ,必须在共同的外部类使用 ThreadLocal 保存 Connection 。

但这个例子也基本上说明了 Spring 对所有状态类线程安全化的解决思路。

posted @ 2021-07-06 17:17  乐子不痞  阅读(102)  评论(0编辑  收藏  举报
回到顶部