并发编程——ThreadLocal原理分析,Looper如何和线程关联

Handler消息机制和ThreadLocal原理 一、简介 Handler作为Android线程间通信的常用方式,主要由Handler、Looper、MessageQueue、message四个部分组成 , Handler负责线程间消息通讯的发送消息和接收处理消息 Looper用于从MessageQueue中轮询获得Message MessageQueue则是一个单线链表的用于存储Message的数据结构 Message则是线程间通信的载体 总体流程图如下 简单使用,如下子线程发送message,主线程更新UI private Handler han 阅读详情

说个特别尴尬的事情,最近刚辞职,上上周去极星面试。由于没有什么准备,加之很多东西很久之前看过都忘了,面试官问道我Looper和Thread是如何关联在一起即Looper.myLooper()如何获取到当前线程的looper的时候,我一时想不起来,竟然说了句“ThreadLocal内部维护了一个table,存储了关于线程和变量的key-value,类似map”,当时就觉得不对劲,面试官也就没有再多问,尴尬不已。不仅仅是这个,关于多线程方面的具体原理,cas,锁的原理,死锁……近几个月忘得一干二净,所以趁现在辞职在家躺着期间,打算把并发编程相关的都总结一下。


之前在我另一篇帖子《再论Handler—Handler事件分发、Handler线程切换原理解析》 中1.3部分有简单介绍Looper内的sThreadLocal 存放了当前线程绑定的Looper,这句话深入探究其实是不正确的,正确的说法是,当前线程的Looper是存放在当前线程的ThreadLocalMap中的。

  • ThreadLocal简介:
/**
 * 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).
 */

上面是它简介,ThreadLocal提供了线程本地变量,这些变量是每个线程私有的副本,线程之间互不干扰。

接下来我们通过Looper的两个方法来了解ThreadLocal原理:

  • Looper.myLooper()
  • Looper.preapare()

(Ⅰ) 获取Looper——Looper.myLooper()。

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

我们都知道这个方法,可以获取当前线程的Looper,可以看到,调用了ThreadLocal的get方法。

接下来看ThreadLocal的get方法:

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

 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

get方法主要逻辑如下:

  • 1.先获取当前执行的线程,然后获取当前线程内部的ThreadLocalMap

我们点进Thread中,看一下threadLocals是什么:

ThreadLocal.ThreadLocalMap threadLocals = null;

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

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

}

我们先不要管这个类是哪个类的内部类,这个不是重点。

  • 其内部有一个Entry的内部类,结构和HashMap的内部类Entry很像,只不过这里Key类型已经被指定为ThreadLocal了;
  • ThreadLocalMap内部为Thread维护了一个名为table的Entry[] 数组

是不是很熟悉,这就是一个妥妥的Map结构。

  • 2.以当前ThreadLocal为参数,调用ThreadLocalMap的getEntry方法。

我们再看ThreadLocalMap.Entry e = map.getEntry(this) 这行代码的实现:

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

getEntry方法其实很简单,看过HashMap原理(可以参考我的另一篇帖子《HashMap(Java8)原理解析&&HashMap扩容机制》的同学都知道,首先通过key的hash值与table数组length-1位运算得到table下标,即数组中存储的位置下标;然后从table数组中获取到Entry e;再比较Entry中存放的key是否与当前key一致后返回Entry。getEntryAfterMiss方法原理简单,不再赘述。

看到这里,我们其实已经大概明了:

通过ThreadLocal存储的变量,实际上是以ThreadLocal为key在调用者线程内ThreadLocalMap中存储的,在哪个线程通过TreadLocal存储某个变量,就存储到了哪个线程中。**


有取就必须先存,说完myLooper获取looper的方法,我们再看一下Looper是如何存进ThreadLocalMap中的:

(Ⅱ) Looper.prapare()

我们都知道,在子线程创建Looper需要调用Looper.prapare()方法为子线程初始化Looper,我们看一下这个方法

/** Initialize the current thread as a looper.
      * This gives you a chance to create handlers that then reference
      * this looper, before actually starting the loop. Be sure to call
      * {@link #loop()} after calling this method, and end it by calling
      * {@link #quit()}.
      */
    public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

这个方法作用很简单,为当前线程初始化一个Looper:

  • 1.如果当前线程(ThreadLocalMap中)已存在,就抛出异常,所以这个方法在同一线程不能连续调用;
  • 2.创建一个Looper实例调用set方法存入。

再看ThreadLocal的set方法:

 /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

前面看get方法时候,已经知道前两行作用,看后面的作用:

  • 后面如果map不为null,就以当前TreadLocal为key存储我们的Looper实例;
  • 但如果当前线程初次使用ThreadLocal,那么此处就会为null,就会执行createMap方法。

ThreadLocal的createMap方法:

	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

ThreadLocalMap构造函数:

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

看上去很简单,就是创建它的实例,并以默认容量初始化内部的table数组,然后以ThreadLocal-Looper的k-v形式存储。
我们再看ThreadLocalMap的set方法:

 	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;
            //通过key的hash值与table的lenth-1位运算得到下标
            int i = key.threadLocalHashCode & (len-1);
			//
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
				//如果已经存在当前ThradLocal为key的Entry,就替换value。
                if (k == key) {
                    e.value = value;
                    return;
                }
		
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			// 如果没有存储过当前ThreaLocal为key的Entry,就创建一个新的存储。
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
	
	/**
	* 获取下一个下标
	*/
 	private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

上面关键注释很清楚,原理很简单:

  • 1.如果已存在以当前ThreadLocal的Entry就替换value;
  • 2.如果不存在就创建新的Entry存入。

(Ⅲ)总结

好了,ThreadLocal的几个关键方法已经介绍完,原理非常简单,其他我就不展开介绍。
最后,我们需要明确几个概念:

  • ThreadLocal并不直接存储各个线程变量副本,而是代理线程内部ThreadLocalMap,换言之,各个线程变量的副本由线程内部的ThreadLocalMap存储;

通过ThreadLocal存储的变量,实际上是以ThreadLocal为key在调用者线程内ThreadLocalMap中存储的,在哪个线程通过TreadLocal存储某个变量,就存储到了哪个线程中。**

  • 通过ThreadLocalMap.Entry结构可以知道:每个ThreadLocal只能帮每个线程保存一个变量副本

多个变量可能需要多个ThreadLocal实例帮助保存,或者创建包装对象(实体类)包含多个变量


好了,关于ThreadLocal的介绍到此为止,如有不足,烦请批评指正,非常感谢。

【Android学习笔记】初探Thread、Looper和handler之间的联系 前几天写程序的时候碰到这么一个问题,当时想在子线程中Toast一段话,不废话上代码 new Thread(new Runnable() { @Override public void run() { new Handler(){ @Override ... 阅读详情

相关推荐

Android 消息机制详解(源码分析ThreadLocal、Looper、Handler、AsyncTask)

Android 消息机制 1 概述 Android 的消息机制主要是指 Handler 的运行机制以及 Handler 所附带的 MessageQueue 和 Looper 的工作过程。 Android 应用是事件驱动的,每个事件都会转化为一个系统消息,即 Message 。消息中包含了事件相关的信息以及这个消息的处理人——Handler。每个进程中都有一个默认的消息队列(Message Queue,采用单链表的形式来存储消息列表),这个消息队列维护了一个待处理的消息列表,消息循环不断地从这个队列中取出消息

lly-Rachel的博客 554

Looper如何和一个线程进行绑定,以及Android中的Can't create handler inside thread that has not called Looper.prepare()

Android客户端和服务器端数据交互的第三种方法》中Android手机客户端代码1曾经提到“第41行和第61行不可缺少,否则会出现java.lang.RuntimeException: Can’t create handler inside thread that has not called Looper.prepare()异常”,呵呵呵,难道将”第41行和第61行”代码注销掉以后真的会出现这个

wzgbgz的博客 1156

由浅入深全面剖析ThreadLocal

前言这一阵子一直在看Picasso,在看的过程中发现了很多很有意思的东西,有的是以前见过甚至用过但是没有深入关注的,有些是以前根本没有见过的——比如今天要讲的ThreadLocal。(android 6.0)正文1,ThreadLocal是什么?先看一下Android官网的文档: Implements a thread-local storage, that is, a variable for

lypeer的专栏 1万+

Handler学习笔记2—相关问题

1、一个线程有几个Handler?一个线程有几个Looper?2、Handler内存泄漏的原因是什么?为什么其他的内部类没有说过这个问题?3、主线程为什么可以直接创建使用Handler?子线程需要做什么准备?

wokang123的博客 306

microduck复刻实战:强化学习机器人从仿真训练到真机部署全攻略

强化学习是智能体通过与环境交互试错来学习最优策略的机器学习范式,在机器人控制领域,常借助物理仿真环境进行策略训练,以降低真实世界的试错成本。其核心原理是通过奖励信号引导策略网络迭代优化,PPO等算法则在稳定性和样本效率之间取得平衡。仿真训练的价值在于可批量验证控制策略,但仿真与真实硬件之间存在物理参数偏差,需通过域随机化等技术实现迁移。从四足机器人到仿生机器人,这类方案广泛应用于复杂运动控制任务。microduck作为一个典型的强化学习机器人项目,其复刻过程涉及环境搭建、奖励设计、PPO调参及Sim-to-

weixin_29061425的博客 360

Word毕业论文排版:图片和表格题注的终极指南(含自动编号技巧)

本文是Word毕业论文排版中图片和表格题注的终极指南,详细讲解了如何利用题注功能实现自动编号,告别繁琐易错的手动操作。核心内容包括创建定制标签、配置带章节的编号格式、插入交叉引用以及一键更新全文编号的技巧,旨在帮助学生构建自动化文档体系,提升排版效率和论文严谨性。

weixin_29211309的博客 1094

Hanlder

Handler常见的问题详解

人生在世 3816

LSTM cell结构的理解和计算

LSTM是传统RNN网络的扩展,其核心结构是其cell单元,网上LSTM的相关资料繁多,质量参差不齐,下面主要结合LSTM神经网络的详细推导和 Christopher Olah的blog两篇文章中的内容进行说明。主要介绍网络如何计算,为何这么算先不展开:)。前者一副图加上29个公式,简洁明了;后者娓娓道来,适合初学者。首先是LSTM cell最常见的结构图: 这是变形的版本(找不到更清晰的版本了),

songhk0209的博客 2万+

Android -理解ThreadLocal、Looper、并发

一、理解Looper:消息循环参考:android的消息处理机制(图+源码分析)——Looper,Handler,MessageAndroid消息循环是针对线程的(每个线程都可以有自己的消息队列和消息循环)。Looper用于封装了android线程中的消息循环,默认情况下一个线程是不存在消息循环(message loop)的,需要调用Looper.prepare()来给线程创建一个消息循环,调用Lo

火星男孩的分享空间 996

TEC控制器芯片 ADN8835 中文数据手册

ADN8835是一款高集成度的单芯片TEC控制器。它包括线性电压调节模块脉冲宽度调制(PWM) 电压调节模块和两个零漂移、轨到轨斩波放大器。线性控制器和PWM驱动器用于控制H桥配置中的内部功率MOSFET。通过测量热传感器反馈电压,并使用集成的运算放大器作为比例-积分-微分(PID)补偿器来调理信号,ADN8835通过TEC驱动电流,将连接至TEC模块的激光二极管或无源组件的温度稳定到编程的目标温度。

lu2289504634的博客 9532

从 SM59 到 SPROXY,打通 ABAP 后端与 ES Repository 的代理生成链路

本文探讨了在SAP PI/PO项目中配置ABAP后端系统与ES Repository连接的关键步骤,重点解决ABAP Proxy生成时无法获取接口模型的问题。文章指出: 该配置本质是建立设计时通道,使ABAP系统能通过SPROXY访问ES Repository中的接口模型并生成代理类 配置核心是在SM59创建名为SAP_PROXY_ESR的HTTP连接(类型G),需确保: 目标路径为/rep 使用具备SAP_XI_IS_SERV_USER角色的服务账号 连接测试通过后仍需在SPROXY中验证完整功能 常见陷

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 57

【Android面试题】Android Framework核心面试题——ThreadLocal的原理,以及在Looper是如何应用的?

ThreadLocal是一个创建线程局部变量的类,它的实现机制决定了这个变量的作用域是线程,其他线程访问不了,利用这个机制,可以保障一个线程只有唯一的一个ThreadLocal变量。然后,在looper中通过prepare函数的设计,确保了一个ThreadLocal 只会和一个Looper进行绑定。通过这两个方式确保了一个线程只有一个ThreadLocal变量,一个ThreadLocal变量只有一个Looper,从而形成了一一对应的关系。我整理了一套Android面试题合集,除了以上面试题,还包含【

datian1234的博客 208

Python爬虫实战:高效爬取CNKI知网文献数据的技术解析

本文将详细介绍如何使用Python最新技术构建一个高效的CNKI知网文献爬虫系统。我们将从爬虫基础知识讲起,逐步深入到高级反反爬技术、分布式爬取、智能解析等前沿内容。文章包含完整的代码实现,涵盖请求模拟、数据解析、持久化存储等关键环节,并特别针对CNKI的反爬机制提供解决方案。通过本文,读者将掌握构建专业级学术文献爬虫的全套技术栈。关键词:Python爬虫、CNKI爬取、反反爬技术、文献数据挖掘、Scrapy框架本文详细介绍了如何使用Python最新技术构建CNKI知网文献爬虫。

2201_76125261的博客 2075

一个线程可以有几个Looper?几个Handler?从Looper.prepare()来看看关于Looper的一些问题

前言 之前我有篇文章里面写到了Android的消息机制,Handler发送消息的一些原理。链接如下: 从Handler.post(Runnable r)再一次梳理Android的消息机制(以及handler的内存泄露) 在消息机制里面,有一个非常重要的东西,那就是Looper,Looper的作用主要是从消息队列里面取出消息交给Handler处理,不过不仅限于此,在这里面还有很多东西值得我们去...

梦里梦到醒不来的梦 1万+

最新Python爬虫实战(入门爬虫篇)——案例3:爬取二次元cos图片批量下载到本地(附上完整爬虫代码)

本次案例通过Python爬虫技术实现了二次元图片的批量下载。代码采用模块化设计,函数实现cookies的自动解析,简化了配置流程,涵盖“请求列表页→提取详情页URL→请求详情页→提取图片链接→保存图片”的完整流程,与豆瓣案例的核心逻辑和格式完全对齐。学习重点包括:抓包工具定位多层级数据源、正则表达式匹配复杂链接、模拟请求头绕过反爬机制、文件名安全处理及自动化文件管理。需要注意控制请求频率并遵守网站使用规范,适用于入门爬虫实践,帮助学习者掌握多层级页面爬取的核心逻辑。

努力让自己发光,对的人才能迎着光而来 14万+

ESP8266-Arduino网络编程实例-异步TCP客户端

本次实例将使用ESPAsyncTCP库实现一个简单的客户端。

视觉与物联智能 1740

Handler,Looper,MessageQueue,ThreadLocal的关联

一.前言 本以为这些东西是老生常谈,并且作为Android开发的基础入门知识,是必须熟练掌握的,但是发现每每提起Handler及Looper,ThreadLocal的原理,总是要去翻阅资料,或者查看源码再捋一遍,干脆花一点时间整理一下,等下次再忘记拿出来稍稍翻阅一下就ok了。 二.原理浅析 分析一件事情往往需要带着问题去分析,下面我们将从以下几个问题进行分析: 为什么主线程直接new Handl...

sanshan_star的博客 1068

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Android如何保证一个线程最多只能有一个Looper

1. 如何创建Looper? Looper的构造方法为private,所以不能直接使用其构造方法创建。 private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); } 要想在当前线程创建Looper,需使用Looper的prepare方法,Looper.prepare()。 如果现在要我们来实现Looper.prepare

augfun的博客 1369

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

Handler和ThreadLocal的原理解析

通过创建新线程的方式。msg.target是个啥呢,我们追到Message里面不难发现其实它就是我们发送消息的Handler,这写法是不是很聪明,当从MessageQueen中捞出Message后,我们就能直接调用Handler的dispatchMessage,然后就会走到我们的Handler的handleMessage了。Handler使用是用来进行线程间通信的,所以新开启的线程是会持有Handler引用的,如果在Activity等中创建Handler,并且是非静态内部类的形式,就有可能造成内存泄漏。

qq_27391737的博客 702

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

ThreadLocal原理及在Looper中的应用深度解析

"全局Map需要同步锁,而ThreadLocal无锁访问,在Handler高频场景下性能差异显著(实测吞吐量提升5倍)":"ThreadLocalMap随线程销毁自动清理,而全局Map需要维护复杂的生命周期"OkHttp的ConnectionPool线程绑定":"为什么不用同步的全局Map存储Looper?使用快速路径(firstGet)避免多次哈希查找。Key(ThreadLocal对象)使用。:"符合线程封闭原则,避免交叉污染"添加清除过期Entry的启发式算法"避免使用同步锁(无竞争条件)

斗锋在干嘛的博客 412

Android如何保证一个线程最多只能有一个Looper?

Thread-Specific Storage Pattern

SUN 1万+

android looper_一个线程可以有几个Looper?几个Handler?

前言之前我有篇文章里面写到了Android的消息机制,Handler发送消息的一些原理。链接如下:从Handler.post(Runnable r)再一次梳理Android的消息机制(以及handler的内存泄露)在消息机制里面,有一个非常重要的东西,那就是Looper,Looper的作用主要是从消息队列里面取出消息交给Handler处理,不过不仅限于此,在这里面还有很多东西值得我们去源码看一看:...

weixin_39788986的博客 1517

一切从Android的Handler讲起(三):Looper的唯一性——ThreadLocal

1、Looper 在线程上具备唯一性,其原因在于 ThreadLocal;2、MessageQueue 也具备线程上的唯一性,其原因在于 Looper 构造时创建了对应的 MessageQueue;3、Looper.prepare() 方法只能调用一次- - - - - Looper 的唯一性篇完 - - - - -

贵公子丶笔记 462
上一篇: 什么是虚拟内存?虚拟内存的原理
下一篇: Android性能优化(二)--CPU和内存优化工具及方法
cry kid
博客等级 码龄10年 144粉丝 24原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值