高并发和ThreadLocal以及内存泄漏

并发编程

首先感谢https://blog.csdn.net/iter_zc/article/details/39546405这个系列的作者。之前接触过高并发,但是都是在断断续续的接触和学习,没有一个结构化的学习。我的博文就是在看了他的讲解之后自己理解的。如果我写的不够好,大家可以去看一下大神写的,希望大家都可以提升自己的技术。

1. 并发编程的场景

java里面涉及到并发问题大多是多线程的问题,如果数据是无状态的(不会发生变化的)或者说线程封闭的,那么就不需要考虑多线程下的安全问题。如果数据必须是多个线程共享的,那么就必须要要考虑必要的解决方式。通常我们在项目里面遇见的多线程问题不是特别多,这是因为大多的数据可以满足前面两点,这也是正确的思路。我认为我们在项目里面应该尽量把变量满足无状态和线程封闭的要求,在保证效率的情况下,尽量避免线程安全问题。

2.如何避免并发问题

1.数据无状态

主要是一些只读不写的数据

2.线程封闭

线程封闭指的是数据是每个线程单独持有的,而不是共享。比如我们写web工程,其实理论上来说肯定是有线程安全的问题。但是tomcat会为每个请求创建一个线程直到请求结束。所以我们写的内部逻辑都是线程封闭的。但是在逻辑内部,如果还是存在多线程那么就可以考虑是否可以线程封闭。
#解决方式 ThreadLocal类
源码解读:ThreadLocal类最主要的三个方法就是set get remove

1.set()

 public void set(T value) {<br>
 //调用JNI 获取当前线程<br/>
  Thread t = Thread.currentThread();<br>
//下面介绍
        ThreadLocalMap map = getMap(t);<br>
        if (map != null)<br>
            map.set(this, value);<br>
        else
            createMap(t, value);
    }
//这个值是当前线程持有的一个变量
     ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }  
    

可以看出来ThreadLocalMap是当前线程持有的一个变量 继续点进去

 ThreadLocal.ThreadLocalMap threadLocals = null;<br/>
//这个类是ThreadLocal类的内部类
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

上面的代码可以看出这个ThreadLocalMap这个类维护了一个Entry的内部类,Entry这个类继承了 WeakReference<> 这个类,WeakReference在java里面就是代表弱引用,每次JVM在进行GC的时候都会清除这个ThreadLocal<?>类的对象(为什么使用这种引用的原因后面讲)。这个类维护了一个key value类型的映射,key就是ThreadLocal对象。value就是实际的值。而ThreadLocalMap这个类内部有一个Entry数组来保存多个Entry对象。

接下来看map.set方法 在当前已经存在threadLocal的情况下

 private void set(ThreadLocal<?> key, Object value) {
             //获取数组
            Entry[] tab = table;
            //获取数组长度
            int len = tab.length;
            //hreadLocalHashCode 是ThreadLocal类里面通过ActomicInteger来原子性记录生成的下一个对象的哈希值
            //下面这段代码就是通过散列值来解决map的冲突问题,并且定位这个entry在数组的位置
            //https://blog.csdn.net/y4x5M0nivSrJaY3X92c/article/details/81124944这篇文章对散列算法和ThrreadLocal里面哈 希//算法的介绍
            int i = key.threadLocalHashCode & (len-1);
            //循环定位  查看是否已经存在冲突,如果已经冲突,那么找下一个为空的位置
            for (Entry e = tab[i];e != null; e = tab[i = nextIndex(i, len)])   {
                ThreadLocal<?> k = e.get();
                //如果这个值已经存在 那么采取替换的方式
                if (k == key) {
                    e.value = value;
                    return;
             }
               //如果这个位置还没有使用,那么把key,value放在相应位置
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //如果已经存在冲突,那么找到可以存放的位置 i 并新建entry并把长度加一
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //如果没有被回收的无用的key,并且长度大于临界值的时候,那么采取扩容
            //cleanSomeSlots  这个方法会把key值为null的Entry删除
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
</code>
createMap()  第一次set的时候
<code>
//创建map 并把它赋值给当前线程 
 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
</code>

2.get()

  public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        
        //找到对应的Entry类 首先按照 key.threadLocalHashCode & (len-1(和set采用相同的算法))快速定位到位置,
        //如果不对,那么再调用 getEntryAfterMiss(key, i, e) 方法循环遍历寻找
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果不存在 那么调用下面的方法
        return setInitialValue();
    }
    private T setInitialValue() {
       //返回值为null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //再次验证 是否存在map
        if (map != null)
            map.set(this, value);
        else
        //这个方法上面介绍过 就是创建新的map,并且以当前threadlocal为key value为null创建entry,作为数组的第一个元素
            createMap(t, value);
        return value;
    }
</code>

3.remove()

  private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            
            int len = tab.length;
            //定位 运用hash散列算法
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                //清除引用 this.referent = null;
                    e.clear();
                    //清除数组里面其他的被回收的key值
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

3.Threadlocal的内存泄露问题

内存泄漏和内存溢出的区别,前者是已经没有作用的对象没有得到回收。后者是JVM需要的内存过大。可以见内存泄漏是诱导内存溢出的一个原因。
首先看一张引用图
key
Key是弱引用对象,每次GC的时候都会回收这个对象。(JAVA里面四种引用大家可以网上查一下),这样一旦发生GC,那么key值将会被回收,此时value因为还有强引用,所以没办法回收。但是因为key的回收,此时这个entry就没有任何存在的意义,但是Thread那条引用链导致这个entry没办法被回收。这样就会导致内存泄露的问题。上面我介绍的remove方法就可以手动清除当前entry,他的做法就是把当前的entry设置为null。所以我们再使用这个方法的时候一定要记得remove().防止内存泄露。
既然使用弱引用存在内存泄露的问题,那么为什么没有使用强引用呢。我认为首先使用强引用的话,那么即使threadLocal对象被回收,那么他的静态内部类(缺省修饰符,只能包内调用)map还是没办法去清理entry对象,同样会导致内存泄漏的发生。并且很难去修复。
如果使用弱引用,即使key值被回收,调用remove方法就可以去掉entry对象。并且在调用set时候都会在某种情况下触发清除entry数组里面entry的key为空(已经被回收)的对象的清除工作。注:set会在发生冲突的情况下触发。

4.使用的代码以及简单验证

public class ThreadLocalTest {
    /**
     * 初始值设置为0
     */
     
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    /**
     * 对比上面  number不是线程独占的
     */  private static   int number = 0;
    private static    class  TestLocal implements  Runnable{
        public void run() {
            try {
                int newValue = threadLocal.get()+1;
                threadLocal.set(newValue);
                number++;
                System.out.println("number的值"+number);
                System.out.println("threadlocal的值为"+threadLocal.get());
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                threadLocal.remove();
            }
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new TestLocal()).start();
        }
    }

}
打印结果
number的值2
number的值2
threadlocal的值为1
threadlocal的值为1
number的值3
threadlocal的值为1
number的值4
threadlocal的值为1
number的值6
number的值5
threadlocal的值为1
number的值7
threadlocal的值为1
threadlocal的值为1
number的值8
threadlocal的值为1
number的值9
threadlocal的值为1
number的值10
threadlocal的值为1

由此可以证明threadLocal是线程私有的。

5.应用

实际业务我还没有使用过,因为我的业务暂时还没有用到。但是我了解到spring里面的事务就是通过threadLocal实现的。大家可以看一下https://blog.csdn.net/zdp072/article/details/39214867 里面讲的很清晰,代码逻辑也非常好。

感言

上面有总结不对的地方的话,欢迎大家批评指正。
源码有很多我们可以借鉴的地方,比如数组里面如果想快速定位到某个值所在的下表怎么办?除了使用java提供的原生方法外,可以借鉴这里

  private static AtomicInteger nextHashCode =    new AtomicInteger();
  private final int threadLocalHashCode = nextHashCode();
  //https://www.cnblogs.com/ilellen/p/4135266.html  介绍了这个神奇的值
 final int HASH_INCREMENT = 0x61c88647;
 private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    //定位的方法  
     int i = key.threadLocalHashCode & (len-1);

还有就是适当的使用弱引用防止内存溢出,同时也要考虑内存泄漏的可能性。

猜你喜欢

转载自blog.csdn.net/qq_30055391/article/details/84789175