vs做网站需要的插件,菏泽做网站的,网站程序和空间区别,怎么推广游戏叫别人玩一、写在开头 今天和一个之前研二的学妹聊天#xff0c;聊及她上周面试字节的情况#xff0c;着实感受到了Java后端现在找工作的压力啊#xff0c;记得在18#xff0c;19年的时候#xff0c;研究生计算机专业的学生#xff0c;背背八股文找个Java开发工作毫无问题#x…一、写在开头 今天和一个之前研二的学妹聊天聊及她上周面试字节的情况着实感受到了Java后端现在找工作的压力啊记得在1819年的时候研究生计算机专业的学生背背八股文找个Java开发工作毫无问题但现在即便你是应届生问的考题也非常的深入和细节了只会背八股没有一定的代码量和项目积累根本找不到像样的工作具体聊天内容如下 既然大厂的面试都拷问到ThreadLocal了那今天build哥就花点时间也来温习一下这个知识点吧尽可能整理的细致一点
二、ThreadLocal简介
2.1 ThreadLocal的作用
处理并发编程的时候其核心问题是当多个线程去访问共享变量时因为顺序、资源分配等原因带来了数据的不准确我们叫这种情况为线程不安全为了解决线程安全问题在Java中可以采用Lock、 synchronzed关键字等方式但这种方式对于没有持有锁的线程来说会阻塞这样以来在时间性能上就有所损失。
为了解决这个问题Java的lang包中诞生出了一个类名为 ThreadLocal见名知意它被视为线程的“本地变量”主要用来存储各线程的私有数据当多个线程访问同一个ThreadLocal变量时实际上它们访问的是各自线程本地存储的副本而不是共享变量本身。因此每个线程都可以独立地修改自己的副本而不会影响到其他线程。这种以空间换时间的方式可以大大的提升处理时间。
2.2 ThreadLocal的使用案例
上面了解了它的特性后我们来写一个小demo感受一下ThreadLocal的使用。
public class TestService implements Runnable{// SimpleDateFormat 不是线程安全的所以每个线程都要有自己独立的副本//共享变量private static final ThreadLocalSimpleDateFormat formatter ThreadLocal.withInitial(() - new SimpleDateFormat(yyyyMMdd));public static void main(String[] args) throws InterruptedException {TestService obj new TestService();//循环创建5个线程for(int i0 ; i5; i){Thread t new Thread(obj, i);Thread.sleep(new Random().nextInt(1000));t.start();}}Overridepublic void run() {System.out.println(Thread:Thread.currentThread().getName() default Formatter formatter.get().toPattern());try {Thread.sleep(new Random().nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();}//formatter pattern is changed here by thread, but it wont reflect to other threads//设置副本的值formatter.set(new SimpleDateFormat());System.out.println(Thread:Thread.currentThread().getName() formatter formatter.get().toPattern());}
}输出
Thread:0 default Formatter yyyyMMdd
Thread:1 default Formatter yyyyMMdd
Thread:2 default Formatter yyyyMMdd
Thread:1 formatter yy-M-d ah:mm
Thread:0 formatter yy-M-d ah:mm
Thread:3 default Formatter yyyyMMdd
Thread:2 formatter yy-M-d ah:mm
Thread:3 formatter yy-M-d ah:mm
Thread:4 default Formatter yyyyMMdd
Thread:4 formatter yy-M-d ah:mm从输出中可以看出虽然 Thread-0 已经改变了 formatter 的值但 Thread-1 默认格式化值与初始化值相同并没有被修改其他线程也一样这说明每个线程获取ThreadLocal变量值的时候确访问的时线程本地的副本值。
三、ThreadLocal的实现原理
我们从Thread源码入手一步步的跟进去探索ThreadLocal的实现原理。首先在Thread的源码中我们看到了这样的两句定义语句
public class Thread implements Runnable {//......//与此线程有关的ThreadLocal值。由ThreadLocal类维护ThreadLocal.ThreadLocalMap threadLocals null;//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护ThreadLocal.ThreadLocalMap inheritableThreadLocals null;//......
}threadLocals 、inheritableThreadLocals 都是ThreadLocalMap变量而这个Map我们可以看作是ThreadLocal的定制化HashMap用来存储线程本地变量的容器是一个静态内部类而这两个变量的值初始为null只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们那我们继续去看set/get方法。
【set方法解析】
public void set(T value) {//1. 获取当前线程实例对象Thread t Thread.currentThread();//2. 通过当前线程实例获取到ThreadLocalMap对象ThreadLocalMap map getMap(t);if (map ! null)//3. 如果Map不为null,则以当前ThreadLocal实例为key,值为value进行存入map.set(this, value);else//4.map为null,则新建ThreadLocalMap并存入valuecreateMap(t, value);
}在ThreadLocal的set方法中通过getMap()方法去获取当前线程的ThreadLocalMap对象并对获取到的map进行判断我们跟如到getMap方法中去发现其实里面返回的是初始化定义的threadLocals变量。
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}在threadLocals没有被调用初始化方法重新赋值的时候它为null不为null时直接set进行赋值当前ThreadLocal实例为key,值为valueset方法中会去调用createMap(t,value)进行处理我们继续跟入这个方法的源码去看看
void createMap(Thread t, T firstValue) {t.threadLocals new ThreadLocalMap(this, firstValue);
}我们可以看到在这个方法内部会去新构造一个ThreadLocalMap的实例并将value值初始化进去并赋给threadLocals。
看完了set方法的底层实现我们知道
最终变量存储的位置在ThreadLocalMap里ThreadLocal可以视为这个Map的封装无论如何最终threadLocals存储的数据都是以线程为key对应的局部变量为值得映射表因为映射表的原因确保了每个线程的局部变量都时独立的。
【get方法解析】
看完了set的源码我们继续来看看get方法的底层实现吧既然有存(set)就有取(get)get 方法提供的就是获取当前线程中 ThreadLocal 的变量值的功能
public T get() {//1. 获取当前线程的实例对象Thread t Thread.currentThread();//2. 获取当前线程的ThreadLocalMapThreadLocalMap map getMap(t);if (map ! null) {//3. 获取map中当前ThreadLocal实例为key的值的entryThreadLocalMap.Entry e map.getEntry(this);if (e ! null) {SuppressWarnings(unchecked)//4. 当前entitiy不为null的话就返回相应的值valueT result (T)e.value;return result;}}//5. 若map为null或者entry为null的话通过该方法初始化并返回该方法返回的valuereturn setInitialValue();
}我们上面提到了线程的变量值是和线程的ThreadLocal有映射关系的所以这里将当前线程的ThreadLocal作为key去map中获取值若map为null或者entry为null的话通过该方法初始化并返回该方法返回的value我们去看看setInitialValue的实现
private T setInitialValue() {T value initialValue();Thread t Thread.currentThread();ThreadLocalMap map getMap(t);if (map ! null)map.set(this, value);elsecreateMap(t, value);return value;
}
protected T initialValue() {return null;
}这个方法里的实现和set几乎一模一样这里调用了一个protected访问修饰符的方法initialValue()这个方法可以被子类重写。
我们在2.2使用案例中写道的ThreadLocal.withInitial(() - new SimpleDateFormat(yyyyMMdd));这是在Java8中的写法等价于
private static final ThreadLocalSimpleDateFormat formatter new ThreadLocalSimpleDateFormat(){Overrideprotected SimpleDateFormat initialValue(){return new SimpleDateFormat(yyyyMMdd);}
};setInitialValue 方法的目的是确保每个线程在第一次尝试访问其 ThreadLocal 变量时都有一个合适的值。
3.1 ThreadLocalMap
上面我们也说了ThreadLocalMap是ThreadLocal的静态内部类而每个线程独立的变量副本存储也是在这个Map中它是一个定制的哈希表底层维护了一个Entry 类型的数组类型的数组 table它的内部提供了set、remove、getEntry等方法。
Entry静态内部类 这个Entry又是ThreadLocalMap的一个静态内部类我们看一下它的源码
static class Entry extends WeakReferenceThreadLocal? {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal? k, Object v) {super(k);value v;}
}Entry 继承了弱引用 WeakReferenceThreadLocal?它的 value 字段用于存储与特定 ThreadLocal 对象关联的值key 为弱引用意味着当 ThreadLocal 外部强引用被置为 nullThreadLocalInstancenull时根据可达性分析ThreadLocal 实例此时没有任何一条链路引用它所以系统 GC 的时候 ThreadLocal 会被回收。这种操作看似利用垃圾回收器节省了内存空间实则存在一个风险也就是我们下面要说的内存泄露问题 只具有弱引用的对象拥有更为短暂的生命周期在GC线程扫描到它所在的内存区域的时候一旦发现了只有弱引用的对象的时候不管内存够不够用都会将其回收掉 四、ThreadLocal内存泄漏问题
4.1 内存泄漏的原因
如果非要问ThreadLocal有什么缺点的话那就是使用不当的时候会带来内存泄漏问题。 内存泄漏 是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放造成系统内存的浪费导致程序运行速度减慢甚至系统崩溃等严重后果。 根据3.1中的分析我们知道ThreadLocalMap中的使用的key是ThreadLocal的弱引用Value为强引用如果ThreadLocal没有被强引用的话key会被GC掉而value依旧存在若我们采用任何措施的前提下线程一直运行那这些value值就会一直存在过多的占用内存导致内存泄漏
4.2 如何解决内存泄漏
如何解决内存泄漏呢只需要记得在使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以了。
//ThreadLocal提供的清理方法
public void remove() {//1. 获取当前线程的ThreadLocalMapThreadLocalMap m getMap(Thread.currentThread());if (m ! null)//2. 从map中删除以当前ThreadLocal实例为key的键值对m.remove(this);
}
/*** ThreadLocalMap中的remove方法*/
private void remove(ThreadLocal? key) {Entry[] tab table;int len tab.length;int i key.ThreadLocalHashCode (len-1);for (Entry e tab[i];e ! null;e tab[i nextIndex(i, len)]) {if (e.get() key) {//将entry的key置为nulle.clear();//将该entry的value也置为nullexpungeStaleEntry(i);return;}}
}除此之外我们还可以使用Java 8引入的InheritableThreadLocal来替代ThreadLocal它可以在子线程中自动继承父线程的线程局部变量值从而避免在创建新线程时重复设置值的问题。但是同样需要注意及时清理资源以避免内存泄漏。
五、线程间局部变量传值问题
上面我们提到的Java8中引入的InheritableThreadLocal类这是实现父子线程间局部变量传值的关键 InheritableThreadLocal存在于java.lang包中是ThreadLocal的扩展它有一个特性那就是当创建一个新的线程时如果父线程中有一个 InheritableThreadLocal 变量那么子线程将会继承这个变量的值。这意味着子线程可以访问其父线程为此类变量设置的值。我们写一个小demo感受一下
public class TestService{// 创建一个 InheritableThreadLocal 变量private static final InheritableThreadLocalString inheritableThreadLocal new InheritableThreadLocal();public static void main(String[] args) {// 在主线程中设置值inheritableThreadLocal.set(这是父线程的值);System.out.println(父线程中的值: inheritableThreadLocal.get());// 创建一个子线程Thread childThread new Thread(() - {// 在子线程中尝试获取值由于使用了 InheritableThreadLocal这里会获取到父线程中设置的值System.out.println(子线程中的值: inheritableThreadLocal.get());});// 启动子线程childThread.start();// 等待子线程执行完成try {childThread.join();} catch (InterruptedException e) {e.printStackTrace();}// 主线程结束时清除值防止潜在的内存泄漏inheritableThreadLocal.remove();}
}输出
父线程中的值: 这是父线程的值
子线程中的值: 这是父线程的值输出不出所料在子线程中获取的其实是父线程设置的inheritableThreadLocal值。
5.1 父子线程局部变量传值的实现原理
我们看到上面的输出后应该思考这样的一个问题子线程是怎么拿到父线程的inheritableThreadLocal值得呢其实要从子线程的初始化开始说起在线程Thread的内部有着这样的一个初始化方法
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,// 该参数一般默认是 trueboolean inheritThreadLocals) {// 省略大部分代码Thread parent currentThread();// 复制父线程的 inheritableThreadLocals 属性实现父子线程局部变量共享if (inheritThreadLocals parent.inheritableThreadLocals ! null) {this.inheritableThreadLocals ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); }// 省略部分代码
}在这里将父线程的inheritableThreadLocals赋值了进来我们跟入createInheritedMap方法中继续解析
// 返回一个ThreadLocalMap,传值为父线程的
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);
}
//ThreadLoaclMap构建的过程中会调用该构造方法
private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable parentMap.table;int len parentTable.length;setThreshold(len);table new Entry[len];// 一个个复制父线程 ThreadLocalMap 中的数据for (int j 0; j len; j) {Entry e parentTable[j];if (e ! null) {SuppressWarnings(unchecked)ThreadLocalObject key (ThreadLocalObject) e.get();if (key ! null) {// childValue 方法调用的是 InheritableThreadLocal#childValue(T parentValue)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;}}}
}在这个构造方法中我们终于看到了InheritableThreadLocal的身影childValue()方法就是其中的一个方法用来给子线程赋父线程的inheritableThreadLocals值其实InheritableThreadLocal的源码非常非常的简单大部分的实现都取自父类ThreadLocal。
public class InheritableThreadLocalT extends ThreadLocalT {protected T childValue(T parentValue) {return parentValue;}ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}void createMap(Thread t, T firstValue) {t.inheritableThreadLocals new ThreadLocalMap(this, firstValue);}
}六、总结
OK基于学妹在字节面试的考点我们又梳理了一遍ThreadLocal这个类大家还是要好好学一学的毕竟在日后的工作中我们肯定会使用到譬如用它来保存用户登录信息这样在同一个线程中的任何地方都可以获取到登录信息用于保存事务上下文这样在同一个线程中的任何地方都可以获取到事务上下文等等。
七、结尾彩蛋
如果本篇博客对您有一定的帮助大家记得留言点赞收藏呀。原创不易转载请联系Build哥 如果您想与Build哥的关系更近一步还可以关注“JavaBuild888”在这里除了看到《Java成长计划》系列博文还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等欢迎您的加入