ThreadLocal,是Thread Local Variable(线程局部变量)的意思。线程局部变量(ThreadLocal)的功能其实非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立的改变自己的副本,而不会和其他线程的副本冲突.从线程的角度看,就好像每一个线程都完全拥有该变量一样。
在日常开发中用到ThreadLocal的地方较少,但是在某些特殊的场景下,通过ThreadLocal可以轻松的实现一些看起来很复杂的功能,这一点在Android的源码中也有所体现,比如Looper、ActivityThread以及AMS中都用到了ThreadLocal。
具体到ThreadLocal的使用场景,这个不好统一描述,一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。比如对于Handler来说,它需要获取当前线程的Looper,很显然Looper的作用域就是线程并且不同线程具有不同的Looper,这是时候通过ThreadLocal就可以轻松实现Looper在线程中的存取。如果不采用ThreadLocal,那么系统就必须提供一个全局的哈希表供Handler查询指定线程的Looper,这样一来就必须提供一个类似于LooperManager的类了,但是系统并没有这么做而是选择ThreadLocal,这就是ThreadLocal的好处。
一.ThreadLocal的用法
ThreadLocal的用法非常简单,它只提供了3个public方法:
(1) T get() : 返回此线程局部变量中当前线程副本中的值。
(2) void remove() : 删除此线程局部变量中当前线程的值。
(3) void set(T value) :设置此线程局部变量中当前线程副本中的值。
下面通过demo来看一下:
public class Account {
/**
* 定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量
* 每个线程都会保留该变量的一个副本
*/
private ThreadLocal<String> testStr = new ThreadLocal<>();
//定义一个初始化testStr 成员变量的构造方法
public Account(String str) {
this.testStr.set(str);
Log.e("日志", "--------" + this.testStr.get());
}
public String getTestStr() {
return testStr.get();
}
public void setTestStr(String str) {
this.testStr.set(str);
}
}
public class MyThread extends Thread {
//定义一个Account类型的成员变量
private Account mAccount;
public MyThread(Account account, String name) {
super(name);
mAccount = account;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//当i==6时,输出将账号名替换成当前线程名
if (i == 6) {
mAccount.setTestStr(getName());
}
//输出同一个账号的账号名和循环变量
Log.e("日志", "run: " + mAccount.getTestStr() + "账号的i值:" + i);
}
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//启动两个线程,两个线程共享同一个Account
Account at = new Account("初始名");
/**
* 虽然两个线程共享同一个账号,即只有一个账户名
* 但由于账户名是ThreadLocal类型的,所以每个线程都
* 完全拥有各自的账户名副本,因此在i==6之后,将看到两个
* 线程访问同一个账户时出现不同的账户名
*/
new MyThread(at, "线程甲").start();
new MyThread(at, "线程乙").start();
}
}
上面Account类中的三行粗体字代表分别完成了创建ThreadLocal对象、从ThreadLocal中取出线程局部变量、修改线程局部变量的操作。由于程序中的账户名是一个ThreadLocal变量,所以虽然程序中只有一个Account对象,但两个子线程将会产生两个账户名(主线程也持有一个账户名的副本)。两个线程进行循环时都会在i==6时将账户名改为与线程名相同,这样就可以看到两个线程拥有两个账户名的情形,日志如下:
从上面程序可以看出,实际上账户名有三个副本,主线程1个,另外启动的两个线程各1个,它们的值互不干扰,每个线程完全拥有自己的ThreadLocal变量,这就是ThreadLocal的用途。
二.ThreadLocal场景分析
ThreadLocal和其他所有的同步机制一样,都是为了解决多线程中对同一变量的访问冲突,在普通的同步机制中,是通过对象加锁来实现多个线程对同一变量的安全访问的。该变量是多个线程共享的,所以要使用这种同步机制,需要很细致地分析在什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放该对象的锁等。在这种情况下,系统并没有将这份资源复制多份,只是采用了安全机制来控制对这份资源的访问而已。
ThreadLocal从另一个角度来解决多线程的并发访问,ThreadLocal将需要并发访问的资源的资源复制多份,每个线程拥有一份资源,每个线程都拥有自己的资源副本,从而也就没有必要对该资源进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的整个变量封装进ThreadLocal,或者把该对象与线程相关的状态使用ThreadLocal保存。