欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

简单分析Java线程编程中ThreadLocal类的使用

程序员文章站 2024-03-07 16:20:15
一、概述   threadlocal是什么呢?其实threadlocal并非是一个线程的本地实现版本,它并不是一个thread,而是threadlocalva...

一、概述
 
threadlocal是什么呢?其实threadlocal并非是一个线程的本地实现版本,它并不是一个thread,而是threadlocalvariable(线程局部变量)。也许把它命名为threadlocalvar更加合适。线程局部变量(threadlocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
 
从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 threadlocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
 
通过threadlocal存取的数据,总是与当前线程相关,也就是说,jvm 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
 
threadlocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在threadlocal类中有一个map,用于存储每一个线程的变量的副本。
 
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而threadlocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
 
二、api说明
 
threadlocal()
          创建一个线程本地变量。
 
t get()
          返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
 
protected  t initialvalue()
          返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(t) 方法,则不会在线程中再调用 initialvalue 方法。
 
   若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 threadlocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialvalue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。
 
void remove()
          移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialvalue。
 
void set(t value)
          将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialvalue() 方法来设置线程局部变量的值。
 
在程序中一般都重写initialvalue方法,以给定一个特定的初始值。

三、一.对threadlocal的理解

  threadlocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道threadlocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

  这句话从字面上看起来很容易理解,但是真正理解并不是那么容易。

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

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进行了修改的。

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

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要大。

四、实例

创建一个bean,通过不同的线程对象设置bean属性,保证各个线程bean对象的独立性。
 

/**
 * created by intellij idea.
 * user: leizhimin
 * date: 2007-11-23
 * time: 10:45:02
 * 学生
 */
public class student {
  private int age = 0;  //年龄
 
  public int getage() {
    return this.age;
  }
 
  public void setage(int age) {
    this.age = age;
  }
}
 
/**
 * created by intellij idea.
 * user: leizhimin
 * date: 2007-11-23
 * time: 10:53:33
 * 多线程下测试程序
 */
public class threadlocaldemo implements runnable {
  //创建线程局部变量studentlocal,在后面你会发现用来保存student对象
  private final static threadlocal studentlocal = new threadlocal();
 
  public static void main(string[] agrs) {
    threadlocaldemo td = new threadlocaldemo();
    thread t1 = new thread(td, "a");
    thread t2 = new thread(td, "b");
    t1.start();
    t2.start();
  }
 
  public void run() {
    accessstudent();
  }
 
  /**
   * 示例业务方法,用来测试
   */
  public void accessstudent() {
    //获取当前线程的名字
    string currentthreadname = thread.currentthread().getname();
    system.out.println(currentthreadname + " is running!");
    //产生一个随机数并打印
    random random = new random();
    int age = random.nextint(100);
    system.out.println("thread " + currentthreadname + " set age to:" + age);
    //获取一个student对象,并将随机数年龄插入到对象属性中
    student student = getstudent();
    student.setage(age);
    system.out.println("thread " + currentthreadname + " first read age is:" + student.getage());
    try {
      thread.sleep(500);
    }
    catch (interruptedexception ex) {
      ex.printstacktrace();
    }
    system.out.println("thread " + currentthreadname + " second read age is:" + student.getage());
  }
 
  protected student getstudent() {
    //获取本地线程变量并强制转换为student类型
    student student = (student) studentlocal.get();
    //线程首次执行此方法的时候,studentlocal.get()肯定为null
    if (student == null) {
      //创建一个student对象,并保存到本地线程变量studentlocal中
      student = new student();
      studentlocal.set(student);
    }
    return student;
  }
}

 
运行结果:

a is running! 
thread a set age to:76 
b is running! 
thread b set age to:27 
thread a first read age is:76 
thread b first read age is:27 
thread a second read age is:76 
thread b second read age is:27 

 
可以看到a、b两个线程age在不同时刻打印的值是完全相同的。这个程序通过妙用threadlocal,既实现多线程并发,游兼顾数据的安全性。

五、threadlocal使用的一般步骤
 
1、在多线程的类(如threaddemo类)中,创建一个threadlocal对象threadxxx,用来保存线程间需要隔离处理的对象xxx。
2、在threaddemo类中,创建一个获取要隔离访问的数据的方法getxxx(),在方法中判断,若threadlocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
3、在threaddemo类的run()方法中,通过getxxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。