线程本地变量ThreadLocal

本文详细介绍了ThreadLocal的概念及其在Java并发编程中的应用场景。包括如何利用ThreadLocal解决多线程环境下资源竞争的问题,避免线程安全问题的同时减少服务器压力。并探讨了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()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

相关链接请参考:http://my.oschina.net/xianggao/blog/392440?fromerr=nKHw4fBT

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值