谈谈ThreadLocal和解决线程安全的关系

本文探讨了ThreadLocal与线程安全的关系,通过实例说明ThreadLocal并非为解决线程安全设计,而是用于在线程间隔离数据,避免共享数据引发的问题。
在[url=http://zhangbo-peipei-163-com.iteye.com/blog/2028533]这篇文章中[/url]我粗略的就我的理解谈了一下ThreadLocal。但是很多时候我们还是会认为ThreadLocal是为了解决线程安全的问题而设计的。这篇文章就我的理解再加上[url=http://zhangbo-peipei-163-com.iteye.com/blog/2028533]该文章[/url]
中很多朋友的回复阐述一下ThreadLocal和线程安全的关系。

首先我们来看一下线程安全问题产生的两个前提条件:
1. 数据共享。多个线程访问同样的数据。
2. 共享数据是可变的。多个线程对访问的共享数据作出了修改。

定义一个共享数据:

public static int a = 0;


多线程多该共享数据进行修改:

private static void plus() {
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
a++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("plus:" + Thread.currentThread().getName() + ": " + a);
}
}.start();
}
}

输出结果:

plus:Thread-5: 5
plus:Thread-2: 5
plus:Thread-6: 5
plus:Thread-9: 5
plus:Thread-1: 6
plus:Thread-0: 8
plus:Thread-4: 8
plus:Thread-3: 10
plus:Thread-7: 10
plus:Thread-8: 10

很明显,在第一次输出a的值的时候,a的值就已经被其他线程修改到5了,显然线程不安全。

利用synchronized关键字将修改a值的地方和输出的地方上锁。让这段代码在某一个时间段内始终只有一个
线程在执行:

private static void plus() {
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
synchronized (JavaVariable.class) {
a++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("plus:" + Thread.currentThread().getName() + ": " + a);
}
}
}.start();
}
}

输出结果:

plus:Thread-2: 1
plus:Thread-6: 2
plus:Thread-7: 3
plus:Thread-3: 4
plus:Thread-8: 5
plus:Thread-4: 6
plus:Thread-0: 7
plus:Thread-9: 8
plus:Thread-5: 9
plus:Thread-1: 10

结果正确。

那么,如果ThreadLocal是为了解决线程安全设计的,请同样用ThreadLocal解决上面的问题。即,我需要多个线程共同修改一个值,但是要保持这个值递增,就是说后面的线程不能在前一个线程还没有输出就又去修改。

当然可以尝试一下,首先定义一个ThreadLocal。

private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
}
};

插一句题外话:为什么threadLocal要被声明成静态的?不声明成静态的行不行?
我只是有一点儿想法,还不是很明白,所以暂且先不讨论这个话题,大家也可以思考一下,有什么高见感谢回复到文章后面。
接着上面的话题,也许可以这样:

private static void plus() throws Exception {
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
//1
a = threadLocal.get();
a++;

//2
threadLocal.set(a);
System.out.println("plus:" + Thread.currentThread().getName() + ": " + threadLocal.get());
}
}.start();
}
}

我都不想运行结果了,全部会输出1。很明显,在代码“1”的时候,每一个线程都会将threadLocal的初始值0赋值给共享变量a,因为每一个线程从threadLocal.get()拿到的值都是自己threadLocal保存的。所以,就没有所以了。
所以对于共享变量a来讲,每个线程都会首先将自己threadLocal里面的初始值0赋值给a,然后将共享变量a+1,然后将a+1的值设置到自己的ThreadLocalMap中,其他线程就访问不到了。下一个线程来的时候又会将自己threadLocal里面的初始值0赋值给a,然后将 a+1,然后... 如此周而复始。a只是被在0和1之间改来改去,最终放到每一个线程的threadLocal里面的a+1的值就不再共享。对于a这个共享变量来讲,如果在for循环创建的某线程A即将执行代码“2”之前,被其他for循环以外的某个线程改了一把。那么存到线程A的ThreadLocalMap中的值就是被改过的值了。

综上所述:
Java的ThreadLocal不是设计用来解决多线程安全问题的,事实证明也解决不了,共享变量a还是会被随意更改。ThreadLocal无能为力。所以,一般用ThreadLocal都不会将一个共享变量放到线程的ThreadLocal中。一般来讲,存放到ThreadLocal中的变量都是当前线程
本身就独一无二的一个变量。其他线程本身就不能访问,存到ThreadLocal中只是为了方便在程序中同一个线程之间传递这个变量。
[b]所以,ThreadLocal和解决线程安全没有关系。[/b]
例如Hibernate的代码:

public static final ThreadLocal<session> sessions =
new ThreadLocal<session>();
public static Session currentSession() throws HibernateException {
Session s = sessions.get();
//如果从当前线程变量中获取的session为空,直接open一个session放到当前线程变量中。
//值得注意的是,这里的session引用s是在方法体内声明的,属于局部变量,其他线程根本访问不到。
//就是说,这里的session s本身就不是一个共享对象,本身就是当前线程独一无二的存在。
//下一个线程来的时候,从sessions.get()拿出来的session s又是下一个线程的线程变量中的值。如果为空,同样创建。
//所以,session在线程变量内部还是外部都是没有和任何线程共享的。
if(s == null) {
s = sessionFactory.openSession();
sessions.set(s);
}



补充一个问题:
当使用线程池的时候,由于ThreadLocal的设计原理是将一个ThreadLocalMap的引用作为Thread的一个属性,利用当前ThreadLocal作为key,保存的变量值作为value保存在当前线程的ThreadLocalMap中的。所以ThreadLocalMap是伴随的Thread本身的存在而存在的,只要Thread不被回收,ThreadLocalMap就存在。因此,对于线程池来讲,重复利用一个Thread就等于在重复利用Thread的ThreadLocalMap,所以ThreadLocalMap里面保存的数据可能会被多次使用。
例子:

private static void plus() {
Executor executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
executor.execute(new Runnable() {
public void run() {
a = threadLocal.get();
threadLocal.set(++a);
System.out.println("plus:" + Thread.currentThread().getName() + ": " + a);

}
});
}
}

输出结果:

plus:pool-1-thread-1: 1
plus:pool-1-thread-1: 2
plus:pool-1-thread-1: 3
plus:pool-1-thread-1: 4
plus:pool-1-thread-1: 5
plus:pool-1-thread-1: 6
plus:pool-1-thread-1: 7
plus:pool-1-thread-1: 8
plus:pool-1-thread-1: 9
plus:pool-1-thread-2: 1

可以看出,对于线程池中的两个线程来讲,线程1一直在被重复使用,虽然每次都是从ThreadLocal获取值,但是都是同一个ThreadLocalMap,所以值是递增的。对于线程2来讲,和线程1不是一个线程,不是一个ThreadLocalMap,所以值依然是1。

线程重复利用,又不想重复利用其ThreadLocalMap中的值怎么办呢?
我的理解是:
一般情况下,我们自己用的话ThreadLocal里面都存放的是无状态的对象,只是便于在同一个线程中参数传递,所以个人认为即使重复利用也没有关系。但是如果ThreadLocal里面存放的是有状态的对象的话,在线程使用结束后直接将当前线程的ThreadLocalMap里的值设为初始值就可以了,或者直接remove掉。
比如 Struts2里面的ActionContext:

public class ActionContext implements Serializable {
static ThreadLocal actionContext = new ThreadLocal();
....
public static void setContext(ActionContext context) {
actionContext.set(context);
}
}

在struts2的org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter类中,就会无论如何在请求结束后清除当前线程的ThreadLocalMap中的值:

} finally {
prepare.cleanupRequest(request);
}


public void cleanupRequest(HttpServletRequest request) {
ActionContext.setContext(null);
}
我是7年经验的程序员,以下面试题请给我答案,要求全面有深度: 1.线程池核心参数有哪些?线程池工作流程?有哪几种线程池?说明不同线程池类型的使用场景及其优缺点? 2.synchronizedReentrantLock有什么区别?在实际项目中,您如何选择使用它们? 4.什么是死锁?如何预防解决死锁? 5.阻塞队列有哪些?分别介绍一下 6.在并发编程中,CountDownLatch、CyclicBarrierSemaphore有何异同?请举例说明它们各自的使用场景。 7.如何通过使用volatile关键字解决Java中的可见性问题?volatile与原子操作有什么关系? 8.ConcurrentHashMap在Java1.8中相比之前的版本有哪些重要改进? 9.ConcurrentHashMap是线程安全的吗?在哪种情况下可能会出现数据一致性问题?您会如何防止这种情况发生? 10 在处理并发数据访问时,您是如何考虑选择乐观锁还是悲观锁的?为什么? 11.HashMap存储数据的过程 12.介绍一下ThreadLocalThreadLocal内存泄漏问题 13.谈谈你是如何理解线程安全的?有哪些实现线程安全的方案? 14.并发并行的区别? 15.synchronized的实现原理?它是如何保证原子性、可见性及有序性?锁升级的过程是怎么样的?synchronized是非公平锁吗,是如何体现非公平的? 16.如何理解CAS?存在什么问题?CAS一定要自旋吗? 17.介绍一下AQS?AQS中的同步队列条件队列原理?什么是AQS的独占模式共享模式?AQS为什么采用双向链表?
最新发布
10-21
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值