ThreadLocal的使用案例和实现原理
- (1)ThreadLocal简单介绍
- (2)ThreadLocal的基本使用
- (3)ThreadLocal的实现原理
- (4)理解ThreadLocal中的内存泄漏问题
- (5)ThreadLocalMap中的Hash冲突处理
- (6)可以被继承的ThreadLocal——InheritableThreadLocal
- (7)在Spring中实例中哪里用到了ThreadLocal?
- (8)案例一:每个线程内独享的对象,一般使用场景是工具类中
- (9)案例二:当前用户信息需要被线程内所有方法共享
- (10)案例三:使用ThreadLocal存放JDBC的连接
(1)ThreadLocal简单介绍
(1)为什么会有ThreadLocal
在处理多线程并发安全的方法中,会使用锁synchronized/Lock等来控制多个不同线程对临界区数据的访问,但是使用锁就会出现并发冲突导致出现串行,会降低并发性能。可以思考如何彻底避免这种竞争。
ThreadLocal线程本地存储就可以实现彻底避免竞争,可以看做是线程的局部变量,只有当前线程自身可以访问,别的线程都访问不了,防止自己的变量被其它线程篡改,从而实现一种与众不同的线程安全。
(2)未使用ThreadLocal时是什么样
使用线程池给一个变量赋值,然后输出每个线程获取的变量值,看看是不是线程自身赋值的那一个。
public class TestThreadLocal01 {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(200);
for (int i=0;i<5;i++) {
es.submit(new Runnable() {
@Override
public void run() {
//没有使用ThreadLocal设置的变量
threadLocal01.setContent(Thread.currentThread().getName()+"的数据");
System.out.println("---------------");
System.out.println(Thread.currentThread().getName()+"--->"+threadLocal01.getContent());
}
}
});
}
}
}
这个时候查看输出的结果,发现为:pool-1-thread-3—>pool-1-thread-5的数据,线程3给变量赋值以后,取出来的却是线程5设定的值。
(3)使用ThreadLocal存放线程设定的值
public class TestThreadLocal01 {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
TestThreadLocal01 threadLocal01 = new TestThreadLocal01();
ExecutorService es = Executors.newFixedThreadPool(200);
for (int i=0;i<5;i++) {
es.submit(new Runnable() {
@Override
public void run() {
//使用ThreadLocal设置的变量
threadLocal01.setContentTL(Thread.currentThread().getName()+"的ThreadLocal数据");
System.out.println("---------------");
System.out.println(Thread.currentThread().getName()+"--->"+threadLocal01.getContentTL());
}
}
});
}
}
}
输出结果发现:pool-1-thread-1—>pool-1-thread-1的ThreadLocal数据,线程设定的值存放在自身的ThreadLocal里,然后取出来,可以保证一致。
(4)使用synchronized锁也可以实现上述的效果,但是会降低性能
public class TestThreadLocal01 {
private String contentSynchronized;
public String getContentSynchronized() {
return contentSynchronized;
}
public void setContentSynchronized(String contentSynchronized) {
this.contentSynchronized = contentSynchronized;
}
public static void main(String[] args) {
TestThreadLocal01 threadLocal01 = new TestThreadLocal01();
ExecutorService es = Executors.newFixedThreadPool(200);
for (int i=0;i<5;i++) {
es.submit(new Runnable() {
@Override
public void run() {
//使用sync锁实现绑定(可以实现,但是加锁以后只能排队访问,降低了并发性)
synchronized (this.getClass()) {
threadLocal01.setContentSynchronized(Thread.currentThread().getName()+"的synchronized数据");
System.out.println("---------------");
System.out.println(Thread.currentThread().getName()+"--->"+threadLocal01.getContentSynchronized());
}
}
});
}
}
}
输出结果为:pool-1-thread-1—>pool-1-thread-1的synchronized数据
(5)ThreadLocal和synchronized的区别
(6)ThreadLocal的好处
1-让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
2-在任何方法中都可以轻松获取到该对象
3-达到线程安全
4-不需要加锁,提高执行效率
5-更高效地利用内存节省开销,上面例子中,相比于成千上万个任务,每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销。
6-免去传参的繁琐,不需要每次都传同样的参数,ThreadLocal使得代码耦合度更低,更优雅
(2)ThreadLocal的基本使用
(1)创建ThreadLocal对象
创建一个localInt变量,由于ThreadLocal是一个泛型类,这里指定了localInt的类型为整数。
private ThreadLocal<Integer> localInt = new ThreadLocal<>();
(2)设定ThreadLocal的值set()和获取值get()
ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();
(3)批量初始化赋值initialValue
每个线程只能自己赋值,不能通过其他线程初始化值,ThreadLocal提供了withInitial()方法统一初始化所有线程的ThreadLocal的值。
private ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 6);
上述代码将ThreadLocal的初始值设置为6,这对全体线程都是可见的。
(3)ThreadLocal的实现原理
(1)Thread、ThreadLocal、ThreadLocalMap三者的关系
每个Thread对象都有一个ThreadLocalMap,每个ThreadLocalMap可以存储多个ThreadLocal
(2)get方法的源码
首先获取当前线程,然后获取线程自己的ThreadLocalMap,然后获取map里节点的值value并返回。
public T get() {
//获得当前线程
Thread t = Thread.currentThread();
//每个线程 都有一个自己的ThreadLocalMap,
//ThreadLocalMap里就保存着所有的ThreadLocal变量
ThreadLocalMap map = getMap(t);
if (map != null) {
//ThreadLocalMap的key就是当前ThreadLocal对象实例,
//多个ThreadLocal变量都是放在这个map中的
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//从map里取出来的值就是我们需要的这个ThreadLocal变量
T result = (T)e.value;
return result;
}
}
// 如果map没有初始化,那么在这里初始化一下
return setInitialValue();
}
说明:get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
注意:这个map以及map中的key和value都是保存在线程中ThreadLocalMap的,而不是保存在ThreadLocal中
getMap方法:获取到当前线程内的ThreadLocalMap对象
每个线程内都有ThreadLocalMap对象,名为threadLocals,初始值为null
(3)set方法的源码
// 把当前线程需要全局共享的value传入
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// map对象为空就创建,不为空就覆盖
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
(4)initialValue方法
这个方法没有默认实现,如果要用initialValue方法,需要自己实现,通常使用匿名内部类的方式实现
(5)remove方法
// 删除对应这个线程的值
public void remove() {
// 获取当前线程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 移除这个ThreadLocal对应的值
m.remove(this);
}
(6)ThreadLocalMap类的Entry的源码
所谓的ThreadLocal变量就是保存在每个线程的map中的。这个map就是Thread对象中的threadLocals字段。ThreadLocalMap类,也就是Thread.threadLocals。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 类是每个线程Thread类里面的变量,但ThreadLocalMap这个静态内部类定义在ThreadLocal类中,其中发现这一行代码
private Entry[] table;
里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对:
- 键:这个ThreadLocal
- 值:实际需要的成员变量,比如User或者SimpleDateFormat对象
ThreadLocal.ThreadLocalMap是一个比较特殊的Map,它的每个Entry的key都是一个弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//key就是一个弱引用
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
这样设计的好处是,如果这个变量不再被其他对象使用时,可以自动回收这个ThreadLocal对象,避免可能的内存泄露(注意,Entry中的value,依然是强引用)。
(4)理解ThreadLocal中的内存泄漏问题
(1)什么是内存泄漏
某个对象不再有用,但是占用的内存却不能被回收。
(2)用到的相关引用
1)ThreadLocalMap中的Entry继承自 WeakReference,是弱引用
2)弱引用:通过WeakReference类实现的,在GC的时候,不管内存空间足不足都会回收这个对象,适用于内存敏感的缓存,ThreadLocal中的key就用到了弱引用,有利于内存回收。
3)强引用:我们平日里面的用到的new了一个对象就是强引用,例如 Object obj = new Object();当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象。
(3)泄漏的原因分析
虽然ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry中的value依然是强引用。这个value的引用链条如下:
可以看到,只有当Thread被回收时,这个value才有被回收的机会,否则,只要线程不退出,value总是会存在一个强引用。但是,要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。处理的方法是,在ThreadLocalMap进行set(),get(),remove()的时候,都会进行清理:
以getEntry()为例:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
//如果找到key,直接返回
return e;
else
//如果找不到,就会尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来
return getEntryAfterMiss(key, i, e);
}
下面是getEntryAfterMiss()的实现:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
// 整个e是entry ,也就是一个弱引用
ThreadLocal<?> k = e.get();
//如果找到了,就返回
if (k == key)
return e;
if (k == null)
//如果key为null,说明弱引用已经被回收了
//那么就要在这里回收里面的value了
expungeStaleEntry(i);
else
//如果key不是要找的那个,那说明有hash冲突,这里是处理冲突,找下一个entry
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
真正用来回收value的是expungeStaleEntry()方法,在remove()和set()方法中,都会直接或者间接调用到这个方法进行value的清理:
从这里可以看到,ThreadLocal为了避免内存泄露,也算是花了一番大心思。不仅使用了弱引用维护key,还会在每个操作上检查key是否被回收,进而再回收value。
但是从中也可以看到,ThreadLocal并不能100%保证不发生内存泄漏。
正常情况下,当线程终止,保存在ThreadLocalMap里的value会被垃圾回收,因为没有任何强引用了。但如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:Thread---->ThreadLocalMap---->Entry(key为null,弱引用被回收)---->value。
比如,很不幸的,你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用set()和remove(),那么这个内存泄漏依然会发生。
JDK已经考虑到了这个问题,所以在set, remove, rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收。如果key回收了,那么value也设置为null,断开强引用链路,便于垃圾回收。因此,一个良好的习惯依然是:当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的。
但是如果一个ThreadLocal不被使用,那么实际上set, remove, rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏
(4)ThreadLocal如何避免内存泄漏?
及时调用remove方法,就会删除对应的Entry对象,可以避免内存remove泄漏,所以使用完ThreadLocal之后,应该调用remove方法。
比如拦截器获取到用户信息,用户信息存在ThreadLocalMap中,线程请求结束之前拦住它,并用remove清除User对象,这样就能稳妥的保证不会内存泄漏。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("张三");
……
} finally {
localName.remove();
}
(5)共享对象问题
如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。
(6)不要强行使用ThreadLocal
如果可以不使用ThreadLocal就能解决问题,那么不要强行使用,在任务数很少的时候,可以通过在局部变量中新建对象解决。
(7)优先使用框架的支持,而不是自己创造
在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏。
(5)ThreadLocalMap中的Hash冲突处理
(1)ThreadLocalMap处理Hash冲突的方式
ThreadLocalMap作为一个HashMap和java.util.HashMap的实现是不同的。对于java.util.HashMap使用的是链表法来处理冲突:
但是,对于ThreadLocalMap,它使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放:
整个set()的过程如下:
处理冲突方式不同,HashMap采用链地址法,而ThreadLocalMap采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链。通过源码分析可以看出,setInitialValue和直接set最后都是利用map.set()方法来设置值,最后都会对应到ThreadLocalMap的一个Entry。
(2)为什么需要数组呢?没有了链表怎么解决Hash冲突呢?
用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。
(3)是不是说ThreadLocal的实例以及其值存放在栈上呢?
其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
(6)可以被继承的ThreadLocal——InheritableThreadLocal
在实际开发过程中,我们可能会遇到这么一种场景。主线程开了一个子线程,但是我们希望在子线程中可以访问主线程中的ThreadLocal对象,也就是说有些数据需要进行父子线程间的传递。比如像这样:
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
IntStream.range(0,10).forEach(i -> {
//每个线程的序列号,希望在子线程中能够拿到
threadLocal.set(i);
//这里来了一个子线程,我们希望可以访问上面的threadLocal
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
执行上述代码,你会看到:
因为在子线程中,是没有threadLocal的。如果我们希望子线可以看到父线程的ThreadLocal,那么就可以使用InheritableThreadLocal。顾名思义,这就是一个支持线程间父子继承的ThreadLocal,将上述代码中的threadLocal使用InheritableThreadLocal:
InheritableThreadLocal threadLocal = new InheritableThreadLocal();
再执行,就能看到:
可以看到,每个线程都可以访问到从父进程传递过来的一个数据。虽然InheritableThreadLocal看起来挺方便的,但是依然要注意以下几点:
(1)变量的传递是发生在线程创建的时候,如果不是新建线程,而是用了线程池里的线程,就不灵了
(2)变量的赋值就是从主线程的map复制到子线程,它们的value是同一个对象,如果这个对象本身不是线程安全的,那么就会有线程安全问题
(2)再看一个案例
如果我想共享线程的ThreadLocal数据怎么办?
private void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("帅得一匹");
Thread t = new Thread() {
@Override
public void run() {
super.run();
Log.i( "张三帅么 =" + threadLocal.get());
}
};
t.start();
}
(7)在Spring中实例中哪里用到了ThreadLocal?
- DateTimeContextHolder类,应用了ThreadLocal
- ThreadLocal的典型应用场景:每次HTTP请求都对应一个线程,线程之间相互隔离
- 看RequestContextHolder,也是用到了ThreadLocal,看NamedThreadLocal源码,再看getRequestAttributes的调用
(8)案例一:每个线程内独享的对象,一般使用场景是工具类中
每个线程需要一个独享的对象(通常是工具类,典工具类型需要使用的类有SimpleDateFormat和Random)
先创建静态方法date对时间进行格式化,然后多线程中每个线程都想对当前的时间进行格式化并且输出。
public class ThreadLocalTest {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) { // 新建了1000个SimpleDateFormat对象
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public static String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return dateFormat.format(date);
}
}
【发现问题】
每个线程在执行过程中,每调用以此date方法,都要创建一个SimpleDateFormat对象,这比较耗费内存资源。
【改进一:静态方法】
如上述代码,把date方法改成静态方法,这样每个线程都调用的是同一个date方法,共用一个SimpleDateFormat对象,减少内存消耗。但是这样会打印出相同的时间,所有线程都在争夺这个资源,我们需要一个锁去控制,避免出现线程安全问题。
【改进二:同步锁】
通过加锁,保证每个线程调用date方法的时候,获取的都是自己当前的时间。但是在高并发场景下,所有线程需要一个个的去获取锁,需要排队等待,这显然性能损耗太大。
public class ThreadLocalTest {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public static String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
String s = null;
synchronized (ThreadLocalTest.class) {
s = dateFormat.format(date);
}
return s;
}
}
【改进三:线程本地存储】
使用ThreadLocal(不仅线程安全,而且也没有synchronized带来的性能问题,每个线程内有自己独享的SimpleDateFormat对象)。但是要注意内存泄漏的问题。
// 利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
public class ThreadLocalTest {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public static String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); // 拿到initialValue返回对象
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
// lambda表达式写法,和上面写法效果完全一样
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
(9)案例二:当前用户信息需要被线程内所有方法共享
我在项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
使用到类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我使用到了ThreadLocal去做了一下改造,这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了。
很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的。
before
void work(User user) {
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}
then
void work(User user) {
try{
threadLocalUser.set(user);
// 他们内部 User u = threadLocalUser.get(); 就好了
getInfo();
checkInfo();
setSomeThing();
log();
} finally {
threadLocalUser.remove();
}
}
当一个请求进来了,一个线程负责处理该请求,该请求会依次调用service-1(), service-2(), service-3(), service-4(),同时,每个service()都需要获得调用方用户user的信息,也就是需要拿到user对象。
一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service-2(),再从service-2()传到service-3(),以此类推,但是这样做会导致代码冗余且不易维护。
在此基础上可以演进,使用UserMap,就是每个用户的信息都存在一个Map中,当多线程同时工作时,我们需要保证线程安全,可以用synchronized也可以用ConcurrentHashMap,但这两者无论用什么,都会对性能有所影响。
public class ThreadLocalTest {
public static void main(String[] args) {
new Service1().process("");
}
}
class Service1 {
public void process(String name) {
User user = new User("张三");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
ThreadSafeFormatter.dateFormatThreadLocal.get();
System.out.println("Service2拿到用户名:" + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + user.name);
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>(); // 对比上一个例子,这里没有重写initialValue方法
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
运行的结果是:
Service2拿到用户名:张三
Service3拿到用户名:张三
这样,不管哪个Service都能拿到User对象,能获取User对象内的所有信息。并且假如有多个请求,一个张三,一个李四,因为他们并没有直接共享User对象,所以他们之间不会有线程安全问题。
使用ThreadLocal后无需synchronized,可以在不影响性能的情况下,也无需层层传递参数,就可以达到保存当前线程对应的用户信息的目的。
(10)案例三:使用ThreadLocal存放JDBC的连接
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring的事务主要是ThreadLocal和AOP去做实现的。
写一个数据库连接的工具类
@Component("connectionUtils")//这个类的对象放入IOC容器
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
@Autowired
private DataSource dataSource;
/**
* 获取当前线程上的连接
*
* @MethodName: getThreadConnection
* @Author: AllenSun
* @Date: 2019/8/28 23:23
*/
public Connection getThreadConnection() {
//1-先从ThreadLocal上获取连接
Connection conn = tl.get();
//2-判断当前线程上是否有连接
try {
if (conn == null) {
//3-从数据源中获取一个连接,并且存入ThreadLocal中,绑定线程
conn = dataSource.getConnection();
tl.set(conn);
}
//4-返回当前线程上的连接
return conn;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 线程和连接用完以后进行解绑
*
* @MethodName: removeConnection
* @Author: AllenSun
* @Date: 2019/8/28 23:40
*/
public void removeConnection() {
tl.remove();
}
}
@Component("txManager")//放入容器
@Aspect//说明这个类是一个切面类,同时加上一个
public class TransactionManager {
//引入连接线程的工具类
@Autowired
private ConnectionUtils connectionUtils;
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")//切入点表达式
private void pt1() {
}
/**
* 开启事务
*
* @MethodName: beginTransaction
* @Author: AllenSun
* @Date: 2019/8/28 23:31
*/
//存在弊端:beginTransaction方法名如果发生了改变,则所有引用的方法里都需要改,维护性特别差
//解决方法:使用动态代理来降低耦合,添加周边功能,例如“添加事务管理”
// @Before("pt1()")
public void beginTransaction() {
try {
//1-手动关闭自动提交
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 提交事务
*
* @MethodName: commit
* @Author: AllenSun
* @Date: 2019/8/28 23:32
*/
// @AfterReturning("pt1()")
public void commit() {
try {
connectionUtils.getThreadConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 回滚事务
*
* @MethodName: rollback
* @Author: AllenSun
* @Date: 2019/8/28 23:33
*/
// @AfterThrowing("pt1()")
public void rollback() {
try {
connectionUtils.getThreadConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 释放连接
*
* @MethodName: release
* @Author: AllenSun
* @Date: 2019/8/28 23:33
*/
// @After("pt1()")
public void release() {
try {
//1-把线程换回线程池
connectionUtils.getThreadConnection().close();
//2-线程和连接进行解绑
connectionUtils.removeConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 问题:
* 使用注解AOP时会出现问题,无法进入提交Aotucommit=true
* 分析:
* 注解AOP的执行顺序是有问题的,会先进行最终通知(释放连接资源),再进行后置通知(提交数据连接)
* 在最终通知时,释放连接资源,此时已经没有连接了
* 接着调用后置通知,此时没有连接,它就会从数据源里再拿一个,并且把线程绑上去,
* 虽然绑上去了,但是前置通知已经结束了,aotucommit已经为true,不能再次开启事务了
* 所以,此时再调用后置通知提交业务就不成功了
*
* 打断点来分析:
* 在开启事务之后,根本没有进入commit,而是直接来到最终通知释放资源
* 然后再次进入开启事务,获得一个新的连接,再进入commit,但是其中是null,因为proceed执行方法后的连接已经被清除了
* 新的连接里面没有任何执行操作,提交了也没用了
* 解决:
* 使用环绕通知
*
* 所以必须使用环绕通知才行
*
* @MethodName: arroundAdvice
* @Author: AllenSun
* @Date: 2019/8/31 15:18
*/
@Around("pt1()")
public Object arroundAdvice(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
//1-获取参数
Object args = pjp.getArgs();
//2-(一)前置通知:开启事务
this.beginTransaction();
//3-执行方法
rtValue = pjp.proceed((Object[]) args);
//4-(二)后置通知:提交事务
this.commit();
//7-返回值
return rtValue;
//exception是控制不了proceed的,所以要改成Throwable
} catch (Throwable e) {
//5-(三)异常通知:回滚事务
this.rollback();
throw new RuntimeException(e);
} finally {
//6-(四)最终通知:释放资源
this.release();
}
}
}