范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

Java并发编程的艺术ThreadLocal原理和使用

  带着BAT大厂的面试问题去理解
  请带着这些问题继续后文,会很大程度上帮助你更好地理解相关知识点。什么是ThreadLocal? 用来解决什么问题的?说说你对ThreadLocal的理解ThreadLocal是如何实现线程隔离的?为什么ThreadLocal会造成内存泄露? 如何解决还有哪些使用ThreadLocal的应用场景?ThreadLocal简介
  我们在Java 并发 - 并发理论基础总结过线程安全(是指广义上的共享资源访问安全性,因为线程隔离是通过副本保证本线程访问资源安全性,它不保证线程之间还存在共享关系的狭义上的安全性)的解决思路:互斥同步: synchronized 和 ReentrantLock非阻塞同步: CAS, AtomicXXXX无同步方案: 栈封闭,本地存储(Thread Local),可重入代码
  这个章节将详细地讲讲 本地存储(Thread Local)。官网的解释是这样的:
  This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID) 该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
  总结而言:ThreadLocal是一个将在多线程中为每一个线程创建单独的变量副本的类; 当使用ThreadLocal来维护变量时, ThreadLocal会为每个线程创建单独的变量副本, 避免因多线程操作共享变量而导致的数据不一致的情况。ThreadLocal理解
  提到ThreadLocal被提到应用最多的是session管理和数据库链接管理,这里以数据访问为例帮助你理解ThreadLocal:如下数据库管理类在单线程使用是没有任何问题的class ConnectionManager {     private static Connection connect = null;      public static Connection openConnection() {         if (connect == null) {             connect = DriverManager.getConnection();         }         return connect;     }      public static void closeConnection() {         if (connect != null)             connect.close();     } }
  很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。为了解决上述线程安全的问题,第一考虑:互斥同步
  你可能会说,将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理,比如用Synchronized或者ReentrantLock互斥锁。这里再抛出一个问题:这地方到底需不需要将connect变量进行共享?
  事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。即改后的代码可以这样:class ConnectionManager {     private Connection connect = null;      public Connection openConnection() {         if (connect == null) {             connect = DriverManager.getConnection();         }         return connect;     }      public void closeConnection() {         if (connect != null)             connect.close();     } }  class Dao {     public void insert() {         ConnectionManager connectionManager = new ConnectionManager();         Connection connection = connectionManager.openConnection();          // 使用connection进行操作          connectionManager.closeConnection();     } }
  确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不仅严重影响程序执行效率,还可能导致服务器压力巨大。这时候ThreadLocal登场了
  那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。下面就是网上出现最多的例子:import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException;  public class ConnectionManager {      private static final ThreadLocal dbConnectionLocal = new ThreadLocal() {         @Override         protected Connection initialValue() {             try {                 return DriverManager.getConnection("", "", "");             } catch (SQLException e) {                 e.printStackTrace();             }             return null;         }     };      public Connection getConnection() {         return dbConnectionLocal.get();     } }再注意下ThreadLocal的修饰符
  ThreaLocal的JDK文档中说明:ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread。如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义private static类型的ThreadLocal 实例。
  但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。ThreadLocal原理如何实现线程隔离
  主要是用到了Thread对象中的一个ThreadLocalMap类型的变量threadLocals, 负责存储当前线程的关于Connection的对象, dbConnectionLocal(以上述例子中为例) 这个变量为Key, 以新建的Connection对象为Value; 这样的话, 线程第一次读取的时候如果不存在就会调用ThreadLocal的initialValue方法创建一个Connection对象并且返回;
  具体关于为线程分配变量副本的代码如下:public T get() {     Thread t = Thread.currentThread();     ThreadLocalMap threadLocals = getMap(t);     if (threadLocals != null) {         ThreadLocalMap.Entry e = threadLocals.getEntry(this);         if (e != null) {             @SuppressWarnings("unchecked")             T result = (T)e.value;             return result;         }     }     return setInitialValue(); }首先获取当前线程对象t, 然后从线程t中获取到ThreadLocalMap的成员属性threadLocals如果当前线程的threadLocals已经初始化(即不为null) 并且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象(本例中为Connection);如果当前线程的threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象, 那么重新创建一个Connection对象, 并且添加到当前线程的threadLocals Map中,并返回如果当前线程的threadLocals属性还没有被初始化, 则重新创建一个ThreadLocalMap对象, 并且创建一个Connection对象并添加到ThreadLocalMap对象中并返回。
  如果存在则直接返回很好理解, 那么对于如何初始化的代码又是怎样的呢?private T setInitialValue() {     T value = initialValue();     Thread t = Thread.currentThread();     ThreadLocalMap map = getMap(t);     if (map != null)         map.set(this, value);     else         createMap(t, value);     return value; }首先调用我们上面写的重载过后的initialValue方法, 产生一个Connection对象继续查看当前线程的threadLocals是不是空的, 如果ThreadLocalMap已被初始化, 那么直接将产生的对象添加到ThreadLocalMap中, 如果没有初始化, 则创建并添加对象到其中;
  同时, ThreadLocal还提供了直接操作Thread对象中的threadLocals的方法public void set(T value) {     Thread t = Thread.currentThread();     ThreadLocalMap map = getMap(t);     if (map != null)         map.set(this, value);     else         createMap(t, value); }
  这样我们也可以不实现initialValue, 将初始化工作放到DBConnectionFactory的getConnection方法中:public Connection getConnection() {     Connection connection = dbConnectionLocal.get();     if (connection == null) {         try {             connection = DriverManager.getConnection("", "", "");             dbConnectionLocal.set(connection);         } catch (SQLException e) {             e.printStackTrace();         }     }     return connection; }
  那么我们看过代码之后就很清晰的知道了为什么ThreadLocal能够实现变量的多线程隔离了; 其实就是用了Map的数据结构给当前线程缓存了, 要使用的时候就从本线程的threadLocals对象中获取就可以了, key就是当前线程;
  当然了在当前线程下获取当前线程里面的Map里面的对象并操作肯定没有线程并发问题了, 当然能做到变量的线程间隔离了;
  现在我们知道了ThreadLocal到底是什么了, 又知道了如何使用ThreadLocal以及其基本实现原理了是不是就可以结束了呢? 其实还有一个问题就是ThreadLocalMap是个什么对象, 为什么要用这个对象呢?ThreadLocalMap对象是什么
  本质上来讲, 它就是一个Map, 但是这个ThreadLocalMap与我们平时见到的Map有点不一样它没有实现Map接口;它没有public的方法, 最多有一个default的构造方法, 因为这个ThreadLocalMap的方法仅仅在ThreadLocal类中调用, 属于静态内部类ThreadLocalMap的Entry实现继承了WeakReference>该方法仅仅用了一个Entry数组来存储Key, Value; Entry并不是链表形式, 而是每个bucket里面仅仅放一个Entry;
  要了解ThreadLocalMap的实现, 我们先从入口开始, 就是往该Map中添加一个值: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;     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;         }          if (k == null) {             replaceStaleEntry(key, value, i);             return;         }     }      tab[i] = new Entry(key, value);     int sz = ++size;     if (!cleanSomeSlots(i, sz) && sz >= threshold)         rehash(); }
  先进行简单的分析, 对该代码表层意思进行解读:看下当前threadLocal的在数组中的索引位置 比如: i = 2, 看 i = 2 位置上面的元素(Entry)的Key是否等于threadLocal 这个 Key, 如果等于就很好说了, 直接将该位置上面的Entry的Value替换成最新的就可以了;如果当前位置上面的 Entry 的 Key为空, 说明ThreadLocal对象已经被回收了, 那么就调用replaceStaleEntry如果清理完无用条目(ThreadLocal被回收的条目)、并且数组中的数据大小 > 阈值的时候对当前的Table进行重新哈希 所以, 该HashMap是处理冲突检测的机制是向后移位, 清除过期条目 最终找到合适的位置;
  了解完Set方法, 后面就是Get方法了: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); }
  先找到ThreadLocal的索引位置, 如果索引位置处的entry不为空并且键与threadLocal是同一个对象, 则直接返回; 否则去后面的索引位置继续查找。ThreadLocal造成内存泄露的问题
  网上有这样一个例子:import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit;  public class ThreadLocalDemo {     static class LocalVariable {         private Long[] a = new Long[1024 * 1024];     }      // (1)     final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,             new LinkedBlockingQueue<>());     // (2)     final static ThreadLocal localVariable = new ThreadLocal();      public static void main(String[] args) throws InterruptedException {         // (3)         Thread.sleep(5000 * 4);         for (int i = 0; i < 50; ++i) {             poolExecutor.execute(new Runnable() {                 public void run() {                     // (4)                     localVariable.set(new LocalVariable());                     // (5)                     System.out.println("use local varaible" + localVariable.get());                     localVariable.remove();                 }             });         }         // (6)         System.out.println("pool execute over");     } }
  如果用线程池来操作ThreadLocal 对象确实会造成内存泄露, 因为对于线程池里面不会销毁的线程, 里面总会存在着的强引用, 因为final static 修饰的 ThreadLocal 并不会释放, 而ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的LocalVariable对象也不会释放, 就造成了内存泄露; 如果LocalVariable对象不是一个大对象的话, 其实泄露的并不严重, 泄露的内存 = 核心线程数 * LocalVariable对象的大小;
  所以, 为了避免出现内存泄露的情况, ThreadLocal提供了一个清除线程中对象的方法, 即 remove, 其实内部实现就是调用 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) {             e.clear();             expungeStaleEntry(i);             return;         }     } }
  找到Key对应的Entry, 并且清除Entry的Key(ThreadLocal)置空, 随后清除过期的Entry即可避免内存泄露。再看ThreadLocal应用场景
  除了上述的数据库管理类的例子,我们再看看其它一些应用:每个线程维护了一个"序列号"
  再回想上文说的,如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义private static类型的ThreadLocal 实例。
  每个线程维护了一个"序列号"public class SerialNum {     // The next serial number to be assigned     private static int nextSerialNum = 0;      private static ThreadLocal serialNum = new ThreadLocal() {         protected synchronized Object initialValue() {             return new Integer(nextSerialNum++);         }     };      public static int get() {         return ((Integer) (serialNum.get())).intValue();     } }Session的管理
  经典的另外一个例子:private static final ThreadLocal threadSession = new ThreadLocal();      public static Session getSession() throws InfrastructureException {       Session s = (Session) threadSession.get();       try {           if (s == null) {               s = getSessionFactory().openSession();               threadSession.set(s);           }       } catch (HibernateException ex) {           throw new InfrastructureException(ex);       }       return s;   }  在线程内部创建ThreadLocal
  还有一种用法是在线程类内部创建ThreadLocal,基本步骤如下:在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。在ThreadDemo类的run()方法中,通过调用getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。public class ThreadLocalTest implements Runnable{          ThreadLocal StudentThreadLocal = new ThreadLocal();      @Override     public void run() {         String currentThreadName = Thread.currentThread().getName();         System.out.println(currentThreadName + " is running...");         Random random = new Random();         int age = random.nextInt(100);         System.out.println(currentThreadName + " is set age: "  + age);         Student Student = getStudentt(); //通过这个方法,为每个线程都独立的new一个Studentt对象,每个线程的的Studentt对象都可以设置不同的值         Student.setAge(age);         System.out.println(currentThreadName + " is first get age: " + Student.getAge());         try {             Thread.sleep(500);         } catch (InterruptedException e) {             e.printStackTrace();         }         System.out.println( currentThreadName + " is second get age: " + Student.getAge());              }          private Student getStudentt() {         Student Student = StudentThreadLocal.get();         if (null == Student) {             Student = new Student();             StudentThreadLocal.set(Student);         }         return Student;     }      public static void main(String[] args) {         ThreadLocalTest t = new ThreadLocalTest();         Thread t1 = new Thread(t,"Thread A");         Thread t2 = new Thread(t,"Thread B");         t1.start();         t2.start();     }      }  class Student{     int age;     public int getAge() {         return age;     }     public void setAge(int age) {         this.age = age;     }      }java 开发手册中推荐的 ThreadLocal
  看看阿里巴巴 java 开发手册中推荐的 ThreadLocal 的用法:import java.text.DateFormat; import java.text.SimpleDateFormat;   public class DateUtils {     public static final ThreadLocal df = new ThreadLocal(){         @Override         protected DateFormat initialValue() {             return new SimpleDateFormat("yyyy-MM-dd");         }     }; }
  然后我们再要用到 DateFormat 对象的地方,这样调用:DateUtils.df.get().format(new Date());

一年出口超40万辆,奇瑞在海外闷声发大财来源丨创业邦(IDichuangyebang)作者丨潘磊编辑丨海腰图源丨奇瑞官网近日,阿根廷夺得世界杯冠军梅西加冕球王成为热门话题,很多中国赞助商都在想尽办法增加曝光率,比如蒙牛在创立24年,搜狐获驰名商标,张朝阳虽被边缘化但还会回来!张朝阳回应搜狐被边缘化从1998年到2022年,24年风云变幻,曾经的搜狐集团是互联网三甲,一度风光无限。但近十年,门户逐渐走向下坡路,搜狐发展掉队。2016年的乌镇世界互联网大会形势严峻!美订单减少40,近35万就业岗位流失,明年经济怎么办2022年中国面临比较强的公共卫生事件冲击,严格的社会防控措施对经济造成的创伤目前已经体现了出来,这也引发了国内众多经济学家的强烈担忧。11月份,来自美国的制造业订单下滑40,全年回来了!抢单天团凯旋,订单排到明年5月,人民日报没说错当许多人和企业还在想着如何给公司带来更多订单的时候,有些企业和人们却在想着能不能抢到更多的单。虽然口罩事件给许多企业带来了巨大的危机,但同样是机遇,苏州也不亏是工业大城,单刀匹马跑二十年棉花价格历史规律总结以史为鉴,可以知兴替。今天我们来复盘一下二十年来棉花价格的走势规律,(下附二十年来的价格走势与关键时间节点标记图,大家可以对照查看)通过对历史走势的观察,我们总结出了以下几个规律1风光储的交易拥挤度历史低位,再谈新能源的2023的确定性时光总是让人猝不及防,还有最后一周,2022年就结束了。就像是村上春树讲的我们都是一瞬间变老的,这句话,当我们站在一年时光末尾的时候,感同身受。一瞬间,仿佛就是一年。当然,回首过去邮储银行增发价6。64元,溢价50,谁会参与呢?它是国有银行吗?邮储银行在我国的商业银行中间,是一个非常特殊的存在。一方面好像规模非常大,多项指标全国第一,另外一方面工农建中四大国有银行,为何没有包括邮储银行呢?最近还有一个奇怪的事情,邮储银行事关就业就医明年哪些新政将实施?快来了解新年倒计时,我们即将迎来2023年。明年起,将有哪些新的政策实施?将如何影响你我的生活?一起来看本期快问快答新年临近,工人忙着赶制各式灯笼,供应节日市场。黄晓海摄(新华社)Q明年有运动品牌这一年市场饱和,细分为王回望2022记者覃思悦编辑一白如果说大部分运动品牌在2021年营收不利是受疫情反复和供应链不足的影响,那么在2022年,背锅的应该是整体饱和的运动鞋服市场。美国市场调研机构NPD集团的一份研究砂石易矿权国企控股21亿元拿下安徽大矿,砂石储量6。5亿吨近日,池州市自然资源和规划局在池州市公共资源交易管理中心对安徽省东至县柯家村熔剂用白云岩熔剂用石灰岩矿采矿权公开挂牌出让。现安徽交控东流新材料有限公司以成交价212000万元成功竞没想到彩虹六号的新型外挂,竟然暗藏玄鸡似乎任何一款多人竞技类型的游戏,都离不开开挂这一大问题。LOL吃鸡这些游戏就不说了,就连胜负并不重要的派对游戏糖豆人,都免不了有大把玩家开挂,势必要一举拿下冠军。比赛还没开始就结束
为什么今年支原体感染的儿童这么多?每年都很多的,六岁之前孩子们很容易感染这个菌。一到冬天,各大医院的儿科门诊住院部都爆满。我感觉在幼儿园感染的几率要大一些,孩子小抵抗力差,自理能力又不是太强,幼儿园孩子多,交叉感染詹姆斯拿到4万分的可能性有多大?毫不夸张的说,四万分对于詹姆斯来说已经不是一个遥不可及的梦想了,等到他退役的那一天,你来列詹姆斯创造的那些惊为天人的记录,可能你需要费一番功夫,因为他的记录太多了,等到他退役,他会4k显示器多少寸比较好?4K显示器多少寸比较好?答4K显示器的尺寸常见的有15。6英寸27英寸(28英寸)31。5英寸(34英寸)43英寸50英寸55英寸65英寸等。至于选择多大尺寸,众说纷纭,这个要看购为什么穿着同样款式的衣服拍照,有人好看,有人不好看?拍摄人像题材中,拍摄对象服装搭配拍摄环境光影运用构图方式后期处理等,都是几个非常重要的要素,要拍出好的人像作品,这几个方面都值得我们仔细考究。为什么穿着同样款式的衣服拍照,有人好看股票长线还是短线赚钱?就问题本身来回答,股票短线和长线都能赚钱。而长短钱能不能赚到钱关键取决于你所投资的标的以及入场的时机。一般而言,我们选中基本面及未来预期都向好的公司进行价值投资,也就是长线投资。比你打算什么时候离开股市?这辈子都不会离开股市,除非傻了呆了死了,可以赚钱的地方干嘛要离开,上了30年班赚的钱没有股市里面赚的钱多,感恩有股市。离开股市是不可能的,能做到离开股市,那你就悟道了。还是借助这个一个十五岁的初中毕业生,既没有考上高中又不愿去读技校,该怎么办?十五岁初中毕业,没有考上高中,说明学习不好,不是头脑不灵活,就是学习不努力又不愿意去读技校,说明不愁吃喝不懂生活,还是躲在父母身边的大宝,根本没想过自食其力。恕我直言,初中毕业赋闲钙化灶会死人吗?为什么?平常在门诊坐诊时,经常会有一些患者拿着超声报告过来咨询医生,上面写着肝内钙化灶,什么是钙化灶啊?是不是很严重?有些还以为钙化灶就是癌症,来时双手发抖,异常紧张。面对这么多的疑问和顾头上长了头螨会怎么样呢?危害大不大,有什么办法可以去除?感谢邀请头上长了头螨会出现这4大危害,严重的情况会导致秃头,因为头螨是一种寄生在头皮上嗜脂性微小生物,人的肉眼是看不到的,虽然它长得小,但是它的繁殖能力是超级强的,危害是巨大的,毛什么传染病最厉害?昨天,我接诊了一位美女,真的是美女,特别漂亮,比起任何一个明星模特,都不差。一米七五左右的身高,身材凹凸有致。大眼睛高鼻梁,作为女人我都觉得她真漂亮。姑娘怀孕五个月,是来要求引产的丁宁为何总是喊许昕为师傅呢?你如何评价?近期,女乒队长在接受采访,在谈到最想跟谁配合打混双的问题时,丁宁笑着说到最想和我师父许昕配混双,但我们是两个左手可能会死的很惨,实现的可能性不大。我们俩的打法在男女线上有点相似,如