0
点赞
收藏
分享

微信扫一扫

【ThreadLocal】的使用案例和实现原理

菜菜捞捞 2022-04-02 阅读 75

ThreadLocal的使用案例和实现原理

(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();
        }
    }

}
举报

相关推荐

0 条评论