虚拟主机销售网站,少女前线9a高性能芯片,seo薪资,网站需求列表目录 1.ThreadLocal介绍2.ThreadLocal源码解析2.1 常用方法2.2 结构设计2.3 类图2.4 源码分析2.4.1 set方法分析2.4.2 get方法分析2.4.3 remove方法分析 3.ThreadLocal内存泄漏分析3.1 相关概念3.1.1 内存溢出3.1.2 内存泄漏3.1.3 强引用3.1.4 弱引用 3.2 内存泄漏是否和key使用… 目录 1.ThreadLocal介绍2.ThreadLocal源码解析2.1 常用方法2.2 结构设计2.3 类图2.4 源码分析2.4.1 set方法分析2.4.2 get方法分析2.4.3 remove方法分析 3.ThreadLocal内存泄漏分析3.1 相关概念3.1.1 内存溢出3.1.2 内存泄漏3.1.3 强引用3.1.4 弱引用 3.2 内存泄漏是否和key使用的弱引用有关3.2.1 假设key使用强引用3.2.2 假设key使用弱引用3.2.3 内存泄漏的真实原因3.2.4 ThreadLocalMap的key使用弱引用的原因 4.ThreadLocal使用场景 1.ThreadLocal介绍
ThreadLocal 是Java JDK中提供的一个类用于提供线程内部的局部变量这种变量在多线程下的环境下去访问时能保证各个线程的变量独立于其他线程的变量。也就是说使用ThreadLocal 可以提供线程内部的局部变量通过ThreadLocal的set() 和 get() 方法不同的线程之间不会互相干扰这种变量在线程的生命周期内起作用可以减少同一个线程内多个函数或者组件之间一些公共变量传递的复杂度。听起来好像挺复杂的下面我们使用一个简单的案例来解释一下ThreadLocal的作用。 案例说明 演示的代码将会使用一个TestData类表示存放在线程里面的数据然后开启10个线程在每个线程中设置数据后紧接着获取数据并且使用Thread.currentThread().getName()标识对应的线程。然后为每个线程设置名称方便我们观察线程的数据情况。 在不使用ThreadLocal和加锁的情况下
public class ThreadLocalDemo {public static void main(String[] args) {TestData testData new TestData();for (int i 0; i 10; i) {Thread thread new Thread(new Runnable() {Overridepublic void run() {testData.setData(数据XXX,当前线程是 Thread.currentThread().getName());System.out.println(当前线程是 Thread.currentThread().getName() ,存放的数据是 testData.getData());}});thread.setName(线程 i);thread.start();}}static class TestData {private String data;public void setData(String data) {this.data data;}public String getData() {return data;}}
}运行上面的代码结果如下 如上图所示:我们发现有的线程拿到的数据是其他线程的也就是各个线程之间的数据错乱了这种情况是一种错误因为线程之间的数据发生了相互干扰的情况。比如上图中选中的部分线程1存放的数据被线程4拿到了。正确的情况应该是线程1存放的数据应该也是由线程1取。即各个线程之间不应该相互干扰
解决上面线程间错误的问题有两种方法一是加锁二是使用ThreadLocal,接下来看加锁的方案代码如下 public static void main(String[] args) {TestData testData new TestData();for (int i 0; i 10; i) {Thread thread new Thread(new Runnable() {Overridepublic void run() {// 加锁解决线程间数据错乱的问题synchronized (ThreadLocalDemo.class){testData.setData(数据XXX,当前线程是 Thread.currentThread().getName());System.out.println(当前线程是 Thread.currentThread().getName() ,存放的数据是 testData.getData());}}});thread.setName(线程 i);thread.start();}}在每个线程的run方法中添加一个synchronized锁在多个线程的情况下限制每次只能有一个线程存取数据这样就能解决线程间数据干扰的问题运行结果如下 如上图所示使用加锁的方案后线程存数据和取数据的线程都是同一个了不会出现线程1的数据被线程2取到了 然后我们再看下使用ThreadLocal的方式解决线程间数据相互干扰的问题代码如下 static class TestData {private ThreadLocalString tl new ThreadLocal();public void setData(String data) {tl.set(data);}public String getData() {return tl.get();}}运行结果如下
如上图所示存储数据时使用TreadLocal的set方法取数据时使用ThreadLocal的get()方法这样也能解决线程间数据相互干扰的问题具体原理会在后面源码分析部分解析
看完上面的例子可能会有小伙伴心中有疑问既然加锁可以解决线程间数据相互干扰的问题那么为啥还需要设计出一个ThreadLocal呢其实这得联系synchronized和ThreadLocal的区别synchronized是一种同步机制采用以“时间换空间”的方式只是提供一份数据让不同的线程排队使用它的侧重点在于多个线程之间同步访问资源。而ThreadLocal则是以“空间换时间”为每一个线程都提供了一份数据的副本从而实现同时访问而互不干扰它侧重于多线程中让每个线程之间的数据的相互隔离。在上面的例子中我们强调的是线程数据隔离的问题使用synchronized不仅消耗性能(加锁会使程序的性能降低)而且加锁更加使用于数据共享的场景用在此处并不合适,使用TreadLocal可以使程序获得更高的并发性。
通过上面的例子相信读者已经可以简单的理解ThreadLocal是啥以及它的作用了接下来我们将从源码分析ThreadLocal,一点点解开其背后的神秘面纱。
2.ThreadLocal源码解析
2.1 常用方法
方法描述ThreadLocal()构造方法创建ThreadLocal对象public void set(T value)设置当前线程绑定的数据public T get()获取当前线程绑定的数据public void remove()移除当前线程绑定的数据
2.2 结构设计
这里我们说的ThreadLocal都是JDK 1.8之后的在JDK1.8中每个Thread维护一个ThreadLocalMap哈希表这个Hash表的Key是ThreadLocal本身value是要存储的数据Object具体的过程如下 1.每个Thread都有一个Map名为ThreadLocalMap 2.ThreadLocalMap里面存储了ThreadLocal对象(Key)和线程的数据副本(Value) 3.Thread内部的ThreadLocalMap是由ThreadLocal维护的由ThreadLocal负责向map获取和设置线程的数据 4.对于不同的线程每次获取副本数据时别的线程并不能获取当前线程的副本数据实现了副本数据的隔离。 ThreadLocal的结构如下所示 2.3 类图 ThreadLocalMap 是ThreadLocal的内部类没有实现Map接口而是使用独立的方式实现了Map的功能其内部的Entry也是独立实现的。并且继承自弱引用的接口 2.4 源码分析
下面先解释下ThreadLocal中会用到的存储结构Entry类代码如下所示 static class Entry extends WeakReferenceThreadLocal? {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal? k, Object v) {super(k);value v;}}从Entry的代码中我们可以得知Entry继承自WeakReference,并且使用ThreadLocal作为key.并且这个key只能是ThreadLocal对象. 补充在ThreadLocal中会自定义一个ThreadLocalMap以Key、Value的方式保存值类似于HashMap。我们都知道HashMap会有Hash冲突而解决HashMap冲突的方法是链地址法,而ThreadLocalMap解决冲突的方法是线性探测法该方法一次探测一个地址直到有空地址可插入若是整个空间都找不到空余的地址则产生溢出。比如假设当前数组的长度为16如果计算出来的索引为14而数组中位置为14的地方已经有值了并且这个值的key和当前待插入数据的key不一样那么此时就发生了哈希冲突。线性探测法就是说这时候可以通过一个线性的函数将当前的位置作为输入经过线性函数运算后得到一个输出比如我们确定这个线性函数为y x 1,y为计算后的索引值x为输入的索引值我们这时候可以将14输入线性函数得到新的索引为15取数组中位置为15的位置的值判断如果还是冲突则会溢出这时候可以判断溢出的时候就从位置0继续使用线性探测法查找可以插入数据的位置。 2.4.1 set方法分析
当我们使用ThreadLocal的时候首先会通过new关键字创建一个ThreadLocal对象然后调用ThreadLocal的set方法保存我们想要保存的值set方法执行过程的源码如下 使用ThreadLocal对象调用set方法存储数据的时候会首先调用下面的set方法下面的方法会将当前线程对象和需要保存的数据一起传入内部的set(Thread,value)方法里。 public void set(T value) {// 调用内部的set方法并且将当前的线程对象和要保存的值传递过去set(Thread.currentThread(), value);if (TRACE_VTHREAD_LOCALS) {dumpStackIfVirtualThread();}}set(Thread,value)方法会首先去获取下当前线程是否已经关联了ThreadLocalMap,如果已经关联了就直接取出这个Map调用其set方法保存数据否则创建一个新的ThreadLocalMap,并保存数据并且将创建的ThreadLocalMap赋值给当前线程里面的threadLocals变量。 private void set(Thread t, T value) {// 获取和当前线程相关联的ThreadLocalMapThreadLocalMap map getMap(t);// 如果获取到的ThreadLocalMap不为空则直接调用ThreadLocalMap的set方法直接赋值// 否则使用当前线程和需要保存的值直接创建ThreadLocalMap对象需要注意的是这里不用再// 调用ThreadLocalMap的set方法了因为值的保存操作会在ThreadLocalMap的构造函数中完成if (map ! null) {map.set(this, value);} else {createMap(t, value);}}// 通过线程去拿与其关联的ThreadLocalMap,从下面的代码// 可以看出ThreadLocalMap被作为了一个成员变量声明到了线程// Thread类中ThreadLocalMap getMap(Thread t) {return t.threadLocals;}// 使用线程和需要保存的数据创建一个ThreadLocalMap对象// 并将其赋值给当前线程的threadLocals变量void createMap(Thread t, T firstValue) {t.threadLocals new ThreadLocalMap(this, firstValue);}当线程中关联的ThreadLocalMap为空时会使用ThreadLocal作为key,需要保存的数据作为Value去新建一个ThreadLocalMap对象下面是ThreadLocalMap的构造方法。 ThreadLocalMap(ThreadLocal? firstKey, Object firstValue) {// table 和HashMap中的table类似这里的INITIAL_CAPACITY是16必须为2的幂次方// 原因后面会介绍// 这里主要是初始化table数组数据的元素类型是Entry的初始容量是16table new Entry[INITIAL_CAPACITY];// 和HashMap一样使用key的HashCode和长度减一做与操作计算出一个索引值int i firstKey.threadLocalHashCode (INITIAL_CAPACITY - 1);// 将要保存的数据包装成Entry后保存到索引对应的位置table[i] new Entry(firstKey, firstValue);// 记录当前ThreadLocalMap的大小size 1;// 设置扩容的阈值setThreshold(INITIAL_CAPACITY);}// 设置阈值当阈值达到设置长度的2/3时进行扩容操作private void setThreshold(int len) {threshold len * 2 / 3;}如果线程中的ThreadLocalMap不为空的情况下会被取出来调用其set(ThreadLocal? key, Object value) 方法保存数据这个方法的具体解析如下面代码中的注释所示。 private void set(ThreadLocal? key, Object value) {Entry[] tab table;// 记录下table的长度int len tab.length;// 计算待插入元素在table数组中的索引使用的是和HashMap类似的使用key的hash值和// 数组的长度减一做与操作。这里的数组长度需要是2的幂次方int i key.threadLocalHashCode (len-1);for (Entry e tab[i];e ! null;e tab[i nextIndex(i, len)]) {ThreadLocal? k e.get();// 如果待插入的key已经存在则直接使用待插入的数据覆盖原来的数据即可if (k key) {e.value value;return;}// 如果key为null,但是数据value不为null,则说明之前的ThreadLocal对象已经被回收了if (k null) {// 使用新的元素替换之前的元素replaceStaleEntry(key, value, i);return;}}// ThreadLocal对应的key不存在并且没有找到旧的元素则在空元素的位置新建一个Entrytab[i] new Entry(key, value);// 增加ThreadLocalMap的sizeint sz size;// cleanSomeSlots用于清除e.get null的元素// 因为这种数据key关联的对象已经被回收所以Entry(table[index])可以被置为null,// 如果没有清除任何的Entry,并且当前的使用量达到了负载因子所定义的(长度的2/3)// 那么进行再次哈希计算的逻辑(rehash),执行一次全表的扫描清理工作if (!cleanSomeSlots(i, sz) sz threshold)rehash();}// 循环获取数组的下一个索引private static int nextIndex(int i, int len) {return ((i 1 len) ? i 1 : 0);} 在上面的代码中我们提到了数组的长度需要是2的幂次方原因如下 我们都知道通常情况下如果想要将某个Hash值映射到数组的索引上通常会使用到取模(%)运算符因为这可以确保生成的索引在数组的范围类例如生成数组长度为16计算的结果会在0到15之间。那我们在HashMap和ThreadLocalMap中要规定数组的长度必须是2的幂次方呢那是因为计算数组的索引没有采用传统的取模(%)运算而使用的是与()操作如下图所示 使用与操作会比取模操作快很多但是只有当数组长度为2的幂次方时hashcode(数组长度 - 1) 才等价于 hashcode % 数组长度其次保证数组长度为2的幂次方也恶意减少冲突的次数提高查询的效率。例如若数组长度为2的幂次方则数组长度减一转为二进制必定是1111…的形式在和hashcode二进制做与操作时效率会非常高而且空间不浪费。举个反例假设数组的长度不是2的幂次方不妨设为15则数组的长度减一为14对应的二进制为1110在与hashcode做“与操作”时由于最后一位都是0这就会导致数组位置索引最后一位为“1”的位置(如0001010110111101)永远无法存放元素浪费空间并且导致数组可使用的位置比数组长度小很多发生哈希冲突的几率增大并且降低了查询效率。 注意这里的hashcode不是指通过对象的hashCode()方法获取到的值而是经过一些算法得到的一个哈希值 set方法代码的执行流程
根据key的hashcode计算出索引i,然后查找到i位置上的Entry若Entry存在并且key等于传入的key,那么直接给找到的Entry赋新的value值若Entry存在但是key为null,则调用replaceStaleEntry()方法更换key为空的Entry若不存在上面的情况则开启循环检测直到遇到为null的位置在这个null位置新建一个Entry,然后插入同时将ThreadLocalMap的size增加1调用cleanSomeSlots方法清理Key为null的Entry,最后返回是否清理了Entry的结果然后再判断ThreadLocalMap的size是否大于等于扩容的阈值如果达到了需要执行rehash函数进行全表扫描清理清理完ThreadLocalMap的size还是大于阈值的3/4的化那么就需要进行扩容。扩容操作会将数组的长度扩容为之前的两倍
2.4.2 get方法分析
get方法是获取当前线程中保存的值调用的方式就是使用ThreadLocal的对象调用get方法ThreadLocal中的get方法如下所示 public T get() {// 获取当前线程Thread t Thread.currentThread();// 根据当前线程拿到ThreadLocalMapThreadLocalMap map getMap(t);if (map ! null) {// ThreadLocalMap不为空的情况下调用ThreadLocalMap的getEntry方法// 传入当前的ThreadLocal(key),拿到当前ThreadLocal对应的数据并返回给调用者ThreadLocalMap.Entry e map.getEntry(this);if (e ! null) {SuppressWarnings(unchecked)// 将数据做一个类型转换然后返回T result (T)e.value;return result;}}// ThreadMap为空的情况下会调用setInitialValue方法返回一个值return setInitialValue();}// 拿到线程对应的ThreadLocalMap对象ThreadLocalMap getMap(Thread t) {return t.threadLocals;}// 设置ThreadLocal的初始值private T setInitialValue() {// 通过initialValue方法获取初始值initialValue是一个可供// 子类重写的方法子类可以重写initialValue方法提供一个默认// 值不重写的情况下为null.T value initialValue();// 获取当前线程Thread t Thread.currentThread();// 根据当前线程拿到ThreadLocalMapThreadLocalMap map getMap(t);// 如果ThreadLocalMap不为空则将通过initialValue获取的值设// 置给ThreadLocalMapif (map ! null) {map.set(this, value);} else {// 如果通过当前线程没有获取到ThreadLocalMap,则创建一个ThreadLocalMap并将通过// initialValue方法获取到的值设置给它createMap(t, value);}// 不是重点不分析这里是为了解决ThreadLocal引用泄漏的问题的TerminatingThreadLocal // 提供了一种机制可以在线程终止时自动清理其绑定的数据。if (this instanceof TerminatingThreadLocal) {TerminatingThreadLocal.register((TerminatingThreadLocal?) this);}return value;}// ThreadLocalMap中的getEntry方法参数keyprivate Entry getEntry(ThreadLocal? key) {// 通过key的threadLocalHashCode计算出数组table中的索引位置int i key.threadLocalHashCode (table.length - 1);// 通过计算出的索引值拿到对应的Entry元素Entry e table[i];// 若拿到的元素不为null,并且元素的key和当前传入的key相同则证明找到了// 传入的key对应的Entry元素直接返回if (e ! null e.get() key)return e;else// 否则可能是在插入数据时有冲突被放到了其他位置了通过getEntryAfterMiss方法// 继续查找其他位置return getEntryAfterMiss(key, i, e);}// 查找Entry元素参数key:待查找元素的key,i: 当前元素的索引索引i对应的元素Entryprivate Entry getEntryAfterMiss(ThreadLocal? key, int i, Entry e) {// 拷贝一份当前的table数组Entry[] tab table;// 记录当前数组的长度int len tab.length;// 若是元素Entry不为null,就循环查找直到找到待查找元素key对应的Entry为止while (e ! null) {ThreadLocal? k e.get();// 如果e的key等于待查找的元素的key证明找到了直接返回就行if (k key)return e;// 如果e的key为null,则调用expungeStaleEntry方法替换if (k null)// 清除key为null的EntryexpungeStaleEntry(i);else// 通过线性探测法继续寻找下一个位置插入值的时候如果有冲突也是通过这个方法// 解决的所以查询值的时候如果不在通过key的hashcode值计算出的索引位置// 就可以通过这个函数继续寻找下一个位置。直到找到待查找key对应的数据为止i nextIndex(i, len);e tab[i];}// 如果没有找到就返回nullreturn null;}
2.4.3 remove方法分析
ThreadLocal的remove方法用于删除当前线程中保存的ThreadLocal对应的Entry,代码如下所示 public void remove() {// 首先通过当前线程获取到TheadLocalMap,不为null的情况下// 删除当前ThreadLocal保存的Entry;如果为null,表示// 不需要删除ThreadLocalMap m getMap(Thread.currentThread());if (m ! null) {// 调用ThreadLocalMap的remove方法删除保存的Entrym.remove(this);}}// ThreadLocalMap中删除ThreadLocal对应的Entryprivate void remove(ThreadLocal? key) {// 拷贝一份table数组Entry[] tab table;// 记录数组的长度int len tab.length;// 根据当前的key计算出Entry的索引位置iint i key.threadLocalHashCode (len-1);// 在数组中遍历查找key对应的entryfor (Entry e tab[i];e ! null;e tab[i nextIndex(i, len)]) {// 查找到key对应的entryif (e.get() key) {// 调用Entry的clear方法清理掉Entry,其实就是将当前的Entry引用置为null// 等待垃圾回收器回收e.clear();// 清除key为null的EntryexpungeStaleEntry(i);return;}}// Entry中的clear方法public void clear() {this.referent null;}3.ThreadLocal内存泄漏分析
3.1 相关概念
3.1.1 内存溢出
内存溢出(Memory overflow)是指没有足够的内存提供给申请者使用
3.1.2 内存泄漏
内存泄漏(Memory leak)指的时程序中已经动态分配的堆内存由于某种原因未释放或者是无法释放造成系统内存的浪费从而导致程序运行速度减慢升值系统崩溃的严重后果内存泄漏的堆积最终将会导致内存溢出
3.1.3 强引用
强引用(Strong Reference)就是我们常见的普通对象的引用比如Object strongRef new Object();就是一种强引用只要某个对象有强引用指向它垃圾回收器Garbage Collector 就不会回收该对象。
3.1.4 弱引用
弱引用Weak Reference 是一种特殊的引用类型用于改善内存管理。弱引用允许垃圾回收器回收被其引用的对象即使该对象仍然有活动的弱引用存在。它通常用于缓存、引用监听器和防止内存泄漏的场景。
3.2 内存泄漏是否和key使用的弱引用有关
有读者可能会猜测ThreadLocal的内存泄漏可能会和Entry中使用了弱引用的key有关系其实这个猜测不太准确下面就从两个方面分析下ThreadLocal内存泄漏的原因
3.2.1 假设key使用强引用
假设ThreadLocalMap中的key使用了强引用则此时ThreadLocal的内存图如下所示 如上图所示假设在业务代码中使用玩ThreadLocal后ThreadLocal引用被回收了但是因为ThreadLocalMap的Entry强引用ThreadLocal,会造成ThreadLocal无法被回收这时在没有手动删除Entry和CurrentThread的情况下始终会有强引用链CurrentThread引用CurrentThread ThreadLocalMapEntry.最终导致Entry无法被回收导致内存泄漏所以ThreadLocalMap中的key使用了强引用是无法完全避免内存泄漏的 3.2.2 假设key使用弱引用
假设key使用了弱引用ThreadLocal的内存图如下所示 假设业务代码中使用完ThreadLocal后然后ThreadLocal被回收了此时由于ThreadLocalMap只持有ThreadLcoal的弱引用并且没有任何的强引用指向ThreadLocal实例所以ThreadLocal实例可以顺利的被垃圾回收器回收此时就会导致Entry的key为null,这时候如果我们没有手动删除这个Entry以及CurrentThread仍然运行的前提下也存在强引用链CurrentThread引用CurrentThread ThreadLocalMapEntryValue而这里的Value不会被回收但是这块Value永远不会被访问到了因为key已经被回收了导致Value内存泄漏所以ThreadLocalMap中的key使用了弱引用也有可能导致内存泄漏。 3.2.3 内存泄漏的真实原因
通过上面的两种对key使用强引用和弱引用的方式分析我们发现ThreadLocal的内存泄漏和ThreadLocalMap的key是否使用弱引用是没有关系的真正引起内存泄漏的原因主要有两点第一点是当ThreadLocal被回收后没有手动删除ThreadLocalMap的Entry,这时只要我们使用完后调用ThreadLocal的remove方法删除对应的Entry就可以避免内存泄漏。第二点是当ThreadLocal被回收后CurrentThread依然在运行。由于ThreadLocalMap是Thread的一个属性被当前线程引用所以它的生命周期和Thread一样长那么在使用完ThradLocal如果当前的线程也一起随之结束那么ThreadLocalMap就可以被垃圾回收器回收从根源上避免了内存泄漏 结合上面的分析可以知道ThreadLocal内存泄漏的真实原因是由于ThreadLocalMap的生命周期和Thread一样长如果没有手动删除对应的Entry就会导致内存泄漏。 3.2.4 ThreadLocalMap的key使用弱引用的原因
有的读者可能会问既然ThreadLocalMap的key使用强引用和弱引用都无法避免内存泄漏那么为啥偏偏选择使用弱引用呢经过前面的分析我们发现ThreadLocalMap的key无论使用强引用还是弱引用都无法完全避免内存泄漏如果想要避免内存泄漏主要有两种方式
使用完ThreadLocal后调用其remove方法删除对应的Entry使用完ThreadLocal后当前线程也随之结束
第一种方式相对简单直接调用ThreadLocal提供的remove方法就行但是第二种方式就不是那么好控制了因为如果在使用线程池的场景第二种方式就会出问题因为线程池中的线程有复用的情况。那么回到问题为啥ThreadLocalMap的key偏偏要使用弱引用。其实在ThreadLocal中的get/set/getEntry方法中会对key为null的情况进行判断如果key为null,则value也会被置为null,这时候使用弱引用就会多一层保障假设在ThreadLocal,CurrentThread依然运行的情况下如果忘记调用了ThreadLocal的remove方法ThreadLocalMap的key由于是弱引用所以可以被回收这时候key就为null,然后在下一次ThreadLocalMap调用set、get、getEntry中的任何一个方法都会将key为null的Entry清除掉从而避免了内存泄漏这就是为什么ThreadLocalMap的key要使用弱引用的原因。
4.ThreadLocal使用场景
ThreadLocal目前使用最常见的就是Android中Handler机制中的Looper, UnsupportedAppUsagestatic final ThreadLocalLooper sThreadLocal new ThreadLocalLooper();// 创建Looper时需要调用Looper的prepare方法private static void prepare(boolean quitAllowed) {if (sThreadLocal.get() ! null) {throw new RuntimeException(Only one Looper may be created per thread);}sThreadLocal.set(new Looper(quitAllowed));}我们都知道Android的Handler机制中每个线程有一个Looper,并且每个线程的Looper是互相不干扰的要实现这种功能就得借助ThreadLocal创建Looper的时候先判断当前创建Looper的线程是否已经有了Looper,没有再创建有的化就直接使用。
另外还有一种情景就是在服务端开发的时候读取MySQL的数据库连接对象如果是多个线程去读取的情况下每个线程都需要维护一个自己的数据库连接这样使用完后就释放自己的连接不会影响到其他线程的数据连接。