并发库应用之一 & ThreadLocal实现线程范围的共享变量

  ThreadLocal用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。

  每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map中增加一条记录,key分别是各自的线程,value是各自的set方法传进去的值,在线程结束时可以调用ThreadLocal.clear()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的ThreadLocal变量。

ThreadLocal的应用场景:

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

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

  3. 例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个。

  在web应用中,一个请求就是一个线程,那么如何区分哪些参数属于哪个线程呢?比如struts中,A用户登录,B用户也登录,那么在Action中怎么区分哪个是A用户的数据,哪个是B用户的数据。这就涉及到ThreadLocal类了,将变量与当前线程绑定。比如struts中,有一个容器类,那么A用户将数据放在A的容器中,B用户将数据放在B的容器中,然后再将容器与线程绑定,这样的话,A请求的线程处理A容器的数据,B请求的线程处理B容器的数据,而不会混淆。

实验案例:定义一个全局共享的ThreadLocal变量,然后启动多个线程向该ThreadLocal变量中存储一个随机值,接着各个线程调用另外其他多个类的方法,这多个类的方法中读取这个ThreadLocal变量的值,就可以看到多个类在同一个线程中共享同一份数据。

要求:实现对ThreadLocal变量的封装,让外界不要直接操作ThreadLocal变量。

 1. 首先编写共享容器,供外部多线程使用,类名为:ThreadShareData

/**
 * 定义一个线程共享的对象
 * @author liangyongxing
 * @createTime 2017/02/21
 */
public class ThreadShareData {
    private String name;
    private int age;
    
    //防止new,生成单例类
    private ThreadShareData(){}
    /**
     * ThreadLocal:将变量与当前线程绑定,相当于Map<Thread, value>
     * 此处使用的是饱汉模式构造
     */
    private static ThreadLocal<ThreadShareData> threadLocal = new ThreadLocal<>();
    /**
     * 返回当前线程的单例
     * 此处不需要使用关键字synchronized,想想为什么?
     */
    public static ThreadShareData getCurrentThreadInstance() {
        ThreadShareData shareData = threadLocal.get();
        if (shareData == null) {
            shareData = new ThreadShareData();
            threadLocal.set(shareData);
        }
        return shareData;
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

2. 模拟出两个客户端,后续在主线程中分别启动这两个客户端线程进行模拟,具体的两个类分别为:ClientA和ClientB

public class ClientA {
    public void get() {
        ThreadShareData shardDataA = ThreadShareData.getCurrentThreadInstance();
        System.out.println(String.format("ClientA name is:%s 	 data.name is:%s 	 data.age is:%s", 
                Thread.currentThread().getName(), shardDataA.getName(), shardDataA.getAge()));
    }
}
public class ClientB {
    public void get() {
        ThreadShareData shardDataB = ThreadShareData.getCurrentThreadInstance();
        System.out.println(String.format("ClientB name is:%s 	 data.name is:%s 	 data.age is:%s", 
                Thread.currentThread().getName(), shardDataB.getName(), shardDataB.getAge()));
    }
}

3. 编写对应的主线程函数也就是我们平时说的主函数

public class ThreadLocalMain {
    public static void main(String[] args) {
        Thread th = null//同时启动ClientA和ClientB两次线程,查看打印结果
        for (int i = 0; i < 2; i++) {
            th = new Thread(new Runnable() {
                @Override
                public void run() {
                    //生成随机数
                    int currentRandomInt = new Random().nextInt();
                    System.out.println(String.format("%s--%d", Thread.currentThread().getName(), currentRandomInt));
                    ThreadShareData shardData = ThreadShareData.getCurrentThreadInstance();
                    shardData.setName(String.format("name:%d", currentRandomInt));
                    shardData.setAge(currentRandomInt);
                    
                    //输出两个模块的值
                    new ClientA().get();
                    new ClientB().get();
                }
            });
            th.start();
        }
    }
}

输出结果如下:

  可以发现,线程A和线程B在第一次同步操作对象data的时候是共享对象的,而第一次的线程A和第二次的线程A他们之间是相互独立的,这就验证了ThreadLocal可以实现不同线程内的数据共享。

总结:ThreadLocal存放的类型,针对基本类型的数据封装这种应用相对很少见,而对对象类型的数据封装才是比较常见的,即让某个类针对不同线程分别创建一个独立的实例对象,见上例。用大白话的意思就是说,一个ThreadLocal代表一个变量,故其中往里只能放一个数据,你有两个变量都要线程范围内共享,则要定义两个ThreadLocal对象。如果有一百个变量要线程共享呢?那请先定义一个对象来装这一百个变量,然后在ThreadLocal存储这一个对象。

提示:局部数据线程共享讲完之后,我们就开始进入java5之后为我们提供的最新多线程友好工具类,下一篇先讲一下我们常用到的基本类型并发环境下类的使用,具体详情请查看我的下一篇博客:并发库应用之二 & Java原子性操作类应用

原文地址:https://www.cnblogs.com/liang1101/p/6422149.html