清理ThreadLocal
from http://www.importnew.com/16112.html
在我很多的课程里(master、concurrency、xj-conc-j8),我经常提起ThreadLocal。它经常受到我严厉的指责要尽可能的避免使用。ThreadLocal是为了那些使用完就销毁的线程设计的。线程生成之前,线程内的局部变量都会被清除掉。实际上,如果你读过 Why 0x61c88647?,这篇文章中解释了实际的值是存在一个内部的map中,这个map是伴随着线程的产生而产生的。存在于线程池的线程不会只存活于单个用户请求,这很容易导致内存溢出。通常我在讨论这个的时候,至少有一位学生有过因为在一个线程局部变量持有某个类而导致整个生产系统奔溃。因此,预防实体类加载后不被卸载,是一个非常普遍的问题。
在这篇文章中,我将演示一个ThreadLocalCleaner类。通过这个类,可以在线程回到线程池之前,恢复所有本地线程变量到最开始的状态。最基础的,我们可以保存当前线程的ThreadLocal的状态,之后再重置。我们可以使用Java 7提供的try-with-resource结构来完成这件事情。例如:
| 1 2 3 4 | try(ThreadLocalCleaner tlc = newThreadLocalCleaner()) {  // some code that potentially creates and adds thread locals}// at this point, the new thread locals have been cleared | 
为了简化调试,我们增加一个观察者的机制,这样我们能够监测到线程局部map发生的任何变化。这能帮助我们发现可能出现泄漏的线程局部变量。这是我们的监听器:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | packagethreadcleaner;@FunctionalInterfacepublicinterfaceThreadLocalChangeListener {  voidchanged(Mode mode, Thread thread,               ThreadLocal<?> threadLocal, Object value);  ThreadLocalChangeListener EMPTY = (m, t, tl, v) -> {};  ThreadLocalChangeListener PRINTER =      (m, t, tl, v) -> System.out.printf(          "Thread %s %s ThreadLocal %s with value %s%n",          t, m, tl.getClass(), v);  enumMode {    ADDED, REMOVED  }} | 
这个地方可能需要做一下必要的说明。首先,我添加了注解@FunctionalInterface,这个注解是Java 8提供的,它的意思是该类只有一个抽象方法,可以作为lambda表达式使用。其次,我在该类的内部定义了一个EMPTY的lambda表达式。这样,你可以见识到,这段代码会非常短小。第三,我还定义了一个默认的PRINTER,它可以简单的通过System.out输出改变的信息。最后,我们还有两个不不同的事件,但是因为想设计成为一个函数式编程接口(@FunctionalInterface),我不得不把这个标示定义为单独的属性,这里定义成了枚举。
当我们构造ThreadLocalCleaner时,我们可以传递一个ThreadLocalChangeListener。这样,从Treadlocal创建开始发生的任何变化,我们都能监测到。请注意,这种机制只适合于当前线程。这有一个例子演示我们怎样通过try-with-resource代码块来使用ThreadLocalCleaner:任何定义在在 try(…) 中的局部变量,都会在代码块的自后进行自动关闭。因此,我们需要在ThreadLocalCleaner内部有一个 close() 方法,用于恢复线程局部变量到初始值。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | importjava.text.*;publicclassThreadLocalCleanerExample {  privatestaticfinalThreadLocal df =      newThreadLocal() {        protectedDateFormat initialValue() {          returnnewSimpleDateFormat("yyyy-MM-dd");        }      };  publicstaticvoidmain(String... args) {    System.out.println("First ThreadLocalCleaner context");    try(ThreadLocalCleaner tlc = newThreadLocalCleaner(        ThreadLocalChangeListener.PRINTER)) {      System.out.println(System.identityHashCode(df.get()));      System.out.println(System.identityHashCode(df.get()));      System.out.println(System.identityHashCode(df.get()));    }    System.out.println("Another ThreadLocalCleaner context");    try(ThreadLocalCleaner tlc = newThreadLocalCleaner(        ThreadLocalChangeListener.PRINTER)) {      System.out.println(System.identityHashCode(df.get()));      System.out.println(System.identityHashCode(df.get()));      System.out.println(System.identityHashCode(df.get()));    }  }} | 
在ThreadLocalCleaner类中还有两个公共的静态方法:forEach() 和 cleanup(Thread)。forEach() 方法有两个参数:Thread和BiConsumer。该方法通过ThreadLocal调用来遍历其中的每一个值。我们跳过了key为null的对象,但是没有跳过值为null的对象。理由是如果仅仅是值为null,ThreadLocal仍然有可能出现内存泄露。一旦我们使用完了ThreadLocal,就应该在将线程返回给线程池之前调用 remove() 方法。cleanup(Thread) 方法设置ThreadLocal的map在该线程内为null,因此,允许垃圾回收器回收所有的对象。如果一个ThreadLocal在我们清理后再次使用,就简单的调用 initialValue() 方法来创建对象。这是方法的定义:
| 1 2 3 4 | publicstaticvoidforEach(Thread thread,      BiConsumer<ThreadLocal<?>, Object> consumer) { ... }publicstaticvoidcleanup(Thread thread) { ... } | 
ThreadLocalCleaner类完整的代码如下。该类使用了许多反射来操作私有域。它可能只能在OpenJDK或其直接衍生产品上运行。你也能注意到我使用了Java 8的语法。我纠结过很长一段时间是否使用Java 8 或 7。我的某些客户端还在使用1.4。最后,我的大部分大银行客户已经在产品中开始使用Java 8。银行通常来说不是最先采用的新技术人,除非存在非常大的经济意义。因此,如果你还没有在产品中使用Java 8,你应该尽可能快的移植过去,甚至可以跳过Java 8,直接到Java 9。你应该可以很容易的反向移植到Java 7上,只需要自定义一个BiConsumer接口。Java 6不支持try-with-resource结构,所以反向移植会比较困难一点。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | packagethreadcleaner;importjava.lang.ref.*;importjava.lang.reflect.*;importjava.util.*;importjava.util.function.*;importstaticthreadcleaner.ThreadLocalChangeListener.Mode.*;publicclassThreadLocalCleaner implementsAutoCloseable {  privatefinalThreadLocalChangeListener listener;  publicThreadLocalCleaner() {    this(ThreadLocalChangeListener.EMPTY);  }  publicThreadLocalCleaner(ThreadLocalChangeListener listener) {    this.listener = listener;    saveOldThreadLocals();  }  publicvoidclose() {    cleanup();  }  publicvoidcleanup() {    diff(threadLocalsField, copyOfThreadLocals.get());    diff(inheritableThreadLocalsField,        copyOfInheritableThreadLocals.get());    restoreOldThreadLocals();  }  publicstaticvoidforEach(      Thread thread,      BiConsumer<ThreadLocal<?>, Object> consumer) {    forEach(thread, threadLocalsField, consumer);    forEach(thread, inheritableThreadLocalsField, consumer);  }  publicstaticvoidcleanup(Thread thread) {    try{      threadLocalsField.set(thread, null);      inheritableThreadLocalsField.set(thread, null);    } catch(IllegalAccessException e) {      thrownewIllegalStateException(          "Could not clear thread locals: "+ e);    }  }  privatevoiddiff(Field field, Reference<?>[] backup) {    try{      Thread thread = Thread.currentThread();      Object threadLocals = field.get(thread);      if(threadLocals == null) {        if(backup != null) {          for(Reference<?> reference : backup) {            changed(thread, reference,                REMOVED);          }        }        return;      }      Reference<?>[] current =          (Reference<?>[]) tableField.get(threadLocals);      if(backup == null) {        for(Reference<?> reference : current) {          changed(thread, reference, ADDED);        }      } else{        // nested loop - both arrays *should* be relatively small        next:        for(Reference<?> curRef : current) {          if(curRef != null) {            if(curRef.get() == copyOfThreadLocals ||                curRef.get() == copyOfInheritableThreadLocals) {              continuenext;            }            for(Reference<?> backupRef : backup) {              if(curRef == backupRef) continuenext;            }            // could not find it in backup - added            changed(thread, curRef, ADDED);          }        }        next:        for(Reference<?> backupRef : backup) {          for(Reference<?> curRef : current) {            if(curRef == backupRef) continuenext;          }          // could not find it in current - removed          changed(thread, backupRef,              REMOVED);        }      }    } catch(IllegalAccessException e) {      thrownewIllegalStateException("Access denied", e);    }  }  privatevoidchanged(Thread thread, Reference<?> reference,                       ThreadLocalChangeListener.Mode mode)      throwsIllegalAccessException {    listener.changed(mode,        thread, (ThreadLocal<?>) reference.get(),        threadLocalEntryValueField.get(reference));  }  privatestaticField field(Class<?> c, String name)      throwsNoSuchFieldException {    Field field = c.getDeclaredField(name);    field.setAccessible(true);    returnfield;  }  privatestaticClass<?> inner(Class<?> clazz, String name) {    for(Class<?> c : clazz.getDeclaredClasses()) {      if(c.getSimpleName().equals(name)) {        returnc;      }    }    thrownewIllegalStateException(        "Could not find inner class "+ name + " in "+ clazz);  }  privatestaticvoidforEach(      Thread thread, Field field,      BiConsumer<ThreadLocal<?>, Object> consumer) {    try{      Object threadLocals = field.get(thread);      if(threadLocals != null) {        Reference<?>[] table = (Reference<?>[])            tableField.get(threadLocals);        for(Reference<?> ref : table) {          if(ref != null) {            ThreadLocal<?> key = (ThreadLocal<?>) ref.get();            if(key != null) {              Object value = threadLocalEntryValueField.get(ref);              consumer.accept(key, value);            }          }        }      }    } catch(IllegalAccessException e) {      thrownewIllegalStateException(e);    }  }  privatestaticfinalThreadLocal<Reference<?>[]>      copyOfThreadLocals = newThreadLocal<>();  privatestaticfinalThreadLocal<Reference<?>[]>      copyOfInheritableThreadLocals = newThreadLocal<>();  privatestaticvoidsaveOldThreadLocals() {    copyOfThreadLocals.set(copy(threadLocalsField));    copyOfInheritableThreadLocals.set(        copy(inheritableThreadLocalsField));  }  privatestaticReference<?>[] copy(Field field) {    try{      Thread thread = Thread.currentThread();      Object threadLocals = field.get(thread);      if(threadLocals == null) returnnull;      Reference<?>[] table =          (Reference<?>[]) tableField.get(threadLocals);      returnArrays.copyOf(table, table.length);    } catch(IllegalAccessException e) {      thrownewIllegalStateException("Access denied", e);    }  }  privatestaticvoidrestoreOldThreadLocals() {    try{      restore(threadLocalsField, copyOfThreadLocals.get());      restore(inheritableThreadLocalsField,          copyOfInheritableThreadLocals.get());    } finally{      copyOfThreadLocals.remove();      copyOfInheritableThreadLocals.remove();    }  }  privatestaticvoidrestore(Field field, Object value) {    try{      Thread thread = Thread.currentThread();      if(value == null) {        field.set(thread, null);      } else{        tableField.set(field.get(thread), value);      }    } catch(IllegalAccessException e) {      thrownewIllegalStateException("Access denied", e);    }  }  /* Reflection fields */  privatestaticfinalField threadLocalsField;  privatestaticfinalField inheritableThreadLocalsField;  privatestaticfinalClass<?> threadLocalMapClass;  privatestaticfinalField tableField;  privatestaticfinalClass<?> threadLocalMapEntryClass;  privatestaticfinalField threadLocalEntryValueField;  static{    try{      threadLocalsField = field(Thread.class, "threadLocals");      inheritableThreadLocalsField =          field(Thread.class, "inheritableThreadLocals");      threadLocalMapClass =          inner(ThreadLocal.class, "ThreadLocalMap");      tableField = field(threadLocalMapClass, "table");      threadLocalMapEntryClass =          inner(threadLocalMapClass, "Entry");      threadLocalEntryValueField =          field(threadLocalMapEntryClass, "value");    } catch(NoSuchFieldException e) {      thrownewIllegalStateException(          "Could not locate threadLocals field in Thread.  "+              "Will not be able to clear thread locals: "+ e);    }  }} | 
这是一个ThreadLocalCleaner在实践中应用的例子:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | importjava.text.*;publicclassThreadLocalCleanerExample {  privatestaticfinalThreadLocal<DateFormat> df =      newThreadLocal<DateFormat>() {        protectedDateFormat initialValue() {          returnnewSimpleDateFormat("yyyy-MM-dd");        }      };  publicstaticvoidmain(String... args) {    System.out.println("First ThreadLocalCleaner context");    try(ThreadLocalCleaner tlc = newThreadLocalCleaner(        ThreadLocalChangeListener.PRINTER)) {      System.out.println(System.identityHashCode(df.get()));      System.out.println(System.identityHashCode(df.get()));      System.out.println(System.identityHashCode(df.get()));    }    System.out.println("Another ThreadLocalCleaner context");    try(ThreadLocalCleaner tlc = newThreadLocalCleaner(        ThreadLocalChangeListener.PRINTER)) {      System.out.println(System.identityHashCode(df.get()));      System.out.println(System.identityHashCode(df.get()));      System.out.println(System.identityHashCode(df.get()));    }  }} | 
你的输出结果可能会包含不同的hash code值。但是请记住我在Identity Crisis Newsletter中所说的:hash code的生成算法是一个随机数字生成器。这是我的输出。注意,在try-with-resource内部,线程局部变量的值是相同的。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | First ThreadLocalCleaner context186370029186370029186370029Thread Thread[main,5,main] ADDED ThreadLocal class    ThreadLocalCleanerExample$1with value     java.text.SimpleDateFormat@f67a0200Another ThreadLocalCleaner context209454835820945483582094548358Thread Thread[main,5,main] ADDED ThreadLocal class    ThreadLocalCleanerExample$1with value     java.text.SimpleDateFormat@f67a0200 | 
为了让这个代码使用起来更简单,我写了一个Facede。门面设计模式不是阻止用户使用直接使用子系统,而是提供一种更简单的接口来完成复杂的系统。最典型的方式是将最常用子系统作为方法提供,我们的门面包括两个方法:findAll(Thread) 和 printThreadLocals() 方法。findAll() 方法返回一个线程内部的Entry集合。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | packagethreadcleaner;importjava.io.*;importjava.lang.ref.*;importjava.util.AbstractMap.*;importjava.util.*;importjava.util.Map.*;importjava.util.function.*;importstaticthreadcleaner.ThreadLocalCleaner.*;publicclassThreadLocalCleaners {  publicstaticCollection<Entry<ThreadLocal<?>, Object>> findAll(      Thread thread) {    Collection<Entry<ThreadLocal<?>, Object>> result =        newArrayList<>();    BiConsumer<ThreadLocal<?>, Object> adder =        (key, value) ->            result.add(newSimpleImmutableEntry<>(key, value));    forEach(thread, adder);    returnresult;  }  publicstaticvoidprintThreadLocals() {    printThreadLocals(System.out);  }  publicstaticvoidprintThreadLocals(Thread thread) {    printThreadLocals(thread, System.out);  }  publicstaticvoidprintThreadLocals(PrintStream out) {    printThreadLocals(Thread.currentThread(), out);  }  publicstaticvoidprintThreadLocals(Thread thread,                                       PrintStream out) {    out.println("Thread "+ thread.getName());    out.println("  ThreadLocals");    printTable(thread, out);  }  privatestaticvoidprintTable(      Thread thread, PrintStream out) {    forEach(thread, (key, value) -> {      out.printf("    {%s,%s", key, value);      if(value instanceofReference) {        out.print("->"+ ((Reference<?>) value).get());      }      out.println("}");    });  }} | 
线程可以包含两个不同类型的ThreadLocal:一个是普通的,另一个是可继承的。大部分情况下,我们使用普通的那个。可继承意味着如果你从当前线程构造出一个新的线程,则所有可继承的ThreadLocals将被新线程继承过去。非常同意。我们很少这么使用。所以现在我们可以忘了这种情况,或者永远忘记。
一个使用ThreadLocalCleaner的典型场景是和ThreadPoolExecutor一起使用。我们写一个子类,覆盖 beforeExecute() 和 afterExecute() 方法。这个类比较长,因为我们不得不编写所有的构造函数。有意思的地方在最后面。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | packagethreadcleaner;importjava.util.concurrent.*;publicclassThreadPoolExecutorExt extendsThreadPoolExecutor {  privatefinalThreadLocalChangeListener listener;  // Bunch of constructors following - you can ignore those   publicThreadPoolExecutorExt(      intcorePoolSize, intmaximumPoolSize, longkeepAliveTime,      TimeUnit unit, BlockingQueue<Runnable> workQueue) {    this(corePoolSize, maximumPoolSize, keepAliveTime, unit,        workQueue, ThreadLocalChangeListener.EMPTY);  }  publicThreadPoolExecutorExt(      intcorePoolSize, intmaximumPoolSize, longkeepAliveTime,      TimeUnit unit, BlockingQueue<Runnable> workQueue,      ThreadFactory threadFactory) {    this(corePoolSize, maximumPoolSize, keepAliveTime, unit,        workQueue, threadFactory,        ThreadLocalChangeListener.EMPTY);  }  publicThreadPoolExecutorExt(      intcorePoolSize, intmaximumPoolSize, longkeepAliveTime,      TimeUnit unit, BlockingQueue<Runnable> workQueue,      RejectedExecutionHandler handler) {    this(corePoolSize, maximumPoolSize, keepAliveTime, unit,        workQueue, handler,        ThreadLocalChangeListener.EMPTY);  }  publicThreadPoolExecutorExt(      intcorePoolSize, intmaximumPoolSize, longkeepAliveTime,      TimeUnit unit, BlockingQueue<Runnable> workQueue,      ThreadFactory threadFactory,      RejectedExecutionHandler handler) {    this(corePoolSize, maximumPoolSize, keepAliveTime, unit,        workQueue, threadFactory, handler,        ThreadLocalChangeListener.EMPTY);  }  publicThreadPoolExecutorExt(      intcorePoolSize, intmaximumPoolSize, longkeepAliveTime,      TimeUnit unit, BlockingQueue<Runnable> workQueue,      ThreadLocalChangeListener listener) {    super(corePoolSize, maximumPoolSize, keepAliveTime, unit,        workQueue);    this.listener = listener;  }  publicThreadPoolExecutorExt(      intcorePoolSize, intmaximumPoolSize, longkeepAliveTime,      TimeUnit unit, BlockingQueue<Runnable> workQueue,      ThreadFactory threadFactory,      ThreadLocalChangeListener listener) {    super(corePoolSize, maximumPoolSize, keepAliveTime, unit,        workQueue, threadFactory);    this.listener = listener;  }  publicThreadPoolExecutorExt(      intcorePoolSize, intmaximumPoolSize, longkeepAliveTime,      TimeUnit unit, BlockingQueue<Runnable> workQueue,      RejectedExecutionHandler handler,      ThreadLocalChangeListener listener) {    super(corePoolSize, maximumPoolSize, keepAliveTime, unit,        workQueue, handler);    this.listener = listener;  }  publicThreadPoolExecutorExt(      intcorePoolSize, intmaximumPoolSize, longkeepAliveTime,      TimeUnit unit, BlockingQueue<Runnable> workQueue,      ThreadFactory threadFactory,      RejectedExecutionHandler handler,      ThreadLocalChangeListener listener) {    super(corePoolSize, maximumPoolSize, keepAliveTime, unit,        workQueue, threadFactory, handler);    this.listener = listener;  }  /* The interest bit of this class is below ... */  privatestaticfinalThreadLocal<ThreadLocalCleaner> local =      newThreadLocal<>();  protectedvoidbeforeExecute(Thread t, Runnable r) {    assertt == Thread.currentThread();    local.set(newThreadLocalCleaner(listener));  }  protectedvoidafterExecute(Runnable r, Throwable t) {    ThreadLocalCleaner cleaner = local.get();    local.remove();    cleaner.cleanup();  }} | 
你可以像使用一个普通的ThreadPoolExecutor一样使用这个类,该类不同的地方在于,当每个Runnable执行完之后需要重置线程局部变量的状态。如果需要调试系统,你也可以获取到绑定的监听器。在我们的这个例子里,你可以看到,我们将监听器绑定到我们的增加线程局部变量的LOG上。注意,在Java 8中,java.util.logging.Logger的方法使用Supplier作为参数,这意味着我们不再需要任何代码来保证日志的性能。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | importjava.text.*;importjava.util.concurrent.*;importjava.util.logging.*;publicclassThreadPoolExecutorExtTest {  privatefinalstaticLogger LOG = Logger.getLogger(      ThreadPoolExecutorExtTest.class.getName()  );  privatestaticfinalThreadLocal<DateFormat> df =      newThreadLocal<DateFormat>() {        protectedDateFormat initialValue() {          returnnewSimpleDateFormat("yyyy-MM-dd");        }      };  publicstaticvoidmain(String... args)      throwsInterruptedException {    ThreadPoolExecutor tpe = newThreadPoolExecutorExt(        1, 1, 0, TimeUnit.SECONDS,        newLinkedBlockingQueue<>(),        (m, t, tl, v) -> {          LOG.warning(              () -> String.format(                  "Thread %s %s ThreadLocal %s with value %s%n",                  t, m, tl.getClass(), v)          );        }    );    for(inti = 0; i < 10; i++) {      tpe.submit(() ->          System.out.println(System.identityHashCode(df.get())));      Thread.sleep(1000);    }    tpe.shutdown();  }} | 
我机器的输出结果如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 914524658May 23, 20159:28:50PM ThreadPoolExecutorExtTest lambda$main$1WARNING: Thread Thread[pool-1-thread-1,5,main]     ADDED ThreadLocal classThreadPoolExecutorExtTest$1    with value java.text.SimpleDateFormat@f67a0200957671209May 23, 20159:28:51PM ThreadPoolExecutorExtTest lambda$main$1WARNING: Thread Thread[pool-1-thread-1,5,main]     ADDED ThreadLocal classThreadPoolExecutorExtTest$1    with value java.text.SimpleDateFormat@f67a0200466968587May 23, 20159:28:52PM ThreadPoolExecutorExtTest lambda$main$1WARNING: Thread Thread[pool-1-thread-1,5,main]     ADDED ThreadLocal classThreadPoolExecutorExtTest$1    with value java.text.SimpleDateFormat@f67a0200 | 
现在,这段代码还没有在生产服务器上经过严格的考验,所以请谨慎使用。非常感谢你的阅读和支持。我真的非常感激。
 
                    
                     
                    
                 
                    
                 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号