读书人

懂得ThreadLocal和匿名类

发布时间: 2012-10-12 10:17:04 作者: rapoo

理解ThreadLocal和匿名类
ThreadLocal是什么
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。ThreadLocal并不是一个Thread,而是Thread的局部变量,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
线程局部变量并不是Java的新发明,很多语言(如IBM IBM XL FORTRAN)在语法层面就提供线程局部变量。在Java中没有提供在语言级支持,而是变相地通过ThreadLocal的类提供支持。所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。
ThreadLocal类很简单,只有4个方法,我们先来了解一下:
●void set(Object value)
设置当前线程的线程局部变量的值。
●public Object get()
该方法返回当前线程所对应的线程局部变量。
●public void remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式 调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
●protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执 行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?

其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本。

下面看一个例子:实现功能 :两个线程在data中存放不同的值,然后A/B分别获取

[html] view plaincopyprint?
  1. public class Main {
  2. private static int data = 0; //共享的资源
  3. public static void main(String[] args) {
  4. for (int i = 0; i < 2; i++) {
  5. new Thread(new Runnable() {
  6. @Override
  7. public void run() {
  8. data = new Random().nextInt();
  9. System.out.println(Thread.currentThread().getName()
  10. + " has put data :" + data);
  11. new A().get();
  12. new B().get();
  13. }
  14. }).start();
  15. }
  16. System.out.println(Thread.currentThread().getName());
  17. }
  18. static class A {
  19. public void get() {
  20. System.out.println("A from " + Thread.currentThread().getName()
  21. + " get data :" + data);
  22. }
  23. }
  24. static class B {
  25. public void get() {
  26. System.out.println("B from " + Thread.currentThread().getName()
  27. + " get data :" + data);
  28. }
  29. }
  30. }
main
Thread-0 has put data :-1344224869
Thread-1 has put data :-1710989617
A from Thread-0 get data :-1710989617
A from Thread-1 get data :-1710989617
B from Thread-0 get data :-1710989617
B from Thread-1 get data :-1710989617

上面出现了同步问题.

下面看这个实现,同步问题就不会再有了:

[java] view plaincopyprint?
  1. public class Main {
  2. private static Map<Thread, Integer> threadData = new HashMap<Thread, Integer>();//键值为Thread
  3. public static void main(String[] args) {
  4. for (int i = 0; i < 2; i++) {
  5. new Thread(new Runnable() { //new 接口,匿名对象
  6. public void run() {
  7. int data = new Random().nextInt();//---------------局部变量
  8. System.out.println(Thread.currentThread().getName()
  9. + " has put data :" + data);
  10. threadData.put(Thread.currentThread(), data);//放进map中,这样每个线程有自己的值,获取时根据哪条线程
  11. new A().get();
  12. new B().get();
  13. }
  14. }).start();
  15. }
  16. System.out.println(Thread.currentThread().getName());
  17. }
  18. static class A {
  19. public void get() {
  20. int data = threadData.get(Thread.currentThread());
  21. System.out.println("A from " + Thread.currentThread().getName()
  22. + " get data :" + data);
  23. }
  24. }
  25. static class B {
  26. public void get() {
  27. int data = threadData.get(Thread.currentThread());
  28. System.out.println("B from " + Thread.currentThread().getName()
  29. + " get data :" + data);
  30. }
  31. }
  32. }

上面改进的方法其实是相当于ThreadLocal的实现

[java] view plaincopyprint?
  1. public class Test { //封象
  2. public static void main(String[] args) {
  3. for (int i = 0; i < 2; i++) {
  4. new Thread(new Runnable() {
  5. @Override
  6. public void run() {
  7. int data = new Random().nextInt();
  8. System.out.println(Thread.currentThread().getName()
  9. + " has put data :" + data);
  10. MyThreadScopeData.getThreadInstance()
  11. .setName("name" + data);
  12. MyThreadScopeData.getThreadInstance().setAge(data);
  13. new A().get();
  14. new B().get();
  15. }
  16. }).start();
  17. }
  18. }
  19. static class A {
  20. public void get() {
  21. MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
  22. System.out
  23. .println("A from " + Thread.currentThread().getName()
  24. + " getMyData: " + myData.getName() + ","
  25. + myData.getAge());
  26. }
  27. }
  28. static class B {
  29. public void get() {
  30. MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
  31. System.out
  32. .println("B from " + Thread.currentThread().getName()
  33. + " getMyData: " + myData.getName() + ","
  34. + myData.getAge());
  35. }
  36. }
  37. }
  38. class MyThreadScopeData {
  39. private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<MyThreadScopeData>();
  40. public static/* synchronized */MyThreadScopeData getThreadInstance() {// 这里不再使用同步块了
  41. MyThreadScopeData instance = map.get();
  42. if (instance == null) {
  43. instance = new MyThreadScopeData();
  44. map.set(instance);
  45. }
  46. return instance;
  47. }
  48. private String name;
  49. private int age;
  50. public String getName() {
  51. return name;
  52. }
  53. public void setName(String name) {
  54. this.name = name;
  55. }
  56. public int getAge() {
  57. return age;
  58. }
  59. public void setAge(int age) {
  60. this.age = age;
  61. }
  62. }

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。Spring使用ThreadLocal解决线程安全问题
我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,
非线程安全
[java] view plaincopyprint?
  1. public class TopicDao {
  2. <span style="white-space:pre"> </span>private Connection conn;//①一个非线程安全的变量
  3. <span style="white-space:pre"> </span>public void addTopic(){
  4. <span style="white-space:pre"> </span>Statement stat = conn.createStatement();//②引用非线程安全变量
  5. }
  6. }

由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

TopicDao:线程安全

[java] view plaincopyprint?
  1. public class TopicDao {
  2. private static ThreadLocal connThreadLocal = new ThreadLocal();// ①使用ThreadLocal保存Connection变量
  3. public static Connection getConnection() {
  4. if (connThreadLocal.get() == null) {// ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,并将其保存到线程本地变量中。
  5. Connection conn = ConnectionManager.getConnection();
  6. connThreadLocal.set(conn);
  7. return conn;
  8. } else {
  9. return connThreadLocal.get();// ③直接返回线程本地变量
  10. }
  11. }
  12. public void addTopic() {
  13. Statement stat = getConnection().createStatement();// ④从ThreadLocal中获取线程对应的Connection
  14. }
  15. }
不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。
小结

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。


上面出现了匿名类,在java一些地方常用,在释疑:

匿名类格式: new interfacename(){......}; 或 new abstractlassname(){......};
匿名内部类由于没有名字,所以没有构造函数
在接口前面能不能使用new操作符
  看例子:
  public interface Humans{}
  如果我们手中没有Humans的具体实例类,又要在程序中用到一个。
  Object obj=new Humans(){};
  上面的语句是对的,其实不要认为接口能直接实例化了,注意后面还有"{}",这里其实new出来的是一个Humans的实现类,而在java内部,是一个匿名内部类。


  下面再看一个接口:
  interface Animal{ void eat(); }
  错误的形式(编译都通不过的):
  Animal a=new Animal(){};
正确的形式:
  Animal a=new Animal(){public void eat()};
直接在new后面跟接口名称来实例化一个接口是不可能的,还必须保证接口名称后面有完整的实现体。现在应该明白其实质了吧,我们new其实是操作在匿名内部类上的,不是用在接口本身上。接口本身是不能实例化的!

读书人网 >编程

热点推荐