ThreadLocal

ThreadLocal提供线程私有变量,这些变量和普通的变量不同,因为每个线程通过get或set方法持有它们独自的变量并且单独初始化变量副本。ThreadLocal的对象一般应该用private static来修饰从而关联上一个线程的状态。
每个线程持有引用来关联线程本地变量副本只要这个线程还活着并且ThreadLocal对象可以访问。在线程结束后线程本地变量的副本会被GC回收(除非存在其它能关联到这些变量的引用)

1、构造

我们通常这样来构造它:

private static ThreadLocal<Integer> a = new ThreadLocal<Integer>();

源码中它只有一个构造器:

public ThreadLocal() {
}

构造完对象后我们会往里面塞值,比如:
a.set(1);
这样,它会调用set方法:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
} 
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

执行流程如下:
1、获取属于当前线程的ThreadLocalMap 的对象map
2、若map不为空就将当前线程插入map里
3、为空,就创建一个ThreadLocalMap对象,将ThreadLocal作为参数key传进去

一开始ThreadLocalMap 肯定为空,所以一定会去构造一个ThreadLocalMap 对象,然后往里塞值。在这里ThreadLocalMap 是ThreadLocal的一个内部类,它是实现set和get方法的核心,下面着重去看它:
然后它会调用以下构造器,传入的是作为key值的当前线程私有的ThreadLocal对象和value值:

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;
        setThreshold(INITIAL_CAPACITY);
    }

1、创建了一个Entry类型的数组,初始值默认为 INITIAL_CAPACITY=16
2、和hashmap里的数组定位一样,用了除留余数法算出当前线程和值在Entry数组里的位置
3、创建一个Entry对象,用将属于当前线程的ThreadLocal作为firstKey,并将其插入Entry数组里
4、size 数组中的元素个数为1,并设置阈值确保最坏装载因子为len*2/3

这里调用了Enrty类并创建了一个数组对象和普通类对象,来看看这个Entry类:

static class Entry extends WeakReference<ThreadLocal<?>> {
        /** 和当前线程相关联的值 */
        Object value;
        /*构造器,赋值*/
        Entry(ThreadLocal<?> k, Object v) {
        /*继承父类WeakReference构造器,将ThreadLocal对象设置为弱引用*/
            super(k);
            value = v;
        }
    }

官方对它的解释:Entry类继承了WeakReference,使用了它的主要引用域作为key值(通常是ThreadLocal对象)。空key(entry.get()== null)意味着这个key再也不能被引用,所以这个entry可以从表中被清理出去。
ThreadLocalMap是一个只适合维持线程本地值而定制的hash表。为了帮助处理大量或长时间的使用,这个表使用了WeakReferences类型的数据来作为key。

2、set方法

上面解释了若初始状态ThreadLocalMap为空的插值场景,如果ThreadLocalMap不为空呢?

Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
map.set(this, value);

getMap:获取属于当前线程的ThreadLocalMap ,然后调用set进行插值
下面是ThreadLocalMap中set方法:

   private void set(ThreadLocal<?> key, Object value) {
        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;
            }
            /*替换位置上key和value*/
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

1、和上面一样计算出这个版本ThreadLocal在ThreadLocalMap数组中的位置i,然后进行插值
2、若位置i上的key与当前key相等则用新值替换旧值,然后退出循环;若key为空则替换,然后退出循环;否则就一直使用线性探测法往后搜,然后重复之前的操作
3、最后size+1,并且调用cleanSomeSlots方法判断,若之后的元素被清理过则不需要rehash;若之后的元素没有被清理,并且size要大于阈值,则进行一次再hash

private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

线性探测法,若i + 1 >len则将i置0,由此操作可确定ThreadLocalMap是一个环相对的,有以下代码:

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

上面是往后探测,这里往前

3、get方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

1、获取属于当前线程的ThreadLocalMap
2、若ThreadLocalMap 为空则调用setInitialValue方法创建一个ThreadLocalMap对象并将value设置为null然后返回;或者ThreadLocalMap 不为空但getEntry方法调用值为空,就将当前key对应的value设置为null然后返回。这里setInitialValue方法和set方法相类似,不再赘述

3、否则返回取得的值

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对象,比如ThreadLocal< Integer > ,ThreadLocal< Long>,ThreadLocal< Date>等,但是同一线程的所有版本ThreadLocal对象和相对应的值都放在同一个ThreadLocalMap里,所以不同版本调用threadLocalHashCode 算出来的hash值是不同的,对应的在同一个ThreadLocalMap中存放的位置也不同
1、算出所在位置
2、若位置上有值并且两者是同一版本的ThreadLocal对象,返回这个值
3、否则返回 getEntryAfterMiss方法的结果

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

传入的参数:作为key的ThreadLocal对象,getEntry返回的位置i,位置i中的元素k
1、重新尝试 getEntry方法,若成功则返回
2、若k中ThreadLocal对象为null,则调用expungeStaleEntry方法进行清理,然后在此方法中找到相应的位置i值并返回,然后将值插入到此位置
3、否则使用线性探测法往后找到相应的值并返回,然后将值插入到此位置

4、清理

public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

调用ThreadLocalMap的remove方法进行删除

 private void remove(ThreadLocal<?> key) {
        Entry[] tab = table;
        int len = tab.length;
        /*算出位置*/
        int i = key.threadLocalHashCode & (len-1);
         /*对位置i上的元素做清理*/
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }
    public void clear() {
    this.referent = null;
}

1、算出位置
2、调用clear方法,clear方法是Reference类的方法,将ThreadLocal对象的引用置为null
3、使用expungeStaleEntry方法对其清理

 private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        /*将此位置的value置空,并将此位置的元素整体置空*/
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;
        Entry e;
        int i;
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
        /*将此位置的value置空,并将此位置的元素整体置空*/
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;
           /*在我们找到空位之前一直rehash*/
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }

这个方法传入的参数是ThreadLocal对象在环形数组中的位置staleSlot
1、将位置i上的元素的value置为null,并将这个元素置为null,size-1
2、一个循环,从位置i+1开始搜索,只要相应位置上有值,即key和value有一者不为null,则执行第3或4步
3、判断key值(也就是线程的ThreadLocal对象):若key为空,就执行第1步操作,然后继续循环
3、key不为null则rehash,重新计算线程的ThreadLocal对象在数组中的位置,直到我们找到一个空位,进入第4步
4、若即key和value都为null,返回此空位i

set方法会调用的清理方法

private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            i = nextIndex(i, len);
            Entry e = tab[i];
            if (e != null && e.get() == null) {
                n = len;
                removed = true;
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }

这个方法会在新元素被添加进数组时调用,传入的参数为:位置i和表长n
1、置标记位为false
2、从位置i+1开始往后搜,若找到一个位置,此位置上key值为空,则置标记位为true,调用 expungeStaleEntry 方法进行清理
3、每次调用expungeStaleEntry 方法成功size就会减一,每次循环都会将改变的值右移1位并付给长度n,直到n=0才会退出循环

5、rehash

private void rehash() {
        expungeStaleEntries();
        if (size >= threshold - threshold / 4)
            resize();
    }
private void expungeStaleEntries() {
        Entry[] tab = table;
        int len = tab.length;
        for (int j = 0; j < len; j++) {
            Entry e = tab[j];
            if (e != null && e.get() == null)
                expungeStaleEntry(j);
        }
    }

以数组长度为循环周期,将所有陈旧的值(key==null)进行一次处理 ,调用了expungeStaleEntry方法
扩容:

private void resize() {
/*双倍容量的数组*/
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;
        Entry[] newTab = new Entry[newLen];
        int count = 0;
/*清理陈旧的元素*/
        for (int j = 0; j < oldLen; ++j) {
            Entry e = oldTab[j];
            if (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null; // Help the GC
                } else {
/*定位*/
                    int h = k.threadLocalHashCode & (newLen - 1);
                    while (newTab[h] != null)
                        h = nextIndex(h, newLen);
                    newTab[h] = e;
                    count++;
                }
            }
        }
        setThreshold(newLen);
        size = count;
        table = newTab;
    }

扩容,将容量变为以前的两倍,并将陈旧的元素置空然后在新数组中重定位

基本用法:
一、简单的塞值、取值

public class ThreadLocalTest extends Thread{
	private final static ThreadLocal threadLocal = new ThreadLocal();
	
	private final static ReentrantLock lock = new ReentrantLock();
	
	static int i = 0;
	
	public void run() {
		lock.lock();
		synchronized(this) {
			for(int j=1;j<=40000;j++) {
				i = i+j;
				threadLocal.set(i);
				System.out.println(threadLocal.get());
			}
		}
		lock.unlock();
	}
	
	public static void main(String[] args) throws InterruptedException {
		ThreadLocalTest test1 = new ThreadLocalTest();
		ThreadLocalTest test2 = new ThreadLocalTest();
		test1.start();
		test2.start();
		test1.join();
		test2.join();
	}
}

我们在这里开启了三个线程:主线程、副线程一、副线程二,接着我们首先调用了主线程的join方法将主线程阻塞,然后我们测试时就可以不受到主线程的影响了。
然后我们要对两个线程的run方法进行加锁,一面出现共享数据出问题的情况。
接下来调用threadLocal的set和get方法向里面塞值和取值

二、hibernate中的ThreadLocal:

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

这样,每个线程都会创建一个属于自己的session,然后就可以对自己的session做操作了。当然,每个线程使用的肯定是属于它们自己ThreadLocal中的session值,也不可能取到别的线程的ThreadLocal或者session,因此各个线程的session相互不会干扰
(此处参考博客 https://www.iteye.com/topic/103804)

三、数据库连接

  private static final ThreadLocal data = new ThreadLocal();  
  Connection conn = null;
    public static Connection getConnection()  {  
        conn = (Connection) data .get();  
        try {  
            if (conn == null) {  
                conn = DriverManager.getConnection(URL,USERNAME,PASSWORD);
                data.set(conn);  
            }  
        } catch (Exception e) {  
            e.printStackTrace();
        }  
        return conn;  
    }  

每个线程都会创建一个属于自己的connection对象,各个线程的connection相互不会干扰。即threadlocal让同一个线程使用同一个conn,从而保证事务。

四、ReentrantReadWriteLock中的应用

ReentrantReadWriteLock的内部类Sync中的一些属性或方法:

//HoldCounter的一个对象
private transient HoldCounter cachedHoldCounter;
//此类为每个线程都有的
static final class HoldCounter {
           //代表每个线程的计数值(加锁lock次数)
            int count = 0;
            //线程ID
            final long tid = getThreadId(Thread.currentThread());
        }
//ThreadLocalHoldCounter的对象
private transient ThreadLocalHoldCounter readHolds;
//此类继承了ThreadLocal类,并重写了initialValue
//即倘若使用get取值为0,就会先将HoldCounter对象设为值,然后返回此对象
static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

//下面是加读锁的部分方法
HoldCounter rh = cachedHoldCounter;
//若此线程之前没有获取锁或因为并发问题导致线程ID与当前线程ID不一致
    if (rh == null || rh.tid != getThreadId(current))
    //先设置初值(调用get方法后会会调用以上initialValue方法取设置初值)
       cachedHoldCounter = rh = readHolds.get();
    else if (rh.count == 0)
       readHolds.set(rh);
    //计数值加一
    rh.count++;

//下面是释放读锁的部分方法
HoldCounter rh = cachedHoldCounter;
//若此线程之前没有获取锁或因为并发问题导致线程ID与当前线程ID不一致
    if (rh == null || rh.tid != getThreadId(current))
    //设置初值
        rh = readHolds.get();
    //获取当前线程的计数值
    int count = rh.count;
    //若计数值小于1,即当前线程未尺有锁
    if (count <= 1) {
        //删除当前线程的HoldCounter 属性
        readHolds.remove();
            if (count <= 0)
               throw unmatchedUnlockException();
            }
    //计数值减一
    --rh.count;

其实ReentrantReadWriteLock对ThreadLocal的使用也是简单的get和set操作,再读锁方面,ThreadLocal为每个线程提供了一个计数值count,加一次读锁,计数值加一;释放一次,计数值减一;并且通过这个每个线程都持有的、并且互不相干的count值来和AQS中的原子值state相互配合来达到对线程加锁/释放锁的目的

以下片段摘自网络:
针对内存泄漏问题:
我们在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。
请确保在我们完成对线程的ThreadLocal对象进行操作后立即删除此对象,并且最好在将线程返回到线程池之前执行remove操作。 最佳做法是使用remove()而不是set(null) ,因为这将导致WeakReference立即被删除,并与值一起被删除。
web容器(如tomcat)一般都是使用线程池处理用户到请求, 此时用ThreadLocal要特别注意内存泄漏的问题, 一个请求结束了,处理它的线程也结束,但此时这个线程并没有死掉,它只是归还到了线程池中,这时候应该清理掉属于它的ThreadLocal信息.所以我们最好在使用线程的ThreadLocal对象后立即将其释放(即调用remove()方法)从而避免内存泄漏。

总结:
每个线程(Thread类)内部都有一个属于自己的ThreadLocalMap对象,里面存放着键(当前ThreadLocal对象)值(属于自己的值)对;若此时有一个共享值,我们可以通过此对象来给需要访问此共享值的线程们每人拷贝一份此对象的副本,从而达到共享对象线程私有、并且每个线程对各自的共享对象副本互不干扰(也有线程安全的意思在里面)的目的。并且我们也要注意它的内存泄漏问题。

内容概要:本文详细介绍了一种基于Simulink的表贴式永磁同步电机(SPMSM)有限控制集模型预测电流控制(FCS-MPCC)仿真系统。通过构建PMSM数学模型、坐标变换、MPC控制器、SVPWM调制等模块,实现了对电机定子电流的高精度跟踪控制,具备快速动态响应和低稳态误差的特点。文中提供了完整的仿真建模步骤、关键参数设置、核心MATLAB函数代码及仿真结果分析,涵盖转速、电流、转矩和三相电流波形,验证了MPC控制策略在动态性能、稳态精度和抗负载扰动方面的优越性,并提出了参数自整定、加权代价函数、模型预测转矩控制和弱磁扩速等优化方向。; 适合人群:自动化、电气工程及其相关专业本科生、研究生,以及从事电机控制算法研究与仿真的工程技术人员;具备一定的电机原理、自动控制理论和Simulink仿真基础者更佳; 使用场景及目标:①用于永磁同步电机模型预测控制的教学演示、课程设计或毕业设计项目;②作为电机先进控制算法(如MPC、MPTC)的仿真验证平台;③支撑科研中对控制性能优化(如动态响应、抗干扰能力)的研究需求; 阅读建议:建议读者结合Simulink环境动手搭建模型,深入理解各模块间的信号流向与控制逻辑,重点掌握预测模型构建、代价函数设计与开关状态选择机制,并可通过修改电机参数或控制策略进行拓展实验,以增强实践与创新能力。
根据原作 https://pan.quark.cn/s/23d6270309e5 的源码改编 湖北省黄石市2021年中考数学试卷所包含的知识点广泛涉及了中学数学的基础领域,涵盖了实数、科学记数法、分式方程、几何体的三视图、立体几何、概率统计以及代数方程等多个方面。 接下来将对每道试题所关联的知识点进行深入剖析:1. 实数与倒数的定义:该题目旨在检验学生对倒数概念的掌握程度,即一个数a的倒数表达为1/a,因此-7的倒数可表示为-1/7。 2. 科学记数法的运用:科学记数法是一种表示极大或极小数字的方法,其形式为a×10^n,其中1≤|a|<10,n为整数。 此题要求学生运用科学记数法表示一个天文单位的距离,将1.4960亿千米转换为1.4960×10^8千米。 3. 分式方程的求解方法:考察学生解决包含分母的方程的能力,题目要求找出满足方程3/(2x-1)=1的x,需通过消除分母的方式转化为整式方程进行解答。 4. 三视图的辨认:该题目测试学生对于几何体三视图(主视图、左视图、俯视图)的认识,需要识别出具有两个相同视图而另一个不同的几何体。 5. 立体几何与表面积的计算:题目要求学生计算由直角三角形旋转形成的圆锥的表面积,要求学生对圆锥的底面积和侧面积公式有所了解并加以运用。 6. 统计学的基础概念:题目涉及众数、平均数、极差和中位数的定义,要求学生根据提供的数据信息选择恰当的统计量。 7. 方程的整数解求解:考察学生在实际问题中进行数学建模的能力,通过建立方程来计算在特定条件下帐篷的搭建方案数量。 8. 三角学的实际应用:题目通过在直角三角形中运用三角函数来求解特定线段的长度。 利用正弦定理求解AD的长度是解答该问题的关键。 9. 几何变换的应用:题目要求学生运用三角板的旋转来求解特定点的...
Python基于改进粒子群IPSO与LSTM的短期电力负荷预测研究内容概要:本文围绕“Python基于改进粒子群IPSO与LSTM的短期电力负荷预测研究”展开,提出了一种结合改进粒子群优化算法(IPSO)与长短期记忆网络(LSTM)的混合预测模型。通过IPSO算法优化LSTM网络的关键参数(如学习率、隐层节点数等),有效提升了模型在短期电力负荷预测中的精度与收敛速度。文中详细阐述了IPSO算法的改进策略(如引入自适应惯性权重、变异机制等),增强了全局搜索能力与避免早熟收敛,并利用实际电力负荷数据进行实验验证,结果表明该IPSO-LSTM模型相较于传统LSTM、PSO-LSTM等方法在预测准确性(如MAE、RMSE指标)方面表现更优。研究为电力系统调度、能源管理提供了高精度的负荷预测技术支持。; 适合人群:具备一定Python编程基础、熟悉基本机器学习算法的高校研究生、科研人员及电力系统相关领域的技术人员,尤其适合从事负荷预测、智能优化算法应用研究的专业人士。; 使用场景及目标:①应用于短期电力负荷预测,提升电网调度的精确性与稳定性;②为优化算法(如粒子群算法)与深度学习模型(如LSTM)的融合应用提供实践案例;③可用于学术研究、毕业论文复现或电力企业智能化改造的技术参考。; 阅读建议:建议读者结合文中提到的IPSO与LSTM原理进行理论学习,重点关注参数优化机制的设计思路,并动手复现实验部分,通过对比不同模型的预测结果加深理解。同时可拓展尝试将该方法应用于其他时序预测场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值