头图

How ThreadLocal achieves isolation between threads and why ThreadLocal is often prone to memory overflow. With these two questions, find the answer in the source code.
Let's start with setting the value first, and see ThreadLocal.set() how to realize the value preservation.

 public void set(T value) {
        Thread t = Thread.currentThread();
        //获取线程私有属性 threadLocals
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
  • threadLocals: The internal property of the thread Thread object. This property is null by default. It seems that it is initialized by ThreadLocal.set.
    Don't care how ThreadLocalMap is implemented, only treat it as a container of type Map. At first, I didn't want to understand that the variable used by the thread is not an Object, but a container. Why not directly ThreadLocal set val directly to Thread.threadLocals when setting the value, but assign a value to a container. On second thought, a Thread + a ThreadLocal can only save one val, but a Thread can correspond to multiple ThreadLocals, and a thread object attribute may be shared by multiple ThreadLocals, which is almost limited by a Thread and a ThreadLocal.
    Let's first look at createMap how to initialize and how to save the value.

     void createMap(Thread t, T firstValue) {
          t.threadLocals = new ThreadLocalMap(this, firstValue);
      }
  • ThreadLocalMap : ThreadLocal is an inner class whose name looks like a Map implementation class. In fact, it has nothing to do with Map and does not implement the Map interface. Internally, Entry (similar to Map key-value objects) array is used to store data, and Hash algorithm is used to calculate subscripts. If there is a hash conflict, how to solve it? This low-profile Entry does not have such black technology as a linked list or a red-black tree.

     static class ThreadLocalMap {
    
          /**
           *  使用弱引用包装ThreadLocal 作为map Key 
           *  在某些情况下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(ThreadLocal<?> firstKey, Object firstValue) {
              table = new Entry[INITIAL_CAPACITY];
             //使用hashCode 计算下标
            //这里使用hashCode 跟我们普通对象不一样,通过自增长计算出来
              int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
              table[i] = new Entry(firstKey, firstValue);
              size = 1;
              setThreshold(INITIAL_CAPACITY);
          }
  • WeakReference : Weakly referenced objects have shorter lifetimes. In the process of scanning the memory area under the jurisdiction of the garbage collector thread, once it finds an object with only weak references, its memory will be reclaimed regardless of whether the current memory space is sufficient or not.
    Therefore, once the key of gc ThreadLocalMap appears, it will be recycled, resulting in that the ThreadLocal setting value cannot be deleted, and the object backlog is too large, resulting in memory overflow. Now the second question is answered because the map key will be released by gc, so the value cannot be deleted, so the val must be released manually after use.

     private void set(ThreadLocal<?> key, Object value) {
              Entry[] tab = table;
              int len = tab.length;
              int i = key.threadLocalHashCode & (len-1);
            // 循环里面处理hash冲突情况
              for (Entry e = tab[i];
                   e != null;
                   e = tab[i = nextIndex(i, len)]) {
                  ThreadLocal<?> k = e.get();
    
                  if (k == key) { //相等直接覆盖
                      e.value = value;
                      return;
                  }
    
                  if (k == null) { // key 已经被gc
                     //这个i 不会变,只能向后找一位
                     //这里会继续向后找,直到找到符合条件位置,并且将key被gc  value 也回收掉,
                    //这个只有在一定条件下才生效
                      replaceStaleEntry(key, value, i);
                      return;
                  }
              }
              // 当hash 冲突时,会一直向后找,直到有空位置
              tab[i] = new Entry(key, value);
              int sz = ++size;
              //在位置i 后面搜寻是否有key 回收情况,则删除数组位置,返回true 
              // 当有删除,不需要判断扩容情况了,一个新增对应删除,容量都没有增加
              if (!cleanSomeSlots(i, sz) && sz >= threshold)
                  rehash(); //扩容数组
          }

    When there is a hash conflict, just move the subscript backward to find a free position. As written in the set method comment, set does not support fast set, which conflicts with finding empty positions by traversing the array backwards.
    ThreadLocalMap actually has a mechanism to detect that the key is empty, and delete the position in the array. This mechanism can only be triggered under certain circumstances. First, the recovered key must be found after the newly added key.
    ThreadLocal.get how to get the value

     public T get() {
          Thread t = Thread.currentThread();
          // 调用get 并不会初始化threadLocals
          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;
              }
          }
          //没有取到值,会返回null
          return setInitialValue();
      }

    Let's see how the value is returned inside ThreadLocalMap

     private Entry getEntry(ThreadLocal<?> key) {
              int i = key.threadLocalHashCode & (table.length - 1);
              Entry e = table[i];
              //通过计算下标就找到
              if (e != null && e.get() == key)
                  return e;
              else
                  return getEntryAfterMiss(key, i, e);
          }
    
          //向后查找符合要去key
          private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
              Entry[] tab = table;
              int len = tab.length;
    
              while (e != null) { //遇到null 就停下来
                  ThreadLocal<?> k = e.get();
                  if (k == key)
                      return e;
                  if (k == null)
                       // k 已经被gc 了
                      //在数组中删除这个位置,这样可以帮助value 回收了
                      expungeStaleEntry(i);
                  else
                      i = nextIndex(i, len);
                  e = tab[i];
              }
              return null;
          }

    Here, get is not found at one time, it will be matched by backward traversal, which is quite different from HashMap. Insertion and search efficiency are between N.
    The first two doubts have now been resolved.
    I don’t know if you and I are curious about why the weak reference of ThreadLocalMap will not be dropped by gc when it is used, so that the return value cannot be obtained when the value is passed in, why should the weak reference be used to wrap the Key, and how to weigh the pros and cons.
    make a small example

     public void  print(){
          WeakReference<Object> reference = getReference();
          System.gc();
          Object o = reference.get();
          System.out.println(o);
      }
    
      public WeakReference<Object> getReference(){
          WeakReference<Object> reference = new WeakReference(new Object());
          return reference;
      }

    The print method will output null, which is where I am puzzled. Why does ThreadLocal not appear above in the use of weak references. After executing gc, the reference is reclaimed and the key is empty, and the value cannot be obtained.
    I found the answer on the Internet, and I will understand it after using an example to verify

     @Test
      public void sidi() throws InterruptedException {
          getLocal();
          System.gc();
          TimeUnit.SECONDS.sleep(1);
          Thread thread = Thread.currentThread();
          System.out.println(thread); // 在这里打断点,观察thread对象里的ThreadLocalMap数据
    
      }
    
      private Local getLocal() {
          Local local = new Local();
          System.out.println(local.get());
          return local;
      }
    
      private static class Local {
          private ThreadLocal<String> local = ThreadLocal.withInitial(() -> "ssssssssss");
    
          public String get() {
              return local.get();
          }
    
          public void set(String str) {
              local.set(str);
          }

    Take a look at the debug result below
    shenyifeng.github.io.png
    Now make a small change
    jxa7IH.png
    Why is ThreadLocal not recycled by gc after there is a return value, in fact, it has something to do with strong references. The current root object is the local object, which holds ThreadLocal<String> local. Although the local is wrapped by a weak reference and may be used by gc, it is also associated with the strong reference of the current local. The object is still reachable and will not be garbage collected. Lose. When the current method does not hold the local, the local internal local is not referenced by any object, the strong reference does not affect ThreadLocal, and the weak reference will definitely be deleted when gc is executed. Summary When gc is executed, weak references will always be garbage collected, but if the weakly referenced object is held by strong references at the same time, the scope of the strong reference will override the weak reference, and the object will not be used until the strong reference is reachable. recycled. Therefore, we usually do not worry about the deletion of weak references when using ThreadLocal. We must hold its object reference when operating ThreadLocal. Strong reference ensures that the object will not be recycled in the code that currently holds the object.

vpfEAH.png
Looking at the classic picture above, the solid line represents a strong reference, and the dashed line represents a weak reference. When the thread stack holds ThreadLocal, it will not be gc as the Entry key. When the ThreadLocalRef reference is invalid, ThreadLocal will be recycled in the next gc. When holding a ThreadLocal object reference chain, ThreadLocal weak references will not be recycled.
The last question, why does ThreadLocal exist as a weak reference as the internal key of ThreadLocalMap. We know that ThreadLocal, as a tool class for multi-threaded operation of its own private variables, does not hold any thread variables, but only encapsulates the specific implementation for the convenience of users. The object life cycle of ThreadLocal itself is very short, and it can be recycled when it is used up. Imagine that ThreadLocal exists as an Entry key, and the thread reference chain will become Thread->ThreadLocalMap->ThreadLocal, and the ThreadLoca object will not be recycled until the thread object is not released. If we don't release it manually, is it the same as the Entry key being deleted, too many objects may lead to memory leaks. As a tool class, ThreadLocal does not need to be bound with Thread. Setting a weak reference to wrap it can help the garbage collector to recycle it after the object scope disappears. Even if you don't manually release the set value, you don't need to worry too much about memory leaks. When adding new values or expanding the array, you will check if there is a key being gc, and release the val.


神易风
106 声望52 粉丝

alert("hello world")