深入学习ThreadLocal和InheritableThreadLocal
来源:互联网 发布:2017网络综艺节目排名 编辑:程序博客网 时间:2024/06/05 16:45
最新看项目代码时, 发现有地方用到了InheritableThreadLocal,之前只用过ThreadLocal,于是就查了点资料,看了下源码,稍微学习一下。
本文地址:http://blog.csdn.net/v123411739/article/details/78697099
ThreadLocal
1.ThreadLocal的定义
这个类提供了线程局部变量。 这些变量与正常的变量不同,因为每个访问一个线程的线程(通过它的get或set方法)都有自己的,独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程关联的类(例如,用户ID或事务ID)中的私有静态字段。只要线程处于活动状态并且ThreadLocal实例可以访问,每个线程就拥有对其线程局部变量副本的隐式引用; 在一个线程消失之后,线程本地实例的所有副本都会被垃圾收集(除非存在对这些副本的其他引用)。
2.ThreadLocalMap的定义
ThreadLocalMap是一个自定义哈希映射,仅适用于维护线程本地值。 没有任何操作被导出到ThreadLocal类之外。 该类是封装私有的,允许在Thread类中声明字段。 为了处理非常大和长期使用的用法,哈希表条目使用WeakReferences作为键。 但是,由于不使用引用队列,所以只有当表开始空间不足时才能删除旧条目。
ThreadLocalMap是ThreadLocal的静态内部类,是一个类似HashMap的实现,有兴趣的可以去看下源码,用法和HashMap没有区别,这里不在赘述。
3.ThreadLoca.set()方法
/** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;先拿到当前线程,再使用getMap方法拿到当前线程的threadLocals变量,如果threadLocals不为空,则将当前ThreadLocal作为key,传入的值作为value,插入threadLocals;如果threadLocals为空则新建一个,然后将ThreadLocal作为key,传入的值作为value,插入threadLocals。注意此处的threadLocals变量是一个ThreadLocalMap,是Thread的一个局部变量,因此它只与当前线程绑定。
4.ThreadLoca.get()方法
/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
/** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */ private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
protected T initialValue() { return null; }
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }跟set方法差不多,先拿到当前的线程,再使用getMap方法拿到当前线程的threadLocals变量,如果threadLocals不为空,则将当前ThreadLocal作为key,拿到对应的值;如果threadLocals为空,则会创建一个新的ThreadLocalMap,并将当前的ThreadLocal作为key,null作为value,插入到ThreadLocalMap,并返回null。注意上面的 initialValue()方法为protected,官方注释写到:如果程序员希望线程局部变量具有非null的初始值,则必须对ThreadLocal进行子类化,并重写此方法。 通常,将使用匿名内部类。
5.ThreadLocal.remove()方法
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }方法很简单,拿到当前线程的threadLocals属性,如果不为空,则将key为当前ThreadLocal的键值对移除。
6.功能测试
package com.chillax.test;import java.util.concurrent.TimeUnit;/** * ThreadLocal测试 * * @author JoonWhee * @Date 2017年12月2日 */public class TestThreadLocal { public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(); public static void main(String args[]) throws InterruptedException { Thread threadOne = new ThreadOne(); // 线程1 Thread threadTo = new ThreadTo(); // 线程2 threadTo.start(); // 线程2开始执行 TimeUnit.MILLISECONDS.sleep(100); // 睡眠, 以等待线程2执行完毕 threadOne.start(); // 线程1开始执行 TimeUnit.MILLISECONDS.sleep(100); // 睡眠, 以等待线程1执行完毕 // 此时线程1和线程2都已经设置过值, 此处输出为空, 说明子线程与父线程之间也是互不影响的 System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get()); } private static class ThreadOne extends Thread { @Override public void run() { // 此时线程2已经调用过set(456), 此处输出为空, 说明线程之间是互不影响的 System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get()); threadLocal.set(123); System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get()); } } private static class ThreadTo extends Thread { @Override public void run() { threadLocal.set(456); // 设置当前ThreadLocal的值为456 System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get()); } }}
输出结果:
Thread-1: 456
Thread-0: null
Thread-0: 123
main: null
总结:
1.每个线程都有一个ThreadLocalMap 类型的 threadLocals 属性。2.ThreadLocalMap 类相当于一个Map,key 是 ThreadLocal 本身,value 就是我们的值。
3.当我们通过 threadLocal.set(new Integer(123)); ,我们就会在这个线程中的 threadLocals 属性中放入一个键值对,key 是 这个 threadLocal.set(new Integer(123))的threadlocal,value 就是值new Integer(123)。
4.当我们通过 threadlocal.get() 方法的时候,首先会根据这个线程得到这个线程的 threadLocals 属性,然后由于这个属性放的是键值对,我们就可以根据键 threadlocal 拿到值。 注意,这时候这个键 threadlocal 和 我们 set 方法的时候的那个键 threadlocal 是一样的,所以我们能够拿到相同的值。
5.ThreadLocalMap 的get/set/remove方法跟HashMap的内部实现都基本一样,都是通过hashCode和Entry长度-1进行位于运算得到我们想要找的索引位置,如果该索引位置的键值对不止1个,则遍历并找到我们所需的键值对然后进行相应操作即可,ThreadLocalMap 的hashCode跟HashMap不一样,有兴趣的可以去看看。
InheritableThreadLocal
1.InheritableThreadLocal的定义
InheritableThreadLocal继承了ThreadLocal,此类扩展了ThreadLocal以提供从父线程到子线程的值的继承:当创建子线程时,子线程接收父线程具有的所有可继承线程局部变量的初始值。 通常子线程的值与父线程的值是一致的。 但是,通过重写此类中的childValue方法,可以将子线程的值作为父线程的任意函数。
2.InheritableThreadLocal的源码
public class InheritableThreadLocal<T> extends ThreadLocal<T> { /** * Computes the child's initial value for this inheritable thread-local * variable as a function of the parent's value at the time the child * thread is created. This method is called from within the parent * thread before the child is started. * <p> * This method merely returns its input argument, and should be overridden * if a different behavior is desired. * * @param parentValue the parent thread's value * @return the child thread's initial value */ protected T childValue(T parentValue) { return parentValue; } /** * Get the map associated with a ThreadLocal. * * @param t the current thread */ ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } /** * Create the map associated with a ThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the table. */ void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); }}InheritableThreadLocal的源码很短,只有3个很短的方法,我们主要关注getMap()方法和creatMap()方法,这两个方法都是重写的,跟ThreadLocal中的差别在于把ThreadLocal中的threadLocals换成了inheritableThreadLocals,这两个变量都是ThreadLocalMap类型,并且都是Thread类的属性。
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
3.InheritableThreadLocal继承父线程的值
上文的定义说道了InheritableThreadLocal会继承父线程的值,这是InheritableThreadLocal被创造出来的意义,具体是怎么实现的?
让我们从子线程被创建出来开始看起
public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); }
private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null); }
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) {// ... 省略掉一部分代码if (parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);// ... 省略掉一部分代码}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }
/** * Construct a new map including all Inheritable ThreadLocals * from given parent map. Called only by createInheritedMap. * * @param parentMap the map associated with parent thread. */ private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len];// 创建跟父线程相同大小的table for (int j = 0; j < len; j++) {// 遍历父线程的inheritableThreadLocals, 在上面第3个代码块作为参数传下来 Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();// 拿到每个键值对的key, 即ThreadLocal对象 if (key != null) { Object value = key.childValue(e.value);// 此处会调用InheritableThreadLocal重写的方法, 默认直接返回入参值 Entry c = new Entry(key, value);// 使用key和value构造一个Entry int h = key.threadLocalHashCode & (len - 1);// 通过位与运算找到索引位置 while (table[h] != null)// 如果该索引位置已经被占,则寻找下一个索引位置 h = nextIndex(h, len); table[h] = c;// 将Entry放在对应的位置 size++; } } } }这是线程被创建的整个流程,从第3个代码块我们可以知道当父线程的inheritableThreadLocals不为空时,当前线程的inheritableThreadLocals属性值会被直接创建,并被赋予跟父线程的inheritableThreadLocals属性一样的值,从最后一个代码块看出来(已在代码中做详细注释)。
此时我们知道了,当一个子线程创建的时候,会把父线程的inheritableThreadLocals属性的值继承到自己的inheritableThreadLocals属性。那么现在的问题是父线程的inheritableThreadLocals属性会有值吗?因为上文提到的ThreadLocal中,我们知道set()方法时,是把键值对放在threadLocals属性。这就要提到刚才说的InheritableThreadLocal重写的getMap()方法,因为InheritableThreadLocal类的set()和get()方法都是用的父类即ThreadLocal的方法,而从ThreadLocal的源码中我们知道,ThreadLocal的get()、set()、remove()方法都会先调用getMap()方法,而InheritableThreadLocal重写了该方法,因此此时返回的ThreadLocalMap为inheritableThreadLocals,所以我们知道了,当定义为InheritableThreadLocal时,操作的属性为inheritableThreadLocals而不是threadLocals。
/** * Get the map associated with a ThreadLocal. * * @param t the current thread */ ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; }
4.功能测试
下面代码是对InheritableThreadLocal继承父线程的值的验证,可以看出,子线程确实拿到了父线程的值。
5.InheritableThreadLocal变量的可见性探讨
package com.chillax.test;import java.util.concurrent.TimeUnit;/** * InheritableThreadLocal可见性测试 * * @author JoonWhee * @author http://blog.csdn.net/v123411739 * @Date 2017年12月2日 */public class TestInheritableThreadLocal2 { public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(); public static ThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>(); public static ThreadLocal<User> mutableInheritableThreadLocal = new InheritableThreadLocal<User>(); public static ThreadLocal<User> mutableInheritableThreadLocalTo = new InheritableThreadLocal<User>(); public static ThreadLocal<String> immutableInheritableThreadLocal = new InheritableThreadLocal<String>(); public static ThreadLocal<String> immutableInheritableThreadLocalTo = new InheritableThreadLocal<String>(); public static void main(String args[]) throws InterruptedException { // 测试0.ThreadLocal普通测试; // 结论0: ThreadLocal下子线程获取不到父线程的值 threadLocal.set(new Integer(123)); // 父线程初始化 Thread thread = new MyThread(); thread.start(); TimeUnit.MILLISECONDS.sleep(100); // 睡眠, 以等待子线程执行完毕 System.out.println("main = " + threadLocal.get()); System.out.println(); // 测试1.InheritableThreadLocal普通测试; // 结论1: InheritableThreadLocal下子线程可以获取父线程的值 inheritableThreadLocal.set(new Integer(123)); // 父线程初始化 Thread threads = new MyThreadTo(); threads.start(); TimeUnit.MILLISECONDS.sleep(100); // 睡眠, 以等待子线程执行完毕 System.out.println("main = " + inheritableThreadLocal.get()); System.out.println(); // 测试2.父线程和子线程的传递关系测试: 可变对象, 父线程初始化; // 结论2: 父线程初始化, Thread Construct浅拷贝, 共用索引, 子线程先get()对象, 再修改对象的属性, // 父线程跟着变, 注意: 此处子线程如果没有先get()直接使用set()一个新对象, 父线程是不会跟着变的 mutableInheritableThreadLocal.set(new User("joon"));// 2.1父线程初始化 Thread TestThread = new TestThread(); // 2.2先初始化父线程再创建子线程, 确保子线程能继承到父线程的User TestThread.start(); // 开始执行子进程 TimeUnit.MILLISECONDS.sleep(100); // 睡眠, 以等待子线程执行完毕 System.out.println("main = " + mutableInheritableThreadLocal.get()); // 2.5此处输出值为子线程修改的值, 因此可得出上述结论2 System.out.println(); // 测试3.父线程和子线程的传递关系测试: 可变对象, 父线程不初始化; // 结论3: 父线程没有初始化, 子线程初始化, 无Thread Construct浅拷贝, 子线程和主线程都是单独引用, 不同对象, // 子线程修改父线程不跟着变 Thread TestThreadTo = new TestThreadTo(); // 3.1创建子进程 TestThreadTo.start(); TimeUnit.MILLISECONDS.sleep(100); // 睡眠, 以等待子线程执行完毕 System.out.println("main = " + mutableInheritableThreadLocalTo.get()); // 3.3此处输出为null, 可得出上述结论3 System.out.println(); // 测试4.父线程和子线程的传递关系测试: 不可变对象, 父线程初始化; // 结论4: 父线程初始化, Thread Construct浅拷贝, 但由于不可变对象由于每次都是新对象, 所以互不影响 immutableInheritableThreadLocal.set("joon");// 4.1父线程初始化 Thread TestThreadTre = new TestThreadTre(); // 4.2先初始化父线程再创建子线程, 确保子线程能继承到父线程的值 TestThreadTre.start(); // 执行子进程 TimeUnit.MILLISECONDS.sleep(100); // 睡眠, 以等待子线程执行完毕 System.out.println("main = " + immutableInheritableThreadLocal.get()); // 4.5此处输出为父线程的值, 因此可得出上述结论4 System.out.println(); // 测试5.父线程和子线程的传递关系测试: 不可变对象, 父线程不初始化; // 结论5: 父线程没有初始化, 子线程初始化, 无Thread Construct浅拷贝, 子线程和父线程操作不同对象, 互不影响 Thread TestThreadFour = new TestThreadFour(); // 5.1创建子线程 TestThreadFour.start(); TimeUnit.MILLISECONDS.sleep(100); // 睡眠, 以等待子线程执行完毕 System.out.println("main = " + immutableInheritableThreadLocalTo.get()); // 5.3此处输出为空, 因此可得出上述结论5 } private static class MyThread extends Thread { @Override public void run() { System.out.println("MyThread = " + threadLocal.get()); } } private static class MyThreadTo extends Thread { @Override public void run() { System.out.println("inheritableThreadLocal = " + inheritableThreadLocal.get()); } } private static class TestThread extends Thread { @Override public void run() { // 2.3此处输出父线程的初始化对象值, 代表子线程确实继承了父线程的对象值 System.out.println("TestThread.before = " + mutableInheritableThreadLocal.get()); // 2.4子类拿到对象并修改 mutableInheritableThreadLocal.get().setName("whee"); System.out.println("mutableInheritableThreadLocal = " + mutableInheritableThreadLocal.get()); } } private static class TestThreadTo extends Thread { @Override public void run() { mutableInheritableThreadLocalTo.set(new User("whee"));// 3.2子线程调用set方法 System.out.println("mutableInheritableThreadLocalTo = " + mutableInheritableThreadLocalTo.get()); } } private static class TestThreadTre extends Thread { @Override public void run() { // 4.3此处输出父线程初始化的值, 代表子线程确实继承了父线程的对象值 System.out.println("TestThreadTre.before = " + immutableInheritableThreadLocal.get()); // 4.4子线程调用set方法 immutableInheritableThreadLocal.set("whee"); System.out.println("immutableInheritableThreadLocal = " + immutableInheritableThreadLocal.get()); } } private static class TestThreadFour extends Thread { @Override public void run() { immutableInheritableThreadLocalTo.set("whee");// 5.2子线程调用set方法 System.out.println("immutableInheritableThreadLocalTo = " + immutableInheritableThreadLocalTo.get()); } } private static class User { String name; public User(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "User [name=" + name + "]"; } }}
输出结果:
1.对于可变对象:父线程初始化, 因为Thread Construct浅拷贝, 共用索引, 子线程修改父线程跟着变; 父线程不初始化, 子线程初始化, 无Thread Construct浅拷贝, 子线程和父线程都是单独引用, 不同对象, 子线程修改父线程不跟着变。
2.对于不可变对象:不可变对象由于每次都是新对象, 所以无论父线程初始化与否,子线程和父线程都互不影响。
从上面两条结论可知,子线程只能通过修改可变性(Mutable)对象对主线程才是可见的,即才能将修改传递给主线程,但这不是一种好的实践,不建议使用,为了保护线程的安全性,一般建议只传递不可变(Immuable)对象,即没有状态的对象。
虽然说不建议使用,但是有时候还是会碰到这种情况,如果想在修改子线程可变对象,同时不影响主线程,可以通过重写childValue()方法来实现。
6.重写childValue()方法实现子线程与父线程之间互不影响
package com.chillax.test;import java.util.HashMap;import java.util.Map;import java.util.concurrent.TimeUnit;/** * 子线程与父线程实现完全互不影响 * @author JoonWhee * @author http://blog.csdn.net/v123411739 * @Date 2017年12月2日 */public class TestInheritableThreadLocal3 { private static final ThreadLocal<Map<Object, Object>> testThreadLocal = new InheritableThreadLocal<Map<Object, Object>>(); private static final ThreadLocal<Map<Object, Object>> threadLocal = new InheritableThreadLocalMap<Map<Object, Object>>(); public static void main(String args[]) throws InterruptedException { // 下面的测试1在上文已经做过(上文的测试2), 此处拿出来是为了进行比较 // 测试1: 可变对象, 父线程初始化, 子线程先获取对象再修改对象值 // 结论1: 子线程可以通过先获取对象再修改的方式影响父线程的对象值 Map<Object, Object> map = new HashMap<>(); map.put("aa", 123); testThreadLocal.set(map); // 父线程进行初始化 Thread testThreadone = new TestThread(); // 创建子线程 testThreadone.start(); TimeUnit.MILLISECONDS.sleep(100); // 父线程睡眠, 以等待子线程执行完毕 System.out.println("main = " + testThreadLocal.get()); // 此处输出为子线程的值, 说明子线程影响父线程的对象值 System.out.println(); // 测试2: 可变对象, 父线程初始化, 子线程先获取对象再修改对象值 // 结论2: 通过重写childValue()实现子线程与父线程的互不影响 Map<Object, Object> mapTo = new HashMap<>(); mapTo.put("aa", 123); threadLocal.set(mapTo); // 父线程进行初始化 Thread testThread = new TestThreadTo(); // 创建子线程 testThread.start(); TimeUnit.MILLISECONDS.sleep(100); // 父线程睡眠, 以等待子线程执行完毕 System.out.println("main = " + threadLocal.get()); // 此处输出为父线程的值, 说明子线程与父线程已经互不影响 } private static class TestThread extends Thread { @Override public void run() { // 此处输出父线程的初始化对象值, 代表子线程确实继承了父线程的对象值 System.out.println("TestThread.before = " + testThreadLocal.get()); // 子类拿到对象并修改 testThreadLocal.get().put("aa", 456); System.out.println("testThreadLocal = " + testThreadLocal.get()); } } private static class TestThreadTo extends Thread { @Override public void run() { // 此处输出父线程的初始化对象值, 代表子线程确实继承了父线程的对象值 System.out.println("TestThreadTo.before = " + threadLocal.get()); // 子类拿到对象并修改 threadLocal.get().put("aa", 456); System.out.println("threadLocal = " + threadLocal.get()); } } private static final class InheritableThreadLocalMap<T extends Map<Object, Object>> extends InheritableThreadLocal<Map<Object, Object>> { // 重写ThreadLocal中的方法 protected Map<Object, Object> initialValue() { return new HashMap<Object, Object>(); } // 重写InheritableThreadLocal中的方法 protected Map<Object, Object> childValue(Map<Object, Object> parentValue) { if (parentValue != null) { // 返回浅拷贝, 以达到使子线程无法影响主线程的目的 return (Map<Object, Object>) ((HashMap<Object, Object>) parentValue).clone(); } else { return null; } } }}输出结果
TestThread.before = {aa=123}
testThreadLocal = {aa=456}
main = {aa=456}
TestThreadTo.before = {aa=123}
threadLocal = {aa=456}
main = {aa=123}
通过结果,我们可以看出重写childValue()方法确实可以达到使子线程与主线程互不影响的效果。
- 深入学习ThreadLocal和InheritableThreadLocal
- InheritableThreadLocal和ThreadLocal
- ThreadLocal与InheritableThreadLocal学习笔记
- ThreadLocal和InheritableThreadLocal的使用
- ThreadLocal和InheritableThreadLocal的使用
- InheritableThreadLocal类和ThreadLocal类
- ThreadLocal和InheritableThreadLocal的区别
- ThreadLocal和InheritableThreadLocal的使用
- ThreadLocal & InheritableThreadLocal
- ThreadLocal&InheritableThreadLocal解惑
- InheritableThreadLocal-- 特殊的ThreadLocal
- Java多线程之ThreadLocal和InheritableThreadLocal的使用
- ThreadLocal与InheritableThreadLocal的使用
- threadlocal与inheritableThreadLocal的区别
- ThreadLocal与InheritableThreadLocal的使用
- Android ThreadLocal及InheritableThreadLocal分析
- 深入学习ThreadLocal
- InheritableThreadLocal的认识--一种可继承的ThreadLocal
- Java中的Object-equals()方法
- 设计模式-状态模式
- mysql 创建数据库之添加外建出错问题总结
- 数据挖掘中的度量方法
- PAT考试乙级1056(C语言实现)
- 深入学习ThreadLocal和InheritableThreadLocal
- Vue2+VueRouter2+Webpack+Axios 构建项目实战2017重制版(十)打包项目并发布到子目录
- 有序链表的插入
- 聚集索引和非聚集索引(整理)
- pageContext对象
- 江城子篇-一道很模板的Splay题-洛谷P3369
- Android 线程学习
- SDIO 接口 Wifi 驱动流程分析 (AP6354)
- Android中View.setId()