1、Thread、 ThreadLocal 及 ThreadLocalMap
ThreadLocal提供了一种方式,可以让线程在操作共享变量时,复制该共享变量的一个副本到线程自己的栈空间,以后就操作这个副本空间来代替共享空间。
Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系
每个 Thread 对象中都持有一个 ThreadLocalMap 类型的成员变量,ThreadLocalMap 自身类似于是一个 Map,里面会有一个个 key value 形式的键值对,key 就是 ThreadLocal 的引用,value是我们希望 ThreadLocal 存储的内容,例如 user 对象等。
2、ThreadLocal
2.1、ThreadLocal属性
threadLocalHashCode属性决定了ThreadLocal -> value键值对保存在线程属性ThreadLocalMap 中的桶位,桶位 = threadLocalHashCode & (table.length - 1)
public class ThreadLocal<T> {/*** 线程在使用ThreadLocal.get()方法时,如果是第一次在threadLocal对象上get,会给当前线程分配一个value* 该value和ThreadLocal对象被包装成一个entry,其中key为ThreadLocal对象,value为给当前线程分配的value* 这个entry存放到当前线程threadLocals这个map中,而entry在map中的桶位跟ThreadLocal对象的threadLocalHashCode属性值有关* 桶位 = threadLocalHashCode & (table.length - 1)*/private final int threadLocalHashCode = nextHashCode();/*** 创建ThreadLocal对象时,给每个ThreadLocal对象分配Hash值的,每创建一个ThreadLocal对象* 使用nextHashCode分配一个Hash值给这个对象*/private static AtomicInteger nextHashCode =new AtomicInteger();/*** 每创建一个ThreadLocal对象,ThreadLocal.nextHashCode静态属性值会增长的大小* 0x61c88647很特殊,是斐波拉切数也叫黄金分割数,使用该数可以使hash值分布非常均匀*/private static final int HASH_INCREMENT = 0x61c88647;/*** 给新创建的ThreadLocal对象分配nextHashCode时需要调用的方法*/private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}/*** 赋值方法 一般会重写* @return*/protected T initialValue() {return null;}public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {return new SuppliedThreadLocal<>(supplier);}public ThreadLocal() {}}
2.2、ThreadLocal对外方法
2.2.1、ThreadLocal#set
保存键值对,如果当前线程ThreadLocalMap属性初始化过,直接调用ThreadLocalMap#set方法覆盖或者新放入对应的键值;如果当前线程没有初始化过ThreadLocalMap属性,则直接调用ThreadLocal#createMap新建一个ThreadLocalMap,并将当前ThreadLocal对象和对应的值存入。
/*** 修改当前线程保存的当前ThreadLocal对象的对应的value值,** @param value*/public void set(T value) {Thread t = Thread.currentThread();//获取当前线程对象的threadLocals属性ThreadLocalMap map = getMap(t);if (map != null) {//如果当前线程ThreadLocalMap属性初始化过,直接调用set方法覆盖或者新放入对应的键值map.set(this, value);} else {//如果当前线程没有初始化过ThreadLocalMap属性,则直接新建一个ThreadLocalMap,并将当前//ThreadLocal对象和对应的值存入createMap(t, value);}}
2.2.2、ThreadLocal#get
获取当前线程保存的键值对,调用ThreadLocalMap#getEntry方法获取存储的键值对,如果获取的key在ThreadLocalMap中没有或者ThreadLocalMap没有初始化,则先初始化当前线程的ThreadLocalMap属性,然后设置获取key的默认值
/***返回当前线程与当前ThreadLocal对象相关联的线程局部变量 ,该变量只有当前线程能访问到* 如果当前线程没有分配变量,则使用initialValue方法给当前线程分配一个变量* @return*/public T get() {Thread t = Thread.currentThread();//返回当前线程的ThreadLocal.ThreadLocalMap threadLocals 属性ThreadLocalMap map = getMap(t);/*** map != null 成立 说明当前线程已经初始化过ThreadLocalMap 属性了*/if (map != null) {//根据当前的ThreadLocal对象去线程的ThreadLocalMap中获取valueThreadLocalMap.Entry e = map.getEntry(this);//e != null成立说明当前线程ThreadLocalMap中保存有对应ThreadLocal对象的valueif (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;//返回对应的value值return result;}}/*** 走到这里有哪些情况?* 1、当前线程ThreadLocalMap 属性没有被初始化 需要走初始化逻辑* 2、根据当前的ThreadLocal对象去线程的ThreadLocalMap中获取value,无对应的value*/return setInitialValue();}
ThreadLocal#setInitialValue 初始化线程ThreadLocalMap,设置查询未命中的初始值
/***初始化当前线程对应ThreadLocal对象的初始值,如果当前线程的ThreadLocalMap对象为空,* 初始化当前线程的ThreadLocalMap对象,* @return*/private T setInitialValue() {//调用initialValue方法为线程对应的ThreadLocal对象生成默认的valueT value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {//如果线程内部的ThreadLocalMap属性已经初始化,则保存ThreadLocal对象 和 当前线程对应的ThreadLocal对象生成默认的valuemap.set(this, value);} else {//如果ThreadLocalMap为空则对ThreadLocalMap初始化createMap(t, value);}if (this instanceof TerminatingThreadLocal) {TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);}return value;}
ThreadLocal#initialValue设置默认初始值
/*** 赋值方法 一般会重写* @return*/protected T initialValue() {return null;}
2.2.3、ThreadLocal#remove
移除当前线程与当前ThreadLocal对象相关联的value,调用ThreadLocalMap#remove方法完成
/*** 移除当前线程与当前ThreadLocal对象相关联的value*/public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null) {//如果线程的ThreadLocalMap属性已经初始化了,直接调用ThreadLocalMap的remove方法m.remove(this);}}
3、ThreadLocalMap
3.1、ThreadLocalMap属性
ThreadLocalMap内部采用一个初始长度为16的数组保存数据,数组长度必须是2的次方,扩容阈值为散列表数组长度的2/3,扩容每次翻一倍。其向前向后访问都是循环的,第一个位置元素的上一个元素是最后一个元素,最后一个元素的上一个元素为第一个元素。
/*** ThreadLocalMap 是用来存储 线程对应的ThreadLocal的值的自定义hash Map,**/static class ThreadLocalMap {/*** Entry 结构实际上是继承了一个ThreadLocal类型的弱引用并将其作为 key*/static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}/*** 初始化ThreadLocalMap时散列表数组的长度 默认16*/private static final int INITIAL_CAPACITY = 16;/*** 散列表数组 数组长度必须是2的次方,扩容每次翻一倍*/private Entry[] table;/*** ThreadLocalMap的数据数量即散列表数组的占用情况*/private int size = 0;/*** 扩容触发阈值 初始值为len * 2 / 3* 触发之后调用rehash()方法* rehash()方法会先做一次全量的过期数据检查,把散列表中所有的过期entry全部删除* 如果移除之后当前散列表的entry数量任然达到threshold * 3 /4,就进行扩容*/private int threshold; // Default to 0/*** 设置扩容阈值为散列表数组长度的2/3*/private void setThreshold(int len) {threshold = len * 2 / 3;}/*** 后一个元素位置(循环)* 参数1:当前下标* 参数2:当前散列表数组长度** 如果i到达散列表数组最后一个元素位置下一步就变为0,访问数组的第一个元素,循环访问数组*/private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}/*** 前一个元素位置(循环访问)* 参数1:当前下标* 参数2:当前散列表数组长度* 如果i为0,则上一个元素的位置为n-1,为数组最后一个元素*/private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);}/*** 构造一个包含第一个entry的ThreadLocalMap* @param firstKey 第一个键值对的key* @param firstValue 第一个键值对的value*/ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//默认初始长度16table = new Entry[INITIAL_CAPACITY];//计算桶位int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//将第一个键值对放入散列表数组table[i] = new Entry(firstKey, firstValue);//更改size为1size = 1;//设置扩容阈值setThreshold(INITIAL_CAPACITY);}/*** 根据现有的ThreadLocalMap 构建一个新的ThreadLocalMap* @param parentMap 父ThreadLocalMap*/private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];for (Entry e : parentTable) {if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}}}
3.2、ThreadLocalMap对外方法
3.2.1 ThreadLocalMap#set
特别注意ThreadLocalMap没有采用拉链法解决hash冲突,而是采用开放地址法,如果发送hash冲突,则向后查找最靠近的桶位。首先通过ThreadLocal对象的threadLocalHashCode属性计算桶位,桶位 = threadLocalHashCode & (table.length - 1),存放键值对时可能有多种情况:
1、通过key计算出的桶位上没有数据,直接赋值
2、通过key计算出的桶位上有数据,但是键值对的key和要设置的key相同,说明此时为重新赋值
3、发生hash冲突,继续向后遍历,找到entry的key等于要放入数据的key的位置,说明此时为重新赋值
4、发生hash冲突,继续向后遍历,找到entry的key为null,过期数据的位置,此时调用ThreadLocalMap#replaceStaleEntry替换过期数据
5、发生hash冲突,继续向后遍历,找到一个没有存放entry的位置,size +1,调用ThreadLocalMap#cleanSomeSlots进行启发式清理工作,如果没有数据被清理,并且数据量已经超过了扩容阈值,则调用ThreadLocalMap#rehash进行扩容
/*** 调用set方法给当前线程添加ThreadLocal->value键值对* 调用ThreadLocal的set方法时实际调用的ThreadLocalMap的set方法* @param key* @param value*/private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;//计算桶位 即entry存放在散列表数组的下标int i = key.threadLocalHashCode & (len-1);/*** for循环从当前桶位向后遍历 如果发生hash冲突,则继续向后遍历,直到找到一个可用的位置* 1、找到一个没有存放entry的位置* 2、找到entry的key等于要放入数据的key的位置(相同key元素重新赋值)* 3、找到entry的key为null,过期数据的位置** 如果计算的桶位上没有entry则证明没有发生hash冲突,不执行while循环*/for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {//执行while循环证明发生了hash冲突或者是统一key重新赋值ThreadLocal<?> k = e.get();//当前桶位的key和要放入键值对的key相同 则证明为value重赋值if (k == key) {//value赋值 返回e.value = value;return;}//如果key为null,则证明key关联的ThreadLocal对象已经被回收了,该位置数据未过期数据if (k == null) {//替换过期数据replaceStaleEntry(key, value, i);return;}}/*** 执行到这里说明 当前位置entry为null,创建新的entry对象*/tab[i] = new Entry(key, value);//size + 1int sz = ++size;/**** 进行启发式清理工作,如果没有数据被清理,并且数据量已经超过了扩容阈值,则进行扩容*/if (!cleanSomeSlots(i, sz) && sz >= threshold){rehash();}}
3.2.2、ThreadLocalMap#getEntry
根据ThreadLocal对象从当前线程取出对应的value
/*** 获取ThreadLocal对象关联的value** ThreadLocal对象的get()方法实际调用的ThreadLocalMap的getEntry方法* @param key ThreadLocal对象* @return*/private Entry getEntry(ThreadLocal<?> key) {//计算entry的位置 获取数据对应散列表数组的下标int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];//如果对应位置的值不为空 并且对应entry的key为所查找的key 则证明该位置的entry就是要查找的entry 直接返回if (e != null && e.get() == key)return e;else{/*** 执行到这里有几种情况?* 1、当前key 查找到散列表数据对应位置数据为空* 2、发生了hash冲突,当前key查询到的entry的key和要查询的key不一致,需要向当前桶位后查询(ThreadLocalMap是通过开放地址法解决hash冲突的,并未使用拉链法)*/return getEntryAfterMiss(key, i, e);}}
调用getEntry方法时,未获取到对应entry的逻辑
/*** 通过key未查询到entry的处理逻辑* 1、通过key未查询到entry* 2、通过key查询到的entry的key和指定的key不同,发生了hash冲突* @param key ThreadLocal对象* @param i 通过查询的key计算出的桶位 即散列表数组的下标* @param e 查询出来的entry* 1、为空则说明 通过key未查询到entry* 2、不为空则说明 发生了hash冲突,通过key查询到的entry的key和指定的key不同,entry指向查出的冲突的entry* @return*/private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;//从发生hash冲突的entry循环向后访问while (e != null) {ThreadLocal<?> k = e.get();//k == key 说明找到了要查询的entryif (k == key)return e;/*** 元素的key为null(因为ThreadLocalMap的key为弱引用,如果key关联的ThreadLocal对象被GC回收了,那么key就会为null)* 说明当前位置的entry的key关联的ThreadLocal对象被GC回收了*/if (k == null){//当散列表出现过期数据了 需要做一次探测式的过期数据检查和回收expungeStaleEntry(i);}//没有找到对应的entry 继续向后循环查找elsei = nextIndex(i, len);//e为下一个元素e = tab[i];}/*** 此处有两种情况* 1、传入的e就为空,没有执行while循环,通过key未查询到entry,直接返回null* 2、传入的e不为空,通过key查询到的entry的key和指定的key不同,发生了hash冲突,执行了while循环,但是* 循环向后访问对象时,直到查询到entry为null,也没没找对应的entry(ThreadLocalMap采用开放地址法解决hash冲突,、* 一旦发生hash冲突,就会将元素放在冲突位置后的最近可用位置),则返回null*/return null;}
3.2.3、ThreadLocalMap#remove
/*** 删除key对应的entry*/private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;//计算桶位int i = key.threadLocalHashCode & (len-1);//从计算出的位置循环查找到entry为null的位置 如果找到entry的key和当前待删除的key相同 在清除该entryfor (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {//清理当前的entrye.clear();//执行一次探测式的清理expungeStaleEntry(i);return;}}}
3.3、ThreadLocalMap内部核心方法
3.3.1、ThreadLocalMap#replaceStaleEntry
插入数据时,如果key计算出的位置发现被占用了,则从当前位置向后查找,如果找到entry的key为null的位置,则会调用当前方法替换过期数据。
其中主要逻辑为: 前驱扫描 获取最前面的一个过期数据的下标(不一定最小,是循环访问的),以初始值为key计算出的桶位staleSlot向后迭代,寻找过期数据,直到碰到entry为null结束,获取第一个过期数据的下标位置。如果向后查找过程中,找到了和要查询键值对key相同的entry,证明为重新赋值操作,将键值对放到之前entry失效的位置;如果向后查找过程中并未找到key和待插入key相同的entry,说明当前set操作为添加逻辑,直接将失效数据位置重新赋值。
其中向前和向后遍历时,遇到过期数据会修改slotToExpunge的值,当遇到过期数据时或者向后查询找到了和要查询键值对key相同的entry时,都需要调用
ThreadLocalMap#expungeStaleEntry方法进行一次探测式清理,然后调用ThreadLocalMap#cleanSomeSlots方法执行一次启发式清理。

/*** 替换过期Entry(插入数据时,如果key计算出的位置发现被占用了,则从当前位置向后查找,找到一个entry为null的位置* 或者entry的key为null的位置 或者 entry的key为待插入key的位置,如果entry的key为null的位置,则会调用当前方法替换过期数据)* @param key 待插入键值对的key* @param value 待插入键值对的value* @param staleSlot key计算出的桶位即散列数组的下标*/private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;//探测式清理过期数据的下标 初始值为 key计算出的桶位即散列数组的下标staleSlotint slotToExpunge = staleSlot;/*** 前驱扫描 获取最前面的一个过期数据的下标(不一定最小,是循环访问的),赋值给slotToExpunge* 以初始值为key计算出的桶位staleSlot向前迭代,寻找过期数据,直到碰到entry为null结束* int i = prevIndex(staleSlot, len); i的初始值为key计算出的桶位的前一个位置* (e = tab[i]) != null; 循环条件是entry不为null*/for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len)){//如果当前位置为过期数据,更新探测式清理过期数据的下标为iif (e.get() == null){slotToExpunge = i;}}/*** 以初始值为key计算出的桶位staleSlot向后迭代,寻找过期数据,直到碰到entry为null结束* 向后扫描 获取第一个过期数据的下标位置赋值给slotToExpunge*/for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();/*** 如果entry的key和待插入的key一样,说明是替换逻辑* 直接将*/if (k == key) {//更新新的valuee.value = value;//将过期数据tab[staleSlot] 放到 当前循环到的位置i上tab[i] = tab[staleSlot];//将过期数据位置数据tab[staleSlot]保存为更新了的当前entrytab[staleSlot] = e;//slotToExpunge == staleSlot 成立说明一开始向前查找过期数据时,并未找到过期的entry,向后扫描时,也未发现过期数据if (slotToExpunge == staleSlot){//将探测式清理过期数据的下标修改为当前循环的下标slotToExpunge = i;}cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}/*** 条件1: k == null 说明遍历的entry是一个过期数据* 条件2:slotToExpunge == staleSlot说明前驱扫描时未发现过期数据**/if (k == null && slotToExpunge == staleSlot){//向后遍历找到过期数据了,并且前驱扫描时未发现过期数据,更新探测式清理过期数据的下标为islotToExpunge = i;}}/*** 运行到这里的条件* 向后查找过程中并未找到key和待插入key相同的entry,说明当前set操作为添加逻辑*///将原过期数据的value设置为nulltab[staleSlot].value = null;//将新插入键值对添加到staleSlot中tab[staleSlot] = new Entry(key, value);// slotToExpunge != staleSlot 成立说明 除了staleSlot以外还发现其他过期的数据了if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}
3.3.2、ThreadLocalMap#expungeStaleEntry
探测式清理主要是清理了过期的数据以及hash冲突优化
1、从staleSlot这个过期数据位置开始向后查找过期数据,直到对应位置entry为null为止,遍历过程中,如果entry的key为null,则将entry的value设置为null,将当前位置的entry设置为空,size-1
2、遍历过程中对entry的key不为null的数据,需要重新计算桶位,如果是发生hash冲突的,需要将其移到最靠近正确位置的可用位置

/*** 探测式的过期数据回收以及hash冲突优化* 1、从staleSlot这个过期数据位置开始向后查找过期数据,直到对应位置entry为null为止,遍历过程中,如果entry的key为null,* 则将entry的value设置为null,将当前位置的entry设置为空,size-1* 2、遍历过程中对entry的key不为null的数据,需要重新计算桶位,如果是发生hash冲突的,需要将其移到最靠近正确位置的可用位置** 当获取元素时发现,有entry的key为null,即散列表有过期数据时需要调用该方法* @param staleSlot entry的key为null的散列表数组下标* @return 返回从staleSlot这个过期数据位置开始向后查找最后一个不为空数据的下标*/private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 将当前位置的entry的value设置为nulltab[staleSlot].value = null;//将当前位置的entry设置为空tab[staleSlot] = null;//减少一个元素 size-1size--;// 当前遍历的entryEntry e;//当前遍历的下标int i;/*** i = nextIndex(staleSlot, len) i的初始值为staleSlot的下一个元素位置,因为staleSlot位置已经处理过了* 循环的条件是 (e = tab[i]) != null 即下一个位置的entry不为null* i = nextIndex(i, len) 向后循环遍历**/for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {//获取当前遍历entry的keyThreadLocal<?> k = e.get();//如果key为空,则证明为过期数据,当前entry的key关联的ThreadLocal对象已经被gc回收了,做回收处理if (k == null) {// 将entry的value设置为nulle.value = null;//将当前位置的entry设置为空tab[i] = null;//减少一个元素 size-1size--;} else {//key不为空 则证明数据不为空//根据key的hashcode重新计算下 桶位int h = k.threadLocalHashCode & (len - 1);/*** 如果计算的桶位和当前entry的桶位不同,则证明插入数据时发生了hash冲突,此时需要优化* 将数据移到离正确桶位更近的位置*/if (h != i) {//将当前桶位的数据设置为nulltab[i] = null;//从entry应该在的正确位置向后查找,循环条件是对应位置的entry不为null,也就是最多查询到 当前循环处理过期数据的位置 iwhile (tab[h] != null)h = nextIndex(h, len);//将数据放到离数据应该在的正确桶位最靠近的位置tab[h] = e;}}}return i;}
3.3.3、ThreadLocalMap#cleanSomeSlots
启发式清理,返回是否清理了过期数据的标识

/*** 启发式清理* @param i 启发式清理的开始位置* @param n 当前散列表数组的长度* @return*/private boolean cleanSomeSlots(int i, int n) {//启发式清理是否清除过过期数据boolean removed = false;Entry[] tab = table;int len = tab.length;do {//获取下一个位置i = nextIndex(i, len);Entry e = tab[i];//如果当前位置的数据过期了if (e != null && e.get() == null) {n = len;//启发式清理是否清除过过期数据标记为trueremoved = true;//以当前过期的位置做一次探测式清理工作i = expungeStaleEntry(i);}//循环一次 n无符号右移一位} while ( (n >>>= 1) != 0);return removed;}
3.3.4、ThreadLocalMap#cleanSomeSlots
清理所有的过期数据,如果清理后,元素数量还大于散列数组长度的3/4 则调用ThreadLocalMap#resize方法进行扩容
/*** 清理所有的过期数据,如果清理后,元素数量还大于散列数组长度的3/4 则进行扩容*/private void rehash() {//该方法遍历每个桶位进行探测式清理 执行完后当前散列表内所有过期数据都会被移除expungeStaleEntries();// 如果清理所有的过期数据后 size还大于散列数组长度的3/4 则进行扩容if (size >= threshold - threshold / 4)resize();}
3.3.5、ThreadLocalMap#resize
每次双倍扩容,创建一个双倍长度的数组循环迁移数据
/*** 双倍的扩容*/private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;int newLen = oldLen * 2;//创建了一个双倍长度的数组Entry[] newTab = new Entry[newLen];int count = 0;//循环迁移数据for (Entry e : oldTab) {if (e != null) {ThreadLocal<?> k = e.get();//k == null成立 说明为过期数据, 过期数据不用迁移if (k == null) {e.value = null; // Help the GC} else {//重新计算新的桶位int h = k.threadLocalHashCode & (newLen - 1);//hash时拿到最近的位置while (newTab[h] != null)h = nextIndex(h, newLen);newTab[h] = e;count++;}}}//更新扩容阈值setThreshold(newLen);size = count;table = newTab;}
