Java 并发编程:线程变量 ThreadLocal
大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 029 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。
–
在并发编程中,线程安全性始终是开发者关注的重点。为了避免多个线程对同一共享变量的竞争,通常需要复杂的同步机制。然而,
ThreadLocal提供了一种更为简洁的解决方案,它通过为每个线程提供独立的变量副本,避免了线程间的共享状态,极大简化了并发编程中的数据管理。本文将探讨ThreadLocal的使用方法、底层实现原理,以及其在实际开发中的应用场景和潜在的内存泄漏问题。通过对ThreadLocal的深入理解,读者将能够更有效地管理线程中的数据,提高并发程序的安全性与性能。
文章目录
- 1、ThreadLocal 简介
- 2、ThreadLocal 的使用
- 2.1、创建方式
- 2.2、常用方法
- 2.3、Demo
- 3、ThreadLocal 原理
- 3.1、ThreadLocal 原理概述
- 3.2、ThreadLocalMap
- 3.3、ThreadLocal 相关源码解析
- 4、ThreadLocal 内存泄漏问题
- 4.1、ThreadLocal 内存泄漏问题发生的原因
- 4.2、为什么使用弱引用
- 4.3、ThreadLocal 最佳实践
- 5、Thread 相关知识点
- 5.1、关于 ThreadLocal 和 Synchronized 的区别
- 5.2、关于 ThreadLocalMap 中的 Hash 冲突处理
1、ThreadLocal 简介
ThreadLocal 即线程变量,是 Java 提供的用于实现线程本地变量的工具类。每个线程可以通过 ThreadLocal 对象访问其专属的变量,避免了多线程环境下变量共享导致的数据不一致问题。
通常情况下,我们创建的成员变量都是线程不安全的。因为他可能被多个线程同时修改,此变量对于多个线程之间彼此并不独立,是共享变量。而 ThreadLocal 中填充的变量属于当前线程,该变量对其他线程而言是隔离的。
Ps:ThreadLocal 很容易让人望文生义,想当然地认为是一个 “本地线程”。其实,
ThreadLocal并不是一个 Thread,而是 Thread 的局部变量,也许把它命名为 ThreadLocalVariable 更容易让人理解一些。
ThreadLocal 为变量在每个线程中都创建了一个属于当前 Thread 的副本,且该副本只能由当前 Thread 使用,其它 Thread 不可访问,因此也就不存在多线程间共享的问题了。
ThreadLocal 变量通常被 private static 修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
在适用场景上 ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
2、ThreadLocal 的使用
2.1、创建方式
通过 ThreadLocal 的构造方法创建:
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
通过带初始值的工厂方法创建:
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
2.2、常用方法
set(T value)– 设置线程本地变量的内容。get()– 获取线程本地变量的内容。remove()– 移除线程本地变量(值变为 null)。Ps:在线程池的线程复用场景中在线程执行完毕时一定要调用 remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
2.3、Demo
既然 ThreadLocal 的作用是每一个线程创建一个副本,我们使用一个例子来验证一下:
public static void main(String[] args) {// 新建一个ThreadLocalThreadLocal<String> local = new ThreadLocal<>();// 新建一个随机数类Random random = new Random();// 使用 java8 的 Stream 新建 5 个线程IntStream.range(0, 5).forEach(a -> new Thread(() -> {// 为每一个线程设置相应的 local 值local.set(a + " " + random.nextInt(10));System.out.println("线程和local值分别是 " + local.get());try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}).start());}
/*线程和local值分别是 0 4线程和local值分别是 2 8线程和local值分别是 3 9线程和local值分别是 1 4线程和local值分别是 4 4
*/
从结果我们可以看到,每一个线程都有各自的 local 值,我们设置了一个休眠时间,就是为了另外一个线程也能够及时的读取当前的 local 值。
3、ThreadLocal 原理
3.1、ThreadLocal 原理概述
那么如何究竟是如何实现在每个线程里面保存一份单独的本地变量呢?
实际上线程在 Java 中就是一个 Thread 类的实例对象!而 Thread 的实例对象中实例成员字段的内容肯定是这个对象独有的,所以我们也可以将保存 ThreadLocal 线程本地变量作为一个 Thread 类的成员字段,这个成员字段就是:threadLocals。
/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
每个 Thread 维护着一个 ThreadLocalMap 的引用,而 ThreadLocalMap 又是 ThreadLocal 的内部类,用 Entry 来进行存储,ThreadLocal 创建的副本是存储在自己的 threadLocals 中的,也就是自己的 ThreadLocalMap:
-
每个
Thread维护着一个ThreadLocalMap的引用。 -
ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储 -
ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。 -
ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在 map 中 -
在进行 get 之前,必须先 set,否则会报空指针异常,当然也可以初始化一个,但是必须重写 initialValue() 方法。
-
ThreadLocal本身并不存储值,它只是作为一个 key 来让线程从ThreadLocalMap获取 value。
3.2、ThreadLocalMap
ThreadLocalMap 是归 Thread 类所有的。它的引用在 Thread 类里,这也证实了一个问题:ThreadLocalMap 类内部为什么有 Entry 数组,而不是 Entry 对象?
因为你业务代码能 new 好多个 ThreadLocal 对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap 是同一个,而不是多个,不管你 new 几次 ThreadLocal,ThreadLocalMap 在一个线程里就一个,因为再说一次,ThreadLocalMap 的引用是在 Thread 里的,所以它里面的 Entry 数组存放的是一个线程里你 new 出来的多个 ThreadLocal 对象。
static class ThreadLocalMap {/*** The entries in this hash map extend WeakReference, using* its main ref field as the key (which is always a* ThreadLocal object). Note that null keys (i.e. entry.get()* == null) mean that the key is no longer referenced, so the* entry can be expunged from table. Such entries are referred to* as "stale entries" in the code that follows.*/static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}/*** The initial capacity -- MUST be a power of two.*/private static final int INITIAL_CAPACITY = 16;/*** The table, resized as necessary.* table.length MUST always be a power of two.*/private Entry[] table;......}
3.3、ThreadLocal 相关源码解析
ThreadLocal#set 方法的源码:
public void set(T value) {// 获取当前线程Thread t = Thread.currentThread();// 获取当前线程的threadLocals字段ThreadLocalMap map = getMap(t);// 判断线程的threadLocals是否初始化了if (map != null) {map.set(this, value);} else {// 没有则创建一个ThreadLocalMap对象进行初始化createMap(t, value);}
}
ThreadLocal#createMap 方法的源码:
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal.ThreadLocalMap#set 方法源码:
/**
* 往map中设置ThreadLocal的关联关系
* set中没有使用像get方法中的快速选择的方法,因为在set中创建新条目和替换旧条目的内容一样常见,
* 在替换的情况下快速路径通常会失败(对官方注释的翻译)
*/
private void set(ThreadLocal<?> key, Object value) {// map中就是使用Entry[]数据保留所有的entry实例Entry[] tab = table;int len = tab.length;// 返回下一个哈希码,哈希码的产生过程与神奇的0x61c88647的数字有关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#get 方法的源码:
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {// 获取ThreadLocal对应保留在Map中的Entry对象ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")// 获取ThreadLocal对象对应的值T result = (T)e.value;return result;}}// map还没有初始化时创建map对象,并设置null,同时返回nullreturn setInitialValue();
}
ThreadLocal#remove 方法的源码:
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());// 键在直接移除if (m != null) {m.remove(this);}
}
4、ThreadLocal 内存泄漏问题
4.1、ThreadLocal 内存泄漏问题发生的原因
ThreadLocal 自身并不储存值,而是作为 一个 key 来让线程从 ThreadLocal 获取 value。而 Entry 是中的 key 是弱引用(Entry extends WeakReference<ThreadLocal<?>>),如果一个 ThreadLocal 没有外部强引用来引用它,那么系统 GC 的时候,这个 ThreadLocal 势必会被回收。
并且,作为 ThreadLocalMap 的 key,ThreadLocal 被回收后,ThreadLocalMap 就会存在 key 为 null,但 value 不为 null 的 Entry(其实,在预防 ThreadLocal 内存泄漏问题上,Java 也做了一些努力:Java 在 Thread 中维护了 ThreadLocalMap,所以 ThreadLocalMap 的生命周期和 Thread(当前线程)一样长。并且,在 ThreadLocal 中,进行 get,set 操作的时候会清除 Map 里所有 key 为 null 的 value。)
但是,若当前线程一直不结束,可能是作为线程池中的一员,线程结束后不被销毁,或者分配(当前线程又创建了 ThreadLocal 对象)使用了又不再调用 get/set 方法,就可能引发内存泄漏。
其次,就算线程结束了,操作系统在回收线程或进程的时候不是一定杀死线程或进程的,在繁忙的时候,只会清除线程或进程数据的操作,重复使用线程或进程(线程 id 可能不变导致内存泄漏)。因此,key 弱引用并不是导致内存泄漏的原因,而是因为 ThreadLocalMap 的生命周期与当前线程一样长,并且没有手动删除对应 value。
4.2、为什么使用弱引用
通过对上述问题的分析我们可以发现,ThreadLocal 内存泄漏的一个主要原因就是 Entry 是中的 key 是弱引用,那这就有一个问题值得思考:为什么使用弱引用而不是强引用?
较为官方的说法是:为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
下面我们分两种情况讨论:
- key 使用强引用:引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄漏。
- key 使用弱引用**:**引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 的时候会被清除。
比较两种情况,我们可以发现:
由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set,get,remove 的时候会被清除。
因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。
4.3、ThreadLocal 最佳实践
综合上面的分析,我们可以理解 ThreadLocal 内存泄漏的前因后果,那么怎么避免内存泄漏呢?
每次使用完 ThreadLocal,都调用它的 remove() 方法,清除数据。
在使用线程池的情况下,没有及时清理 ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用 ThreadLocal 就跟加锁完要解锁一样,用完就清理。
5、Thread 相关知识点
5.1、关于 ThreadLocal 和 Synchronized 的区别
相同点:ThreadLocal 和 Synchronized 都是为了解决多线程中访问相同变量的冲突问题。
不同点:
- ThreadLocal:以空间换时间,为每个线程提供一个变量副本,消耗较多的内存,但是多个线程可以同时访问该变量而且相互不会影响。
- Synchronized:以时间换空间,多个线程访问的是同一个变量,但是当多个线程同时访问该变量时,需要抢占锁,并且等待获取锁的线程释放锁,会消耗较多的时间。
5.2、关于 ThreadLocalMap 中的 Hash 冲突处理
ThreadLocalMap 作为一个 HashMap 和 java.util.HashMap 的实现是不同的。对于 java.util.HashMap 使用的是链表法来处理冲突。
但是,对于 ThreadLocalMap,它使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放。具体来说:
- 当插入一个键值对时,如果当前索引位置已经被占用,则继续探测下一个位置,直到找到一个空闲的位置为止。
- 这种方法的优点是实现简单,缺点是当发生冲突时可能会导致探测时间较长,特别是在负载因子较高的情况下。
通过对比可以看出,ThreadLocalMap 的线性探测法更适合于线程本地变量的存储,因为在多数情况下,ThreadLocalMap 的负载因子较低,冲突较少,线性探测法的性能影响较小。
相关文章:
Java 并发编程:线程变量 ThreadLocal
大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 029 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进…...
【OpenHarmony4.1 之 U-Boot 2024.07源码深度解析】018 - init_sequence_f 各函数源码分析(二)
【OpenHarmony4.1 之 U-Boot 2024.07源码深度解析】018 - init_sequence_f 各函数源码分析(二) 一、arch_cpu_init二、arch_cpu_init系列文章汇总:《【OpenHarmony4.1 之 U-Boot 源码深度解析】000 - 文章链接汇总》 本文链接:《【OpenHarmony4.1 之 U-Boot 2024.07源码深度…...
LVS原理——详细介绍
目录 介绍 lvs简介 LVS作用 LVS 的优势与不足 LVS概念与相关术语 LVS的3种工作模式 LVS调度算法 LVS-dr模式 LVS-tun模式 ipvsadm工具使用 实验 nat模式集群部署 实验环境 webserver1配置 webserver2配置 lvs配置 dr模式集群部署 实验环境 router 效果呈现…...
MYSQL 5.7.36 等保 建设记录
文章目录 前言一、开启审计日志1.1 查看当前状态1.2 开启方式1.3 查看开启后状态 二、密码有效期2.1 查看当前状态2.2 开启方式2.3 查看开启后状态 三、密码复杂度3.1 查看当前状态3.2 开启方式3.3 查看开启后状态 四、连接控制4.1 查看当前状态4.2 开启方式4.3 查看开启后状态…...
fatal: unable to access ‘https://github.com/xxxxx
ubuntu中git克隆项目异常 git clone https://github.com/xxx Cloning into ‘xxx’… fatal: unable to access ‘https://github.com/xxx/xx.git/’: Could not resolve host: github.com 解决办法使用命令: git config --global http.proxy git config --global…...
从零开始的CPP(38)——递归与动态规划
leetcode46 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1: 输入:nums [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]示例 2: 输入&#…...
从战略到系统架构:信息系统设计的全面解析
在当今数字化时代,信息系统已成为企业运营、管理和创新的核心驱动力。信息系统设计的重要性不仅关乎企业的技术实现,更直接影响到企业的战略执行和市场竞争能力。本文将从战略视角出发,深入探讨信息系统设计的全过程,包括从战略制…...
GEE调用中国(China Land Cover Dataset,简称CLCD)1990-2022年30米分辨率的土地分类数据
博客推荐 GEE土地分类:中国30米年度土地覆盖产品annual China Land Cover Dataset, CLCD(面积提取)_30米土地利用数据gee-CSDN博客 简介 中国陆地覆盖数据集(China Land Cover Dataset,简称CLCD)是一个用…...
三十八、大数据技术之Kafka(1)
🌻🌻 目录 一、Kafka 概述1.1 定义1.2 消息队列1.2.1 消息队列内部实现原理1.2.2 传统消息队列的应用场景1.2.3 消息队列的两种模式 1.3 Kafka 基础架构 二、 Kafka 快速入门2.1 安装前的准备2.2 安装部署2.2.1 集群规划2.2.2 单节点或集群部署2.2.3 集群…...
将 Tcpdump 输出内容重定向到 Wireshark
在 Linux 系统中使用 Tcpdump 抓包后分析数据包不是很方便。 通常 Wireshark 比 tcpdump 更容易分析应用层协议。 一般的做法是在远程主机上先使用 tcpdump 抓取数据并写入文件,然后再将文件拷贝到本地工作站上用 Wireshark 分析。 还有一种更高效的方法…...
【Python蓝屏程序(管理员)】
说明:该程序为临摹(😀)作品,源地址C蓝屏程序(非管理员) 我试图使用Python调用 NtRaiseHardError API ,实现类似的蓝屏效果。可惜我发现Python在普通权限下,直接调用 NtRaiseHardError API 是不被允许的,因为…...
OpenGL ES->GLSurfaceView绘制图形的流程
自定义View代码 class MyGLSurfaceView(context: Context, attrs: AttributeSet) : GLSurfaceView(context, attrs), GLSurfaceView.Renderer {var mProgrem 0init {// 设置 OpenGL ES 3.0 版本setEGLContextClientVersion(3)// 设置当前类为渲染器, 注册回调接口的实现类set…...
Linux OOM Killer详解
Linux OOM Killer详解 一、概述二、OOM Killer的技术原理1. 内存区域划分2. 内存耗尽与OOM Killer触发3. 选择被杀进程的策略4. 内存回收机制5. 内存分配策略 三、OOM Killer的工作机制1. 内存压力监测2. 触发条件3. 选择被杀进程4. 终止进程 四、实际场景举例场景一࿱…...
2024rk(案例二)
试题二(25分) 阅读以下关于数据库缓存的叙述,在答题纸上回答问题1至问题3。 【说明】 某大型电商平台建立了一个在线 B2B 商店系统,并在全国多地建设了货物仓储中心,通过提前备货的方式来提高货物的运送效率。但是在运营过程中,发现会出现很多跨仓储中心调货从而延误货物…...
小红书爆文秘籍:ChatGPT助你从0到1创造热门内容!
在小红书打造爆款文案的策略中,以下是一些调整和同义词替换的建议,以便达到文章去重的要求: 了解目标受众: 在撰写文案前,先深入分析目标读者的属性,如年龄层次、性别、爱好和购买行为。通过ChatGPT, 你能迅…...
django快速实现个人博客(附源码)
文章目录 一、工程目录组织结构二、模型及管理实现1、模型2、admin管理 三、博客展现实现1、视图实现2、模板实现 四、部署及效果五、源代码 Django作为一款成熟的Python Web开发框架提供了丰富的内置功能,如ORM(对象关系映射)、Admin管理界面…...
K8s部署篇之手动部署二进制高可用集群架构
一、系统环境初始化 一)架构设计 所有节点都操作:3个master(etcd集群三个节点)和2个node 1、K8s服务调用如图 2、各组件说明 1、API Server 供Kubernetes API接口,主要处理 REST操作以及更新ETCD中的对象所有资源增删…...
【Unity/XLua】xlua自带教程示例分析(6)—— lua协程
文章目录 工具准备协程测试 工具准备 首先是工具脚本,一个Coroutine_Runner.cs和一个cs_coroutine.lua 前者定义了一个继承自Monobehavior的脚本组件,后者则使用lua去在Unity中实例化一个挂载该组件的GameObject,并将其设置为DontDestroyOn…...
CV目标检测概述
文章目录 目标检测概述目标检测图像分割目标检测和图像分割的区别 目标检测概述 目标检测和图像分割是计算机视觉中的两个重要任务,它们有着不同的目的和应用。以下是它们的简要介绍和区别: 目标检测 目标检测(Object Detection࿰…...
如何在notebook中运行nodejs
在 Python 生态系统的推动下,机器学习和人工智能日益流行,这带来了计算笔记本的概念。这些交互式计算平台主要是为以 Python 为中心的数据科学应用而开发的,它们将代码、计算输出、解释性文本和多媒体合并成一个有内聚力的文档。 作为 JavaS…...
大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...
【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...
【SpringBoot】100、SpringBoot中使用自定义注解+AOP实现参数自动解密
在实际项目中,用户注册、登录、修改密码等操作,都涉及到参数传输安全问题。所以我们需要在前端对账户、密码等敏感信息加密传输,在后端接收到数据后能自动解密。 1、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId...
【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
EtherNet/IP转DeviceNet协议网关详解
一,设备主要功能 疆鸿智能JH-DVN-EIP本产品是自主研发的一款EtherNet/IP从站功能的通讯网关。该产品主要功能是连接DeviceNet总线和EtherNet/IP网络,本网关连接到EtherNet/IP总线中做为从站使用,连接到DeviceNet总线中做为从站使用。 在自动…...
计算机基础知识解析:从应用到架构的全面拆解
目录 前言 1、 计算机的应用领域:无处不在的数字助手 2、 计算机的进化史:从算盘到量子计算 3、计算机的分类:不止 “台式机和笔记本” 4、计算机的组件:硬件与软件的协同 4.1 硬件:五大核心部件 4.2 软件&#…...
通过MicroSip配置自己的freeswitch服务器进行调试记录
之前用docker安装的freeswitch的,启动是正常的, 但用下面的Microsip连接不上 主要原因有可能一下几个 1、通过下面命令可以看 [rootlocalhost default]# docker exec -it freeswitch fs_cli -x "sofia status profile internal"Name …...
pycharm 设置环境出错
pycharm 设置环境出错 pycharm 新建项目,设置虚拟环境,出错 pycharm 出错 Cannot open Local Failed to start [powershell.exe, -NoExit, -ExecutionPolicy, Bypass, -File, C:\Program Files\JetBrains\PyCharm 2024.1.3\plugins\terminal\shell-int…...
实战设计模式之模板方法模式
概述 模板方法模式定义了一个操作中的算法骨架,并将某些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的前提下,重新定义算法中的某些步骤。简单来说,就是在一个方法中定义了要执行的步骤顺序或算法框架,但允许子类…...
