线程本地变量ThreadLocal

一、本地线程变量使用场景

并发应用的一个关键地方就是共享数据。如果你创建一个类对象,实现Runnable接口,然后多个Thread对象使用同样的Runnable对象,全部的线程都共享同样的属性。这意味着,如果你在一个线程里改变一个属性,全部的线程都会受到这个改变的影响。

有时,你希望程序里的各个线程的属性不会被共享。 Java 并发 API提供了一个很清楚的机制叫本地线程变量即ThreadLocal

模拟ThreadLocal类实现:线程范围内的共享变量,每个线程只能访问他自己的,不能访问别的线程。

二、对ThreadLocal的理解

      ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做本地线程变量,其实意思差不多。

ThreadLocal和本地线程没有半毛钱关系,更不是一个特殊的Thread,它只是一个线程的局部变量(其实就是一个Map用于存储每一个线程的变量副本,Map中元素的Key为线程对象,而Value对应线程的变量副本),ThreadLocal会为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

对于多线程资源共享的问题,同步机制(Synchronized)采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

官方对ThreadLocal的描述:

    

1、每个线程都有自己的局部变量

每个线程都有一个独立于其他线程的上下文来保存这个变量,一个线程的本地变量对其他线程是不可见的(有前提,后面解释)

2、独立于变量的初始化副本

ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝。

3、状态与某一个线程相关联

ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用ThreadLocal至关重要。

通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。

我们还是先来看一个例子:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

class ConnectionManager {

    

    private static Connection connect = null;

    

    public static Connection openConnection() {

        if(connect == null){

            connect = DriverManager.getConnection();

        }

        return connect;

    }

    

    public static void closeConnection() {

        if(connect!=null)

            connect.close();

    }

}

  假设有这样一个数据库链接管理类,这段代码在单线程中使用是没有任何问题的,但是如果在多线程中使用呢?很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。

  所以出于线程安全的考虑,必须将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理。

  这样将会大大影响程序执行效率,因为一个线程在使用connect进行数据库操作的时候,其他线程只有等待。

  那么大家来仔细分析一下这个问题,这地方到底需不需要将connect变量进行共享?事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。

  到这里,可能会有朋友想到,既然不需要在线程之间共享这个变量,可以直接这样处理,在每个需要使用数据库连接的方法中具体使用时才创建数据库链接,然后在方法调用完毕再释放这个连接。比如下面这样:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

class ConnectionManager {

    

    private  Connection connect = null;

    

    public Connection openConnection() {

        if(connect == null){

            connect = DriverManager.getConnection();

        }

        return connect;

    }

    

    public void closeConnection() {

        if(connect!=null)

            connect.close();

    }

}

class Dao{

    public void insert() {

        ConnectionManager connectionManager = new ConnectionManager();

        Connection connection = connectionManager.openConnection();

        

        //使用connection进行操作

        

        connectionManager.closeConnection();

    }

}

  这样处理确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不尽严重影响程序执行效率,还可能导致服务器压力巨大。

  那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。

  但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

三、深入解析ThreadLocal类

在上面谈到了对ThreadLocal的一些理解,那我们下面来看一下具体ThreadLocal是如何实现的。

  先了解一下ThreadLocal类提供的几个方法:

?


1

2

3

4

public T get() { }

public void set(T value) { }

public void remove() { }

protected T initialValue() { }

  get()方法是用来获取ThreadLocal在当前线程中保存的变量副本;

set()用来设置当前线程中变量的副本;

remove()用来移除当前线程中变量的副本;

initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法;

ThreadLocal是如何为每个线程创建变量的副本的:

        1、在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,Key为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

   2、初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

  3、在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

下面通过一个例子来证明通过ThreadLocal能达到在每个线程中创建变量副本的效果:

public class Test {

    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();

    ThreadLocal<String> stringLocal = new ThreadLocal<String>();

    

    public void set() {

        longLocal.set(Thread.currentThread().getId());

        stringLocal.set(Thread.currentThread().getName());

    }

    

    public long getLong() {

        return longLocal.get();

    }

    

    public String getString() {

        return stringLocal.get();

    }

    

    public static void main(String[] args) throws InterruptedException {

        final Test test = new Test();

        

        

        test.set();

        System.out.println(test.getLong());

        System.out.println(test.getString());

    

        

        Thread thread1 = new Thread(){

            public void run() {

                test.set();

                System.out.println(test.getLong());

                System.out.println(test.getString());

            };

        };

        thread1.start();

        thread1.join();

        

        System.out.println(test.getLong());

        System.out.println(test.getString());

    }

}

  这段代码的输出结果为:

  

  从这段代码的输出结果可以看出,在main线程中和thread1线程中,longLocal保存的副本值和stringLocal保存的副本值都不一样。最后一次在main线程再次打印副本值是为了证明在main线程中和thread1线程中的副本值确实是不同的。

总结一下:

  1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;

  2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;

四.ThreadLocal的应用场景

最常见的ThreadLocal使用场景为:用来解决数据库连接、Session管理,多线程单例模式访问;

订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。

银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。

我们先看一个简单的例子:

public class ThreadLocalTest {

        

        //创建一个Integer型的线程本地变量

    public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {

        @Override

        protected Integer initialValue() {

            return 0;

        }

    };

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[5];

        for (int j = 0; j < 5; j++) {       

               threads[j] = new Thread(new Runnable() {

                @Override

                public void run() {

                                        //获取当前线程的本地变量,然后累加5次

                    int num = local.get();

                    for (int i = 0; i < 5; i++) {

                        num++;

                    }

                                        //重新设置累加后的本地变量

                    local.set(num);

                    System.out.println(Thread.currentThread().getName() + " : "+ local.get());

                }

            }, "Thread-" + j);

        }

        for (Thread thread : threads) {

            thread.start();

        }

    }

}

运行后结果:

Thread-0 : 5

Thread-4 : 5

Thread-2 : 5

Thread-1 : 5

Thread-3 : 5

我们看到,每个线程累加后的结果都是5,各个线程处理自己的本地变量值,线程之间互不影响。

如:数据库连接:

Session连接:

五、ThreadLocal使用的一般步骤

1、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。

2、在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。

3、在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

时间: 2024-08-05 15:08:45

线程本地变量ThreadLocal的相关文章

线程本地变量ThreadLocal源码解读

  一.ThreadLocal基础知识   原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板类并未采用线程同步机制,因为线程同步会影响并发性和系统性能,而且实现难度也不小. ThreadLocal在Spring中发挥着重要的作用.在管理request作用域的bean,事务管理,任务调度,AOP等模块中都出现了它的身影. ThreadLocal介绍: 它不是一个线程,而是线程的一个本地化

深入理解线程本地变量ThreadLocal

ThreadLocal理解: 如果在多线程并发环境中,一个可变对象涉及到共享与竞争,那么该可变对象就一定会涉及到线程间同步操作,这是多线程并发问题. 否则该可变对象将作为线程私有对象,可通过ThreadLocal进行管理,实现线程间私有对象隔离的目的. 可以发现,ThreadLocal并没有解决多线程并发的问题,因为ThreadLocal管理的可变对象的性质本来就不会涉及到多线程并发而引发的共享.竞争和同步问题,使用ThreadLocal管理只是方便了多线程获取和使用该私有可变对象的途径和方式.

JAVA线程本地变量ThreadLocal和私有变量的区别

ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些. 所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及. ThreadLocal的接口方法 ThreadLocal类接口很简单,只有4个方法,我们先来了解一下: void set(Object value) public void remove() 将当前线程局部变量的值删除,目的是为了减

Java并发机制(4)--ThreadLocal线程本地变量(转)

转自:博客园-海子-http://www.cnblogs.com/dolphin0520/p/3920407.html Java并发编程:深入剖析ThreadLocal 首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的.各个线程中访问的是不同的对象. 另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal

.Net学习难点讨论系列17 - 线程本地变量的使用

*:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* BLOCKS =============================================================================*/ p, blockquote, ul, ol, dl, table, pre { margin: 15px 0; } /* HEAD

java线程 在其他对象上同步、线程本地存储ThreadLocal:thinking in java4 21.3.6

package org.rui.thread.concurrency; /** * 在其他对象上同步 * synchronized 块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象 * :synchronized(this), 在 这种方式中,如果获得了synchronized块上的锁, * 那么该对象其他的synchronized方法和临界区就不能被调用了. * 因此,如果在this上同步,临界区的效果就会直接缩小在同步的范围内. * * 有时必须在另一个

线程本地变量的使用

Net学习难点讨论系列17 - 线程本地变量的使用 关于C#多线程的文章,大部分都在讨论线程的起停或者是多线程同步问题.多线程同步就是在不同线程中访问同一个变量(一般是线程工作函数外部的变量),众所周知在不使用线程同步的机制下,由于竟态的存在会使某些线程产生脏读或者是覆盖其它线程已写入的值(各种混乱).而另外一种情况就是我们想让线程所访问的变量属于线程自身所有,这就是所谓的线程本地变量.下文我们将逐渐扩展一个最简单的示例代码,来展示上面所说的变量并发访问以及线程本地变量的区别和各自解决方案. 这

java线程本地变量

ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名为ThreadLocalVar更加合适.线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突. 从线程的角度看,每个线程都保持一个对其线程局部变量副本

.Net - 线程本地变量(存储)的使用

关于C#多线程的文章,大部分都在讨论线程的开始与停止或者是多线程同步问题.多线程同步就是在不同线程中访问同一个变量或共享资源,众所周知在不使用线程同步的机制下,由于竞争的存在会使某些线程产生脏读或者是覆盖其它线程已写入的值(各种混乱). 而另外一种情况就是多线程时我们想让每个线程所访问的变量只属于各自线程自身所有,这就是所谓的线程本地变量. 线程本地变量不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用线程本来变量至关