java并发之无同步方案

Wesley13
• 阅读 536

前置章节回顾:

JAVA多线程并发容易引发的问题及如何保证线程安全
    之前的章节中我们介绍了在并发时,容易引发的问题及如何保证线程安全,本章节我们主讲JAVA并发中的无同步方案: ThreadLocal

java并发之无同步方案

无同步方案:

1.可重入代码:

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

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

线程本地存储:如果一段代码所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,即是无同步也能做到避免数据争用。

1.ThreadLocal  介绍2.ThreadLocal  应用3.ThreadLocal  源码解析3.1解决 Hash 冲突4.ThreadLocal 特性5.4.ThreadLocal 内存泄露问题

1.ThreadLocal  介绍

一句话总结:

ThreadLocal 是一个存储在线程本地副本的工具类,要保证线程安全,不一定非要进行同步。同步只是保证共享数据争用时的正确性,如果一个方法本来就不涉及共享数据,那么自然无须同步。既然是本地存储的,那么就只有当前线程可以访问,自然是线程安全的

java并发之无同步方案

file

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并发之无同步方案

本文分享自微信公众号 - java宝典(java_bible)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解2016年09月02日00:00:36 \牧野(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fme.csdn.net%2Fdcrmg) 阅读数:59593
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
4个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这