ThreadLocal:不同于“锁”的优化

现在都是多核CPU了,多线程方式可以提高系统的性能,充分利用各个CPU,但是,多线程也增加了很多额外的开销,例如线程之间的切换,线程调度,还有最重要的多线程同步的问题,通常我们都会使用锁来控制线程同步,临界资源的访问通常我们都只加上一把锁,这样做的问题是,一个线程占用了这把锁,其他想要访问临界资源的线程就只能停下来等待,等待锁的释放,如果一个线程拿住了锁后,久久不放,那么越来越多的其他线程被阻塞,最后会导致程序的整体性能下降,所以锁的优化就很有必要了。例如大家可能会立刻想到的就是读写锁,把读线程和写线程的锁分离开来,因为在读操作时,多个线程同时读某一个临界资源,并不会破坏数据的一致性,因为读操作并不会对值做任何的修改,所以多线程读操作之间是并行的,而写-写操作之间就会互相阻塞,读-写操作也会互相阻塞,但是有一种优化方法是写操作把资源复制一份副本出来寄存器中,对这个副本进行写入修改等操作,最后再把副本覆盖回去存储器中,这一在写操作时就可以同时做读操作,这里不过多叙述,因为这篇日志要说的,是另外一种多线程优化方式,ThreadLocak。这种优化方式是用来保证某一个对象在某一个线程中的安全(表述的不太好,不过通过下面举的例子,我相信大家能明白它是干什么的),它不同于锁,锁是控制了某一时刻只能一个线程访问某一个临界资源,而ThreadLocal则是为每一个线程都增加了资源,大家不用抢,你们都可以访问到这个资源,且不会造成安全性问题。这是怎么做到的?来看看例子,首先来看一个线程不安全的:

ThreadLocal示例

这段代码要做的,就是用10个线程分别对对象ID中的value变量做一万次自增操作,最后我们希望看到的value是等于十万,但是因为多线程下,我们的自增操作并没有做同步控制,经常会出现最后value的值小于十万的情况,原因很简单,多线程可能对value做了重复的写入,这里不详述。解决的方法大家都想到,就是在increase()前加锁,increase()后释放锁就行了,但是当一个线程在对value做操作时,其余的线程全都被阻塞了,前面也说到,这样做会降低程序整体的性能,保证了安全,但牺牲了大多的时间。

      另一种方法是ThreadLocal,“ThreadLocal,即线程本地变量,或叫线程局部变量。ThreadLocal为每一个线程创建一个对象的副本,副本包含对象里的变量。”这样每一个线程都得到这个对象的资源(它是一份复制出来的副本),且ThreadLocal是线程的局部变量,这样每一个线程访问自己的局部变量,我们知道,局部变量是存放在各自的线程栈上的,属于线程的私有数据,因此这一就能保证数据操作的安全,每一个线程对各自的数据做操作,其他线程无法干扰,具体来看代码:

ThreadLocal提供了一个容器的作用,每一个线程执行时,先设置该线程持有的对象(这里是ThreadLocalDemo的实例对象TL),放到ThreadLocal容器中,ThreadLocal便会为我们的线程创建一个ThreadLocalDemo对象的副本,该线程访问到的ThreadLocalDemo中的变量value是对该线程唯一的,是该线程的一个局部变量,其他线程无法访问。为了看到每一个线程中的value的变化,我们让自增操作次数减少到10,看看每一个线程中的value是不是属于各自线程的私有局部变量:

可以看到,各自线程对于各自的对象TL的value操作,互不影响,大家都要对value做自增操作,那么就各自对自己的副本value做自增操作,最后想要获得总和,就用一个sum变量来累加就可以了。

      大家可能疑惑,这样做的目的是什么?实际应用中什么时候用得上这样的操作?对于多个线程想访问同一对象,为了数据一致性,我们会通过加锁,让一个对象每次只能被一个线程访问,这样其他线程会被阻塞,导致程序整体性能下降。ThreadLocal的意义在于它为每一个线程都分配这个对象的副本,这样大家都可以访问这个对象里的变量,但是它们又是各地独立分开的,线程之间的这个副本变量只有该线程自己能访问,这样来保证对象的线程安全。

      具体实际应用中的例子,有一个经典的场景就是数据库连接,假如我们只有一个数据库连接对象Connection,假如多个线程例如T1和T2都共享这个Connection对象,在没做同步控制的情况下,有可能出现T1线程连接数据库做提交操作前,T2数据库因为工作完成而关闭了数据库连接,导致T1线程做提交操作时,出现数据库未连接的问题。如果用锁来做同步控制,就会造成当大量数据库操作需要进行时,多线程等待锁而浪费了大量的时间,降低了性能和效率。那么此时使用ThreadLocal就能很好地解决这个问题,ThreadLocal为每一个数据库访问线程都分配一个数据库连接对象Connection,这样各个线程各自对数据库做连接,修改和管理操作都互不影响,因为这个数据库连接对象被创建了各个线程的私有副本,是一个线程内的局部变量,各线程之间互不影响,可以同步进行。

ThreadLocal内部实现

ThreadLocal的实现原理,上面也说了,它是为当前线程复制了一份对象的副本,这个副本成为该线程的私有局部变量,让线程访问各自的这个副本,来达到数据的安全。来看看它是怎么实现对象线程的安全的:

它有四个方法,initialValue​()、set(),get()和remove()。set()方法用来设置当前线程的对象的副本; get()就是获得当前线程中保存的ThreadLocal中的对象副本;remove()用来删除当前线程中的ThreadLocal中保存的对象的副本。主要来看set()和get()两个方法:

set():

可以看到,在set()方法中,首先获得当前Thread的对象,并且拿到该线程对象的ThreadLocalMap,最后将value的值放入到当前线程的ThreadLocalMap中。

get()方法中,同样是先获得当前线程的对象,然后从当前线程对象中获得该线程的ThreadlLocalMap,从这个线程的ThreadLocalMap中取得value并返回出去。

      由此可以看出,在每一个线程Thread中,都有一个Thread类的内部成员:

ThreadLocal.ThreadLocalMap threadLocals = null;

我们设置的副本数据都是保存在这个threadLocals中,因为这是线程的内部变量,只能当前线程能访问,所以就是通过这个内部成员来保证数据的安全性。

回收ThreadLocal

上面还有一个remove()方法就是用来释放ThreadLocal变量,而且这个释放很重要,由上面的set()和get()方法可以看到,对象副本是保存在线程Thread类的内部成员threadLocals中,当线程退出后,线程实例对象被回收,这样没问题,但如果你使用的是线程池,那么里面的线程因为可能会被复用,而不会退出,一直维持在线程池内,这样,对于副本的引用将跟着这个线程实例回到线程池中,后果可能会导致副本一直占用内存。所以,在线程结束时,显式调用ThreadLocal.remove()回收ThreadLocal的变量很重要。

简单使用例子已上传:

https://github.com/justinzengtm/Java-Multithreading/tree/master/ThreadLocal

发布了97 篇原创文章 · 获赞 71 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/justinzengTM/article/details/89679902