关于ThreadLocal 的用法

ThreadLocal,直译为“线程本地”或“本地线程”,如果你真的这么认为,那就错了!其实,它就是一个容器,用于存放线程的局部变量,我认为应该叫做 ThreadLocalVariable(线程局部变量)才对,真不理解为什么当初 Sun 公司的工程师这样命名。

早在 JDK 1.2 的时代,java.lang.ThreadLocal 就诞生了,它是为了解决多线程并发问题而设计的,只不过设计得有些难用,所以至今没有得到广泛使用。其实它还是挺有用的,不相信的话,我们一起来看看这个例子吧。

一个序列号生成器的程序,可能同时会有多个线程并发访问它,要保证每个线程得到的序列号都是自增的,而不能相互干扰。

先定义一个接口:

1 public interface Sequence
{
2  
3     int getNumber();
4 }

每次调用 getNumber() 方法可获取一个序列号,下次再调用时,序列号会自增。

再做一个线程类:

01 public class ClientThread extends Thread
{
02  
03     private Sequence
sequence;
04  
05     public ClientThread(Sequence
sequence) {
06         this.sequence
= sequence;
07     }
08  
09     @Override
10     public void run()
{
11    
    
for (int i
0;
i < 
3;
i++) {
12    
        
System.out.println(Thread.currentThread().getName()
"
=> "
 +
sequence.getNumber());
13    
    
}
14     }
15 }

在线程中连续输出三次线程名与其对应的序列号。

我们先不用 ThreadLocal,来做一个实现类吧。

01 public class SequenceA implements Sequence
{
02  
03     private static int number
0;
04  
05     public int getNumber()
{
06         number
= number + 
1;
07         return number;
08     }
09  
10     public static void main(String[]
args) {
11         Sequence
sequence = 
new SequenceA();
12  
13         ClientThread
thread1 = 
new ClientThread(sequence);
14         ClientThread
thread2 = 
new ClientThread(sequence);
15         ClientThread
thread3 = 
new ClientThread(sequence);
16  
17         thread1.start();
18         thread2.start();
19         thread3.start();
20     }
21 }

序列号初始值是0,在 main() 方法中模拟了三个线程,运行后结果如下:

Thread-0 => 1

Thread-0 => 2

Thread-0 => 3

Thread-2 => 4

Thread-2 => 5

Thread-2 => 6

Thread-1 => 7

Thread-1 => 8

Thread-1 => 9

由于线程启动顺序是随机的,所以并不是0、1、2这样的顺序,这个好理解。为什么当 Thread-0 输出了1、2、3之后,而 Thread-2 却输出了4、5、6呢?线程之间竟然共享了 static 变量!这就是所谓的“非线程安全”问题了。

那么如何来保证“线程安全”呢?对应于这个案例,就是说不同的线程可拥有自己的 static 变量,如何实现呢?下面看看另外一个实现吧。

01 public class SequenceB implements Sequence
{
02  
03     private static ThreadLocal<Integer>
numberContainer = 
new ThreadLocal<Integer>()
{
04    
    
@Override
05    
    
protected Integer
initialValue() {
06    
        
return 0;
07    
    
}
08     };
09  
10     public int getNumber()
{
11    
    
numberContainer.set(numberContainer.get()
1);
12    
    
return numberContainer.get();
13     }
14  
15     public static void main(String[]
args) {
16    
    
Sequence
sequence = 
new SequenceB();
17  
18    
    
ClientThread
thread1 = 
new ClientThread(sequence);
19    
    
ClientThread
thread2 = 
new ClientThread(sequence);
20    
    
ClientThread
thread3 = 
new ClientThread(sequence);
21  
22    
    
thread1.start();
23    
    
thread2.start();
24    
    
thread3.start();
25     }
26 }

通过 ThreadLocal 封装了一个 Integer 类型的 numberContainer 静态成员变量,并且初始值是0。再看 getNumber() 方法,首先从 numberContainer 中 get 出当前的值,加1,随后 set 到 numberContainer 中,最后将 numberContainer 中 get 出当前的值并返回。

是不是很恶心?但是很强大!确实稍微饶了一下,我们不妨把 ThreadLocal 看成是一个容器,这样理解就简单了。所以,这里故意用 Container 这个单词作为后缀来命名 ThreadLocal 变量。

运行结果如何呢?看看吧。

Thread-0 => 1

Thread-0 => 2

Thread-0 => 3

Thread-2 => 1

Thread-2 => 2

Thread-2 => 3

Thread-1 => 1

Thread-1 => 2

Thread-1 => 3

每个线程相互独立了,同样是 static 变量,对于不同的线程而言,它没有被共享,而是每个线程各一份,这样也就保证了线程安全。 也就是说,TheadLocal 为每一个线程提供了一个独立的副本!

搞清楚 ThreadLocal 的原理之后,有必要总结一下 ThreadLocal 的 API,其实很简单。

  1. public void set(T value):将值放入线程局部变量中
  2. public T get():从线程局部变量中获取值
  3. public void remove():从线程局部变量中移除值(有助于 JVM 垃圾回收)
  4. protected T initialValue():返回线程局部变量中的初始值(默认为 null)

为什么 initialValue() 方法是 protected 的呢?就是为了提醒程序员们,这个方法是要你们来实现的,请给这个线程局部变量一个初始值吧。

了解了原理与这些 API,其实想想 ThreadLocal 里面不就是封装了一个 Map 吗?自己都可以写一个 ThreadLocal 了,尝试一下吧。

01 public class MyThreadLocal<T>
{
02  
03     private Map<Thread,
T> container = Collections.synchronizedMap(
new HashMap<Thread,
T>());
04  
05     public void set(T
value) {
06         container.put(Thread.currentThread(),
value);
07     }
08  
09     public T
get() {
10         Thread
thread = Thread.currentThread();
11         T
value = container.get(thread);
12         if (value
== 
null &&
!container.containsKey(thread)) {
13             value
= initialValue();
14             container.put(thread,
value);
15         }
16         return value;
17     }
18  
19     public void remove()
{
20         container.remove(Thread.currentThread());
21     }
22  
23     protected T
initialValue() {
24         return null;
25     }
26 }

以上完全山寨了一个 ThreadLocal,其中中定义了一个同步 Map(为什么要这样?请读者自行思考),代码应该非常容易读懂。

下面用这 MyThreadLocal 再来实现一把看看。

01 public class SequenceC implements Sequence
{
02  
03     private static MyThreadLocal<Integer>
numberContainer = 
new MyThreadLocal<Integer>()
{
04         @Override
05         protected Integer
initialValue() {
06             return 0;
07         }
08     };
09  
10     public int getNumber()
{
11         numberContainer.set(numberContainer.get()
1);
12         return numberContainer.get();
13     }
14  
15     public static void main(String[]
args) {
16         Sequence
sequence = 
new SequenceC();
17  
18         ClientThread
thread1 = 
new ClientThread(sequence);
19         ClientThread
thread2 = 
new ClientThread(sequence);
20         ClientThread
thread3 = 
new ClientThread(sequence);
21  
22         thread1.start();
23         thread2.start();
24         thread3.start();
25     }
26 }

以上代码其实就是将 ThreadLocal 替换成了 MyThreadLocal,仅此而已,运行效果和之前的一样,也是正确的。

其实 ThreadLocal 可以单独成为一种设计模式,就看你怎么看了。

ThreadLocal 具体有哪些使用案例呢?

我想首先要说的就是:通过 ThreadLocal 存放 JDBC Connection,以达到事务控制的能力。

还是保持我一贯的 Style,用一个 Demo 来说话吧。用户提出一个需求:当修改产品价格的时候,需要记录操作日志,什么时候做了什么事情。

想必这个案例,只要是做过应用系统的小伙伴们,都应该遇到过吧?无外乎数据库里就两张表:product 与 log,用两条 SQL 语句应该可以解决问题:

1 update product set price
= ? 
where id
= ?
2 insert into log
(created, description) 
values (?,
?)

But!要确保这两条 SQL 语句必须在同一个事务里进行提交,否则有可能 update 提交了,但 insert 却没有提交。如果这样的事情真的发生了,我们肯定会被用户指着鼻子狂骂:“为什么产品价格改了,却看不到什么时候改的呢?”。

聪明的我在接到这个需求以后,是这样做的:

首先,我写一个 DBUtil 的工具类,封装了数据库的常用操作:

01 public class DBUtil
{
02     //
数据库配置
03     private static final String
driver = 
"com.mysql.jdbc.Driver";
04     private static final String
url = 
"jdbc:mysql://localhost:3306/demo";
05     private static final String
username = 
"root";
06     private static final String
password = 
"root";
07  
08     //
定义一个数据库连接
09     private static Connection
conn = 
null;
10  
11     //
获取连接
12     public static Connection
getConnection() {
13         try {
14             Class.forName(driver);
15             conn
= DriverManager.getConnection(url, username, password);
16         catch (Exception
e) {
17             e.printStackTrace();
18         }
19         return conn;
20     }
21  
22     //
关闭连接
23     public static void closeConnection()
{
24         try {
25             if (conn
!= 
null)
{
26                 conn.close();
27             }
28         catch (Exception
e) {
29             e.printStackTrace();
30         }
31     }
32 }

里面搞了一个 static 的 Connection,这下子数据库连接就好操作了,牛逼吧!

然后,我定义了一个接口,用于给逻辑层来调用:

1 public interface ProductService
{
2  
3     void updateProductPrice(long productId, int price);
4 }

根据用户提出的需求,我想这个接口完全够用了。根据 productId 去更新对应 Product 的 price,然后再插入一条数据到 log 表中。

其实业务逻辑也不太复杂,于是我快速地完成了 ProductService 接口的实现类:

01 public class ProductServiceImpl implements ProductService
{
02  
03     private static final String
UPDATE_PRODUCT_SQL = 
"update
product set price = ? where id = ?"
;
04     private static final String
INSERT_LOG_SQL = 
"insert
into log (created, description) values (?, ?)"
;
05  
06     public void updateProductPrice(long productId, int price)
{
07         try {
08             //
获取连接
09             Connection
conn = DBUtil.getConnection();
10             conn.setAutoCommit(false); //
关闭自动提交事务(开启事务)
11  
12             //
执行操作
13             updateProduct(conn,
UPDATE_PRODUCT_SQL, productId, price); 
//
更新产品
14             insertLog(conn,
INSERT_LOG_SQL, 
"Create
product."
); //
插入日志
15  
16             //
提交事务
17             conn.commit();
18         catch (Exception
e) {
19             e.printStackTrace();
20         finally {
21             //
关闭连接
22             DBUtil.closeConnection();
23         }
24     }
25  
26     private void updateProduct(Connection
conn, String updateProductSQL, 
long productId, int productPrice) throws Exception
{
27         PreparedStatement
pstmt = conn.prepareStatement(updateProductSQL);
28         pstmt.setInt(1,
productPrice);
29         pstmt.setLong(2,
productId);
30         int rows
= pstmt.executeUpdate();
31         if (rows
!= 
0)
{
32             System.out.println("Update
product success!"
);
33         }
34     }
35  
36     private void insertLog(Connection
conn, String insertLogSQL, String logDescription) 
throws Exception
{
37         PreparedStatement
pstmt = conn.prepareStatement(insertLogSQL);
38         pstmt.setString(1new SimpleDateFormat("yyyy-MM-dd
HH:mm:ss SSS"
).format(new Date()));
39         pstmt.setString(2,
logDescription);
40         int rows
= pstmt.executeUpdate();
41         if (rows
!= 
0)
{
42             System.out.println("Insert
log success!"
);
43         }
44     }
45 }

代码的可读性还算不错吧?这里我用到了 JDBC 的高级特性 Transaction 了。暗自庆幸了一番之后,我想是不是有必要写一个客户端,来测试一下执行结果是不是我想要的呢? 于是我偷懒,直接在 ProductServiceImpl 中增加了一个 main() 方法:

1 public static void main(String[]
args) {
2     ProductService
productService = 
new ProductServiceImpl();
3     productService.updateProductPrice(13000);
4 }

我想让 productId 为 1 的产品的价格修改为 3000。于是我把程序跑了一遍,控制台输出:

Update product success!

Insert log success!

应该是对了。作为一名专业的程序员,为了万无一失,我一定要到数据库里在看看。没错!product 表对应的记录更新了,log 表也插入了一条记录。这样就可以将 ProductService 接口交付给别人来调用了。

几个小时过去了,QA 妹妹开始骂我:“我靠!我才模拟了 10 个请求,你这个接口怎么就挂了?说是数据库连接关闭了!”。

听到这样的叫声,让我浑身打颤,立马中断了我的小视频,赶紧打开 IDE,找到了这个 ProductServiceImpl 这个实现类。好像没有 Bug 吧?但我现在不敢给她任何回应,我确实有点怕她的。

我突然想起,她是用工具模拟的,也就是模拟多个线程了!那我自己也可以模拟啊,于是我写了一个线程类:

01 public class ClientThread extends Thread
{
02  
03     private ProductService
productService;
04  
05     public ClientThread(ProductService
productService) {
06         this.productService
= productService;
07     }
08  
09     @Override
10     public void run()
{
11         System.out.println(Thread.currentThread().getName());
12         productService.updateProductPrice(13000);
13     }
14 }

我用这线程去调用 ProduceService 的方法,看看是不是有问题。此时,我还要再修改一下 main() 方法:

01 //
public static void main(String[] args) {
02 //    
ProductService productService = new ProductServiceImpl();
03 //    
productService.updateProductPrice(1, 3000);
04 //
}
05      
06 public static void main(String[]
args) {
07     for (int i
0;
i < 
10;
i++) {
08         ProductService
productService = 
new ProductServiceImpl();
09         ClientThread
thread = 
new ClientThread(productService);
10         thread.start();
11     }
12 }

我也模拟 10 个线程吧,我就不信那个邪了!

运行结果真的让我很晕、很晕:

Thread-1

Thread-3

Thread-5

Thread-7

Thread-9

Thread-0

Thread-2

Thread-4

Thread-6

Thread-8

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.

at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)

at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)

at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)

at java.lang.reflect.Constructor.newInstance(Constructor.java:513)

at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)

at com.mysql.jdbc.Util.getInstance(Util.java:386)

at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1015)

at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:989)

at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:975)

at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:920)

at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1304)

at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1296)

at com.mysql.jdbc.ConnectionImpl.commit(ConnectionImpl.java:1699)

at com.smart.sample.test.transaction.solution1.ProductServiceImpl.updateProductPrice(ProductServiceImpl.java:25)

at com.smart.sample.test.transaction.ClientThread.run(ClientThread.java:18)

我靠!竟然在多线程的环境下报错了,果然是数据库连接关闭了。怎么回事呢?我陷入了沉思中。于是我 Copy 了一把那句报错信息,在百度、Google,还有 OSC 里都找了,解答实在是千奇百怪。

我突然想起,既然是跟 Connection 有关系,那我就将主要精力放在检查 Connection 相关的代码上吧。是不是 Connection 不应该是 static 的呢?我当初设计成 static 的主要是为了让 DBUtil 的 static 方法访问起来更加方便,用 static 变量来存放 Connection 也提高了性能啊。怎么搞呢?

于是我看到了 OSC 上非常火爆的一篇文章《ThreadLocal 那点事儿》,终于才让我明白了!原来要使每个线程都拥有自己的连接,而不是共享同一个连接,否则线程1有可能会关闭线程2的连接,所以线程2就报错了。一定是这样!

我赶紧将 DBUtil 给重构了:

01 public class DBUtil
{
02     //
数据库配置
03     private static final String
driver = 
"com.mysql.jdbc.Driver";
04     private static final String
url = 
"jdbc:mysql://localhost:3306/demo";
05     private static final String
username = 
"root";
06     private static final String
password = 
"root";
07  
08     //
定义一个用于放置数据库连接的局部线程变量(使每个线程都拥有自己的连接)
09     private static ThreadLocal<Connection>
connContainer = 
new ThreadLocal<Connection>();
10  
11     //
获取连接
12     public static Connection
getConnection() {
13         Connection
conn = connContainer.get();
14         try {
15             if (conn
== 
null)
{
16                 Class.forName(driver);
17                 conn
= DriverManager.getConnection(url, username, password);
18             }
19         catch (Exception
e) {
20             e.printStackTrace();
21         finally {
22             connContainer.set(conn);
23         }
24         return conn;
25     }
26  
27     //
关闭连接
28     public static void closeConnection()
{
29         Connection
conn = connContainer.get();
30         try {
31             if (conn
!= 
null)
{
32                 conn.close();
33             }
34         catch (Exception
e) {
35             e.printStackTrace();
36         finally {
37             connContainer.remove();
38         }
39     }
40 }

我把 Connection 放到了 ThreadLocal 中,这样每个线程之间就隔离了,不会相互干扰了。

此外,在 getConnection() 方法中,首先从 ThreadLocal 中(也就是 connContainer 中) 获取 Connection,如果没有,就通过 JDBC 来创建连接,最后再把创建好的连接放入这个 ThreadLocal 中。可以把 ThreadLocal
看做是一个容器,一点不假。

同样,我也对 closeConnection() 方法做了重构,先从容器中获取 Connection,拿到了就 close 掉,最后从容器中将其 remove 掉,以保持容器的清洁。

这下应该行了吧?我再次运行 main() 方法:

Thread-0

Thread-2

Thread-4

Thread-6

Thread-8

Thread-1

Thread-3

Thread-5

Thread-7

Thread-9

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

Update product success!

Insert log success!

我去!总算是解决了,QA 妹妹,你应该会对我微笑一下吧?

感谢您的关注,分享是一种快乐,也希望得到您的支持与批评!

关于ThreadLocal 的用法,布布扣,bubuko.com

时间: 2024-08-04 00:49:57

关于ThreadLocal 的用法的相关文章

Java_解密ThreadLocal

概述 相信读者在网上也看了很多关于ThreadLocal的资料,很多博客都这样说:ThreadLocal为解决多线程程序的并发问题提供了一种新的思路:ThreadLocal的目的是为了解决多线程访问资源时的共享问题.如果你也这样认为的,那现在给你10秒钟,清空之前对ThreadLocal的错误的认知! 看看JDK中的源码是怎么写的: This class provides thread-local variables. These variables differ from their norm

[Java并发包学习七]解密ThreadLocal

概述 相信读者在网上也看了很多关于ThreadLocal的资料,很多博客都这样说:ThreadLocal为解决多线程程序的并发问题提供了一种新的思路:ThreadLocal的目的是为了解决多线程访问资源时的共享问题.如果你也这样认为的,那现在给你10秒钟,清空之前对ThreadLocal的错误的认知! 看看JDK中的源码是怎么写的: This class provides thread-local variables. These variables differ from their norm

【Java】深入理解ThreadLocal

一.前言 要理解ThreadLocal,首先必须理解线程安全.线程可以看做是一个具有一定独立功能的处理过程,它是比进程更细度的单位.当程序以单线程运行的时候,我们不需要考虑线程安全.然而当一个进程中包含多个线程的时候,就需要考虑线程安全问题,因为此时线程可能会同时操作同一个资源,当两个或者两个以上线程同时操作一个资源的时候,就会造成冲突.不一致等问题,即线程不安全. 解决线程安全问题,本质上就是解决资源共享问题,一般有以下手段: 1)可重入(不依赖环境):2)互斥(同一时间段只允许一个线程使用)

【转载】解密ThreadLocal

转载自:http://qifuguang.me/2015/09/02/[Java%E5%B9%B6%E5%8F%91%E5%8C%85%E5%AD%A6%E4%B9%A0%E4%B8%83]%E8%A7%A3%E5%AF%86ThreadLocal/ 相信读者在网上也看了很多关于ThreadLocal的资料,很多博客都这样说:ThreadLocal为解决多线程程序的并发问题提供了一种新的思路:ThreadLocal的目的是为了解决多线程访问资源时的共享问题.如果你也这样认为的,那现在给你10秒钟

Java ThreadLocal

背景: 最近项目中需要调用其他业务系统的服务,使用的是Java的RMI机制,但是在调用过程中中间件发生了Token校验问题.而这个问题的根源是每次用户操作,没有去set Token导致的.这个Token是存储在ThreadLocal变量中的,根据servlet的单例多线程原理,使用一个拦截器每次向Thread中写入这个token完美的解决了这个问题. ThreadLocal ThreadLocal是Java lang包里面的一个类,这个类用来提供线程级变量的实现.想要明白ThreadLocal首

ThreadLocal使用和原理简析

1. 解决共享资源冲突 对于并发工作,需要某种方式来防止两个任务同时访问相同的资源,至少在关键阶段不能出现这种冲突情况. 方法之一就是当资源被一个任务使用时,在其上加锁.第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它,以此类推.Java中的synchronized.ReentrantLock就属于这种方式,关于这部分,前面有专门撰文详述: synchronized学习.synchronized底层实现学习 Ree

惊:FastThreadLocal吞吐量居然是ThreadLocal的3倍!!!

说明 接着上次手撕面试题ThreadLocal!!!面试官一听,哎呦不错哦!本文将继续上文的话题,来聊聊FastThreadLocal,目前关于FastThreadLocal的很多文章都有点老有点过时了(本文将澄清几个误区),很多文章关于FastThreadLocal介绍的也不全,希望本篇文章可以带你彻底理解FastThreadLocal!!! FastThreadLocal是Netty提供的,在池化内存分配等都有涉及到!? 关于FastThreadLocal,零度准备从这几个方面进行讲解: F

什么是 ThreadLocal?

什么是 ThreadLocal? ThreadLocal 诞生于 JDK 1.2,用于解决多线程间的数据隔离问题.也就是说 ThreadLocal 会为每一个线程创建一个单独的变量副本. ThreadLocal 有什么用? ThreadLocal 最典型的使用场景有两个: ThreadLocal 可以用来管理 Session,因为每个人的信息都是不一样的,所以就很适合用 ThreadLocal 来管理: 数据库连接,为每一个线程分配一个独立的资源,也适合用 ThreadLocal 来实现. 其中

面试常问问题:银行网上支付项目中怎么控制多线程高并发访问?

面试常问问题:银行网上支付项目中怎么控制多线程高并发访问? synchronized关键字主要解决多线程共享数据同步问题. ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题. ThreadLocal和Synchonized都用于解决多线程并发访问.但是ThreadLocal与synchronized有本质的区别: synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问.而ThreadLocal为每一个线程都提供了变量的副本,使 得每个线程在某一时