关于java:java并发之无同步方案ThreadLocal

37次阅读

共计 7295 个字符,预计需要花费 19 分钟才能阅读完成。

JAVA 多线程并发容易引发的问题及如何保障线程平安
之前的章节中咱们介绍了在并发时, 容易引发的问题及如何保障线程平安, 本章节咱们主讲 JAVA 并发中的无同步计划: ThreadLocal

无同步计划:

1. 可重入代码:

可重入代码:能够在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回之后,原来的程序不会呈现任何的谬误。可重入代码有一些公共的特色,例如不依赖存储在堆上的数据和专用的系统资源、用到的状态量都由参数传入、不调用非可重入的办法等。简而言之:如果一个办法,它的返回后果是能够预测的,只有输出了雷同的数据,就能返回雷同的后果,那它就满足可重入性的要求,当然也就是线程平安的。

2. 线程本地存储(ThreadLocal):

线程本地存储:如果一段代码所须要的数据必须与其余代码共享,那就看看这些共享数据的代码是否能保障在同一个线程中执行?如果能保障,咱们就能够把共享数据的可见范畴限度在同一个线程之内,这样,即是无同步也能做到防止数据争用。

[TOC]

1.ThreadLocal 介绍

一句话总结:

ThreadLocal 是一个存储在线程本地正本的工具类, 要保障线程平安,不肯定非要进行同步。同步只是保障共享数据争用时的正确性,如果一个办法原本就不波及共享数据,那么天然毋庸同步。既然是本地存储的,那么就只有以后线程能够拜访,天然是线程平安的

2.ThreadLocal 利用

ThreadLocal 的罕用办法:

public class ThreadLocal<T> {public T get() {}
    public void set(T value) {}
    public void remove() {}
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {}}

阐明:

  • get – 用于获取 ThreadLocal 在以后线程中保留的变量正本。
  • set – 用于设置以后线程中变量的正本。
  • remove – 用于删除以后线程中变量的正本。如果此线程局部变量随后被以后线程读取,则其值将通过调用其 initialValue 办法从新初始化,除非其值由两头线程中的以后线程设置。这可能会导致以后线程中屡次调用 initialValue 办法。
  • initialValue – 为 ThreadLocal 设置默认的 get 初始值,须要重写 initialValue 办法。

用法:

ThreadLocal<String> threadLocal = new ThreadLocal<>();//ThreadLocal 对象
threadLocal.set("java 宝典");// 存储内容
String str = threadLocal.get();// 获取内容

实例:

public class ThreadLocalTest {public static void main(String[] args) {ThreadLocal<Boolean> threadLocal = new ThreadLocal<>();
        threadLocal.set(false);// 存数据
        printCurrentThread(threadLocal.get());
      
        new Thread(new Runnable() {
            @Override
            public void run() {threadLocal.set(true);// 存数据
                printCurrentThread(threadLocal.get());
            }
        }, "test1").start();// 线程 test1
      
        new Thread(new Runnable() {
            @Override
            public void run() {threadLocal.set(true);// 存数据
                printCurrentThread(threadLocal.get());
            }
        }, "test2").start();// 线程 test2}

    private static void printCurrentThread(boolean b) {System.out.println(Thread.currentThread().getName() + ":\t" + b);// 打印出线程名与传的 boolean 值
    }
}

//result:
//main:    false
//test1:    true
//test2:    true

3.ThreadLocal 源码解析

ThreadLocal 做为数据存储类,那么关键点在于 setget办法。上面代码比拟多, 解说次要在正文内.

public void set(T value) {Thread t = Thread.currentThread();// 获取以后线程
  ThreadLocalMap map = getMap(t);// 获取线程的 ThreadLocalMap
  if (map != null)
    map.set(this, value);// 给 map 设置值,键为以后的 ThreadLocal,值为传入的 value
  else
    createMap(t, value);
}

/**
* getMap 办法
*/
ThreadLocalMap getMap(Thread t) {return t.threadLocals;// 返回传入的线程的 ThreadLocalMap}

/**
* createMap 办法
*/
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);// 创立新的 ThreadLocalMap 并将值传入将线程的 threadLocals 设置为这个 ThreadLocalMap
}
public T get() {Thread t = Thread.currentThread();// 获取以后线程
  ThreadLocalMap map = getMap(t);// 获取以后线程的 map
  if (map != null) {// 如果 map 不为空
    ThreadLocalMap.Entry e = map.getEntry(this);// 获取 Entry
    if (e != null) {//Entry 不为空
      @SuppressWarnings("unchecked")
      T result = (T)e.value;// 获取 Entry 的值
      return result;// 返回获取的值
    }
  }
  return setInitialValue();}

/**
* setInitialValue 办法
*/
private T setInitialValue() {T value = initialValue();// 初始值
  Thread t = Thread.currentThread();// 获取以后线程
  ThreadLocalMap map = getMap(t);// 获取以后线程的 map
  if (map != null)//map 不为空设置默认的值,也就是 null
    map.set(this, value);
  else
    createMap(t, value);//map 为空新建一个 map 在存储默认的值
  return value;// 返回默认值
}
/**
* initialValue 办法
*/
protected T initialValue() {return null;}

剖析:在调用 set 办法时获取以后线程,通过获取以后线程的 ThreadLocalMap,在 map 不为空的时候将值存储进去,如果 map 为空那么新建一个 ThreadLocalMap 并设置给 Thread 后存储传入的数据。

通过这部分源码能够看出为什么 ThreadLocal 只能操作本人线程里的数据,因为这里跟它线程的 ThreadLocalMap 有关系,再来剖析 ThreadLocalMap

// 存储数据的构造,并且是弱援用
static class Entry extends WeakReference<ThreadLocal<?>> {
  /** 与 ThreadLocal 关联的值 */
  Object value;

  Entry(ThreadLocal<?> k, Object v) {super(k);
    value = v;
  }
}

//table 的初始容量,必须是 2 的幂
private static final int INITIAL_CAPACITY = 16;

//table 用于存储数据,长度必须是 2 的幂
private Entry[] table;

//table 中存在的数据的条目数
private int size = 0;

// 阀值,用于扩容
private int threshold; // Default to 0

// 阀值设置为以后传入的值的 2 / 3 倍
private void setThreshold(int len) {threshold = len * 2 / 3;}

// 下一个值
private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);
}

// 上一个值
private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);
}

// 构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];// 初始化数组
  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  table[i] = new Entry(firstKey, firstValue);// 数据存储进去
  size = 1;// 数组外面的数据条目数量设置为 1
  setThreshold(INITIAL_CAPACITY);// 这是阀值
}
private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at
  // least as common to use set() to create new entries as
  // it is to replace existing ones, in which case, a fast
  // path would fail more often than not.

  Entry[] tab = table;
  int len = tab.length;
  // 依据 threadLocalHashCode 进行一个位运算(取模)失去索引 i
  int i = key.threadLocalHashCode & (len-1);
 
  for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();// 获取以后下标 Entry 的值
    // 如果获取的 ThreadLocal 雷同间接替换 e 的值 *1*
    if (k == key) {
      e.value = value;
      return;
    }
    // 如果 Entry key 对应的 k 为为 null 那么清空所有 key 为 null 的数据 *2*
    if (k == null) {replaceStaleEntry(key, value, i);
      return;
    }
  }
  // 如果上述都不满足,间接增加 *3*
  tab[i] = new Entry(key, value);
  int sz = ++size;
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();}
// 哈希值
private final int threadLocalHashCode = nextHashCode();

private static final int HASH_INCREMENT = 0x61c88647;
// 返回下一个哈希值
private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}

3.1 解决 Hash 抵触

ThreadLocalMap 尽管是相似 Map 构造的数据结构,但它并没有实现 Map 接口。它不反对 Map 接口中的 next 办法,这意味着 ThreadLocalMap 中解决 Hash 抵触的形式并非 拉链表 形式。

实际上,ThreadLocalMap 采纳线性探测的形式来解决 Hash 抵触。所谓线性探测,就是依据初始 key 的 hashcode 值确定元素在 table 数组中的地位,如果发现这个地位上曾经被其余的 key 值占用,则利用固定的算法寻找肯定步长的下个地位,顺次判断,直至找到可能寄存的地位。

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);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;
  int len = tab.length;

  while (e != null) {ThreadLocal<?> k = e.get();
    if (k == key)
      return e;
    if (k == null)
      expungeStaleEntry(i);
    else
      i = nextIndex(i, len);
    e = tab[i];
  }
  return null;
}
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());// 获取以后线程的 ThreadLocalMap
  if (m != null)// 如果 ThreadLocalMap 不为空
    m.remove(this);// 调用 ThreadLocalMap 的 remove 办法
}

/**
* ThreadLocalMap#remove
*/
private void remove(ThreadLocal<?> key) {Entry[] tab = table;
  int len = tab.length;//table 的长度
  int i = key.threadLocalHashCode & (len-1);// 应用哈希值取模(求余操作)for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();
      expungeStaleEntry(i);
      return;
    }
  }
}

/**
* ThreadLocalMap.Entry#clear
*/
public void clear() {this.referent = null;// 值设置为 null}

/**
* ThreadLocal#expungeStaleEntry
*/
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;
  int len = tab.length;

  // expunge entry at staleSlot
  tab[staleSlot].value = null;
  tab[staleSlot] = null;
  size--;

  // Rehash until we encounter null
  Entry e;
  int i;
  for (i = nextIndex(staleSlot, len);
       (e = tab[i]) != null;
       i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();
    if (k == null) {
      e.value = null;
      tab[i] = null;
      size--;
    } else {int h = k.threadLocalHashCode & (len - 1);
      if (h != i) {tab[i] = null;

        // Unlike Knuth 6.4 Algorithm R, we must scan until
        // null because multiple entries could have been stale.
        while (tab[h] != null)
          h = nextIndex(h, len);
        tab[h] = e;
      }
    }
  }
  return i;
}

4.ThreadLocal 个性

ThreadLocal 和 Synchronized 都是为了解决多线程中雷同变量的拜访抵触问题,不同的点是

  • Synchronized 是通过线程期待,就义工夫来解决拜访抵触
  • ThreadLocal 是通过每个线程独自一份存储空间,就义空间来解决抵触,并且相比于 Synchronized,ThreadLocal 具备线程隔离的成果,只有在线程内能力获取到对应的值,线程外则不能拜访到想要的值。

正因为 ThreadLocal 的线程隔离个性,使他的利用场景相对来说更为非凡一些。在 android 中 Looper、ActivityThread 以及 AMS 中都用到了 ThreadLocal。当某些数据是以线程为作用域并且不同线程具备不同的数据正本的时候,就能够思考采纳 ThreadLocal。

5.4.ThreadLocal 内存泄露问题

ThreadLocalMapEntry 继承了 WeakReference,所以它的 key(ThreadLocal 对象)是弱援用,而 value(变量正本)是强援用

  • 如果 ThreadLocal 对象没有内部强援用来援用它,那么 ThreadLocal 对象会在下次 GC 时被回收。
  • 此时,Entry 中的 key 曾经被回收,然而 value 因为是强援用不会被垃圾收集器回收。如果创立 ThreadLocal 的线程始终继续运行,那么 value 就会始终得不到回收,产生 内存泄露

那么如何防止内存透露呢?
办法就是:应用 ThreadLocalset 办法后,显示的调用 remove 办法

ThreadLocal<String> threadLocal = new ThreadLocal();
try {threadLocal.set("xxx");
    // ...
} finally {threadLocal.remove();
}

关注公众号:java 宝典

正文完
 0