ThreadLocal“你”真的了解吗?(二)

《ThreadLocal“你”真的了解吗?(一)》这篇文章梳理了ThreadLocal的基础知识,同时还梳理了java中线程的创建方法以及这两者之间的关系,本篇文章我们将继续梳理与ThreadLocal相关,在上一节也提过的另一组件ThreadLocalMap。在开始梳理ThreadLocalMap之前,让我们会先回顾一下前篇文章讲的一些知识点。

ThreadLocal之set(T)方法

这里再次梳理这个方法,主要目的不是讲解这个方法的存储流程,而是想验证和强化一下上篇文章末尾那个故事中的一个说法:在Thread的run()方法中无论调用多少次set(T)方法,最终存储到Thread中的threadLocals中的值只有一个。这个问题有跟同事探讨过。在验证之前,我们先来看一下这个方法的源码(含ThreadLocal中的createMap()方法的源码及ThreadLocalMap构造方法的源码):

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

从源码中不难看出,当当前线程中的ThreadLocalMap对象不为空时,会直接调用ThreadLocalMap对象上的set(T)方法将要保存的值V保存到线程本地ThreadLocalMap中,这个值V对应的key是当前对象(this),即ThreadLocal对象。上一篇提到过ThreadLocalMap是一个和Map类似的工具(这里先这么理解,后面一小节会对本类进行详细介绍),所以结合源码我们有理由相信:在线程的run()方法中无论你调用多少次set(T),只要ThreadLocal对象是同一个,那Map中的数据只会是最后一次调用set()时存进去的数据V。这个可以参看后面的数据对比图。下面再来啰嗦一下当当前线程中的ThreadLocalMap对象为空时的处理逻辑,直接调用createMap(Thread, T)方法创建一个ThreadLocalMap对象,该对象持有当前的ThreadLocal对象和当前值。通过ThreadLocalMap的构造方法不难发现这两个值,即ThreadLocal对象和当前值,被包装进了Entry对象中,然后会将当前Entry对象赋值给ThreadLocal中的Entry[]数组中。下面是一组数据对比图:

图一 

图二

上篇文章末尾的故事中,还提出了一个小问题:为什么要这么定义呢?这肯定和线程中存储的数据量和向线程中存储数据的调用地方有关系。跟同事探讨这个问题时,他是这样说的:如果要存储很多数据,为什么要定义多个ThreadLocal呢?直接将ThreadLocal的泛型定义为Map不就可以了吗?是的他这个说法没毛病(如果要在一个run()方法执行过程中存储多个数据到当前线程中,泛型使用Map结构不失为上上策。他有这个说法,是因为我给他展示代码是在同一个类中定义多个 ThreadLocal,然后分别调用,具体可以参照下面的图片),但如果要在run()方法调用的方法调用的另一个方法中向线程中存储数据呢?比如线程A的run()方法调用类B的某个方法job(),而job又调用了C类的某个方法doJob(),注意B类和C类相互独立,即这两者并非互为内部类,这个时候如果在C#doJob()方法中向线程中存储一个数据(线程私有数据),按照同事的说法就需要先拿到线程本地变量中的Map,然后从Map中拿到泛型Map,接着向泛型Map中存放数据,最后再把泛型Map放回到ThreadLocalMap中。这就比较麻烦了。不如直接在C类中再定义一个ThreadLocal对象,然后直接调用ThreadLocal的set(T)方法存储数据方便。所以Thread中的私有变量ThreadLocalMap定义成与Map类似的结构是为了方便不同类同时向当前线程中存储线程私有变量的

好了,这两个萦绕我多年的疑惑终于解决了,那我们对这个一直被提及的ThreadLocalMap究竟有多少认识呢?1) 和Map类似;2) 可以存储数据;3) 通过它可以解决多线程间共享数据线程安全的问题。了解这么多就够了吗?

ThreadLocalMap深入了解

本小节我们将继续深入学习实现线程本地存储的关键数据结构ThreadLocalMap,学习它的目的有这样几个:

  1. 了解其设计思想,为后续梳理HashMap打下基础
  2. 梳理该数据结构中涉及的一些基础知识,比如java中的引用类型
  3. 梳理ThreadLocal内存泄漏这个知识点

概述

ThreadLocalMap是Java中ThreadLocal类中的一个静态内部类,其主要作用是用于实现线程的本地存储(ThreadLocalStorage,即TLS)的功能每个线程都有一个与之关联的ThreadLocalMap,在这个map中,键是ThreadLocal对象,值则是我们真正想要在当前线程中保存和隔离的变量

当我们在一个线程中调用ThreadLocal的get()或set()方法时,实际上就是在操作该线程对应的ThreadLocalMap。这样就能保证每个线程只能访问到自己线程局部变量的副本,而不会影响其他线程中的副本,从而有效地避免了多线程环境下的数据共享问题。

需要注意的是,ThreadLocalMap使用弱引用(WeakReference)来存储ThreadLocal实例作为其键。这意味着如果只有ThreadLocalMap引用了ThreadLocal实例,而没有其他强引用指向ThreadLocal实例,那么在垃圾回收时,这个ThreadLocal实例及其在ThreadLocalMap中对应的值都可能被回收,以防止内存泄漏。但这也可能导致一些不易察觉的问题,比如预期的数据无法获取,因此在使用ThreadLocal时应确保正确管理其生命周期。关于ThreadLocalMap的源码,请参照上一章节或者直接翻看源码。下面将详细梳理我们开发过程中遇到的几个高频操作及相关知识吧。

set操作

第一小节提到过ThreadLocal的set(T)方法最终调用的是这个类的set(ThreadLocal, T)方法或该类的构造方法,首先看该类的构造方法:

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对象(一个Entry类型的数组);确定当前ThreadLocal对象在table中的存储位置;创建Entry对象,并将其赋值到Entry数组中下标为i的位置上;初始化表长度;计算threshold的大小,算法见下述代码。这里解释一下INITIAL_CAPACITY变量是一个固定值,为16,表示Entry表的初始长度。

private void setThreshold(int len) {threshold = len * 2 / 3;
}

这里的Entry又是什么呢?它是ThreadLocalMap中的一个静态内部类,其继承了java中的弱引用类,即WeakReference类。该类持有的目标对象为ThreadLocal。前面说过“ThreadLocalMap使用弱引用(WeakReference)来存储ThreadLocal实例作为其键。这意味着如果只有ThreadLocalMap引用了ThreadLocal实例,而没有其他强引用指向ThreadLocal实例,那么在垃圾回收时,这个ThreadLocal实例及其在ThreadLocalMap中对应的值都可能被回收,以防止内存泄漏”。不过根据网上资料,这也是造成ThreadLocal内存泄漏的根本原因。真的是这样吗?后面再一起分析这个问题。这个Entry中还有一个Object类型的value属性,记录的就是你存放到线程中的值,比如前面案例中local.set("1234567890")这句中的1234567890最终会被Entry中的value承接。

上面我们一起梳理了ThreadLocalMap的构造方法及其处理逻辑,下面再让我们一起来梳理一下ThreadLocalMap中的set(ThreadLocal, T)方法。先来看一下这个方法的源码:

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)]) {if (e.refersTo(key)) {e.value = value;return;}if (e.refersTo(null)) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}

这段代码的处理逻辑非常清晰:把ThreadLocalMap中的table属性拷贝一份出来赋值给Entry[]类型的数组对象tab获取table属性的长度,并赋值给len计算将要保存的数据在table表中的位置(这段代码是理解“用同一个ThreadLocal对象存储数据,最终只会保存最后一个值”这个说法的关键,由于是同一个ThreadLocal对象,所以最终计算出来的数组下标是同一个,因此调用两次ThreadLocal的set(T)方法,最终存储的数据是最后一次调用传递进去的数据遍历Entry类型的数组,如果数组下标i所表示的位置存在值,则判断Entry对象和方法接收的ThreadLocal对象是否一致,如果一致则直接替换其中的value值,然后结束如果下标i所表示的位置没有数据,则直接创建Entry对象,然后将其赋值到i所表示的位置上,接着将数组长度自增1接着调用cleanSomeSlots(数组下标-3,数组实际长度-表示数组中的哪些下标中有数据-10)【注意这里所讲的3和10是按照下述案例代码执行到local9.set(Thread.currentThread().getName() + " - 9abcdefghij")时遇到的】,总之cleanSomeSlots()这个方法的主要作用就是清理下标位置到实际长度(Entry数组中槽位不为空的数量)间的数据。由于Entry继承了WeakReference类,在内存不足时,该Entry对象包裹的ThreadLocal对象极易被回收,这会导致数组中一些数据无效,所以这样做可将一些已经被标记为无效的槽位重新利用起来,如果有清理,该方法会返回true,这样就不会调用if分支中的rehash()方法;如果该方法返回false则表示没有清理,然后判断当前map的长度是否达到了需要清理的标准(threshold,默认值为10)【案例中执行到local9.set(∙∙∙),当前map的长度已经达到10了,所以会调用rehash()方法,经过该方法后,map的长度由原来的16变成了32,具体见下图】。

public class SpringTransactionApplication {static ThreadLocal<String> local = new ThreadLocal<>();static ThreadLocal<String> local1 = new ThreadLocal<>();static ThreadLocal<String> local2 = new ThreadLocal<>();static ThreadLocal<String> local3 = new ThreadLocal<>();static ThreadLocal<String> local4 = new ThreadLocal<>();static ThreadLocal<String> local5 = new ThreadLocal<>();static ThreadLocal<String> local6 = new ThreadLocal<>();static ThreadLocal<String> local7 = new ThreadLocal<>();static ThreadLocal<String> local8 = new ThreadLocal<>();static ThreadLocal<String> local9 = new ThreadLocal<>();static ThreadLocal<String> local10 = new ThreadLocal<>();static ThreadLocal<String> local11 = new ThreadLocal<>();static ThreadLocal<String> local12 = new ThreadLocal<>();public static void main(String[] args) throws InterruptedException {local.set(Thread.currentThread().getName() + " - 0987654321");Thread t = new Thread() {@Overridepublic void run() {local.set(Thread.currentThread().getName() + " - 1234567890");local.set(Thread.currentThread().getName() + " - abcdefghij");local1.set(Thread.currentThread().getName() + " - 1abcdefghij");local2.set(Thread.currentThread().getName() + " - 2abcdefghij");local3.set(Thread.currentThread().getName() + " - 3abcdefghij");local4.set(Thread.currentThread().getName() + " - 4abcdefghij");local5.set(Thread.currentThread().getName() + " - 5abcdefghij");local6.set(Thread.currentThread().getName() + " - 6abcdefghij");local7.set(Thread.currentThread().getName() + " - 7abcdefghij");local8.set(Thread.currentThread().getName() + " - 8abcdefghij");local9.set(Thread.currentThread().getName() + " - 9abcdefghij");local10.set(Thread.currentThread().getName() + " - 10abcdefghij");local11.set(Thread.currentThread().getName() + " - 11abcdefghij");local12.set(Thread.currentThread().getName() + " - 12abcdefghij");}};t.start();t.join();}}

Thread私有的ThreadLocalMap对象扩容前后的对比图,其中图一时扩容前的效果,图二时扩容后的效果:

图一

图二

扩容前后前后数据存放位置的对比见下表(图中标红的表示扩容前后数据存放位置有发生变化):

数据

扩容前数组下标

扩容后数组下标

Thread-0-abcdefghij

4

20

Thread-0-1abcdefghij

11

27

Thread-0-2abcdefghij

2

2

Thread-0-3abcdefghij

9

9

Thread-0-4abcdefghij

0

16

Thread-0-5abcdefghij

7

23

Thread-0-6abcdefghij

14

30

Thread-0-7abcdefghij

5

5

Thread-0-8abcdefghij

12

12

Thread-0-9abcdefghij

3

19

Thread-0-10abcdefghij

Thread-0-11abcdefghij

Thread-0-12abcdefghij

通过这段梳理我们明白了在向线程本地变量ThreadLocalMap中存放数据的时候,如果数据超过最开始初始化的threshold(默认值为10)且没有数据被垃圾收集器回收时,会进行扩容操作。那具体的扩容过程是怎样的呢?

ThreadLocalMap扩容

我们常常听别人讲:艺术源于生活,却又高于生活。那计算机呢?在日常生活中我们经常看到有些餐厅人满为患,而有的却门可罗雀。为了给顾客提供更好的服务,优化自己的营业收入,生意火爆的老板通常会选择租一间更大的店面,生意相对较差的老板则会将现有店面换掉。这里隐含的处理思路就是扩容、缩容,通过这个,生意好的餐馆会更上一层楼,生意差的也能免于扼杀。ThreadLocalMap这种具有存储功能的组件,会基于所使用的资源情况动态地调整自己所占的内存空间,比如这里要梳理的扩容(前提是内存容量充足)就是动态调整内存容量的一种实现。ThreadLocalMap通过rehash()方法来扩大自己的内存容量。下面先来看一下这个方法的源码:

private void rehash() {expungeStaleEntries();// Use lower threshold for doubling to avoid hysteresisif (size >= threshold - threshold / 4)resize();
}

该方法上有这样一段注释(翻译不准,还望海涵):重新包装和/或调整表格的大小。首先扫描整个表,删除陈旧的条目。如果这不能充分缩小表的大小,则将表的大小增加一倍。如果这段翻译准确的话,那么第一行中的expungeStaleEntries()方法的主要作用就是删除数据集中陈旧的条目,而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.refersTo(null))expungeStaleEntry(j);}
}

通过源码个人理解就是遍历表中的所有数据,然后判断每个数据Entry中的key是否是null对象,如果是就调用expungeStaleEntry(index)方法做进一步处理,这里不再对该方法进行详细梳理,有兴趣的可以自己跟踪一下。下面让我们一起看一下resize()这个方法的源码:

private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;int newLen = oldLen * 2;Entry[] newTab = new Entry[newLen];int count = 0;for (Entry e : oldTab) {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;
}

从源码可以看出这个方法的处理逻辑非常清晰:首先备份原来的数据集,用Entry类型的数组对象oldTab来承接;然后拿到这个数据集的长度,由int类型的oldLen变量来承接;接着指定新的数据集长度,为老数据集长度的2倍,并由int类型的变量newLen来承接;再次创建Entry类型的新数组对象,其长度为原来数组长度的2倍,最后就是创建int类型的count变量,用于存储新数组中的实际长度(数组下标对应的Entry元素不为空的数量)。接着遍历老的Entry类型的数组集合,将其中的数据和新长度重新运算,得出数据在新数组中的存储下标,然后将数据存放进相应位置。然后重新计算threshold数据。最后设置新数组的实际长度到ThreadLocal中的size属性上,设置新的数组到ThreadLocal中Entry类型的的table属性上

这么看来扩容操作也并不复杂嘛!就是将一个数组中的数据拷贝到另外一个新创建的数组中,这个新数组的长度是原来数组长度的2倍。这不就是大学数据结构这门课程数组那节的案例吗?看来是我以小人之心度君子之腹了!这么一看,还是有必要仔细梳理一下expungeStaleEntry(index)这个方法,可这该怎么梳理呢?我想还是暂时放一放吧,先来看一下ThreadLocalMap中涉及的java引用吧!

java中的引用

在ThreadLocalMap中我们看到了WeakReference,不知大家对它有没有印象。对,它就是弱引用。不过在java中我们经常看到的是T t = new T()这种写法,这里的t就是一个比WeakReference更强的引用。那在java中究竟有多少中引用呢?想必这个问题大家在面试中经常见到吧!那这个问题该怎么回答呢?

在Java中,引用类型是用来指向对象的变量。它们并不直接存储对象的数据,而是存储对象在内存中的地址(或称为引用)Java中有以下四种类型的引用,它们分别为:

  1. 强引用 (Strong Reference):强引用是默认的引用类型当一个对象被强引用变量所引用时,只要强引用还在,垃圾回收器就永远不会回收该对象,即使系统内存不足也不会回收只有当不再有强引用指向该对象时,它才会成为垃圾回收的目标。强引用一般是这样子:Object strongRef = new Object();
  2. 软引用 (Soft Reference):软引用是可选的间接引用通过SoftReference类实现,该类位于java.lang.ref包中持有软引用的对象,在系统将要发生内存溢出,即OutOfMemoryError,之前,垃圾回收器会把这些对象列入回收范围进行回收如果回收后仍无法满足内存分配需求,则抛出OOM异常。软引用通常用来实现内存敏感的缓存。软引用一般是这样的:SoftReference<Object> softRef = new SoftReference<>(new Object());
  3. 弱引用 (Weak Reference):弱引用通过java.lang.ref.WeakReference类实现弱引用的对象拥有更短暂的生命周期,只要垃圾回收器发现存在弱引用的对象,不管当前内存是否足够,都会回收该对象弱引用经常用于防止内存泄漏的情形,比如维护一种“无主”数据结构。弱引用一般是这样的:WeakReference<Object> weakRef = new WeakReference<>(new Object());
  4. 虚引用 (Phantom Reference):虚引用是最弱的一种引用关系,也称为幽灵引用,通过java.lang.ref.PhantomReference类实现一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取对象实例虚引用的主要用途是在对象被垃圾回收器回收之前,可以收到一个系统通知。虚引用在java中一般是这样定义的:PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue); // 这里queue是一个ReferenceQueue,用于接收虚引用关联对象被回收时的通知。

通过这些引用类型开发人员能够更加精细地控制对象的生命周期和内存占用,以优化程序性能、避免内存泄漏等问题。看到这里我就有点疑惑了,ThreadLocalMap中的Entry继承了WeakReference,真的会出现网上讲的那种threadLocal失效而value存在的场景吗?

ThreadLocal中的内存泄漏

内存泄漏?java中还会有这种问题?java不是号称自动内存管理吗?好吧,看来是我孤陋寡闻了。那就让我们一起认识一下这个高深的问题吧!内存泄漏(Memory Leak)是计算机程序设计中的一个严重问题,特别是在长时间运行的程序中。它指的是程序在申请分配了一块内存空间后,未能在不再需要这块内存时及时释放,导致系统无法回收这部分内存供其他程序使用随着时间推移,这种未被释放的内存会不断积累,从而消耗掉系统的可用内存资源。造成内存泄漏的原因主要有以下几点:

  1. 忘记释放内存:程序员在使用动态内存分配函数(如C++中的`new`操作符或C语言中的malloc()函数)分配了内存之后,没有在适当的时候调用相应的内存释放函数(如C++中的delete或C语言中的free())
  2. 悬挂指针:即使内存已被释放,但仍然存在指向该内存区域的指针,使得系统误以为该内存仍在使用,从而不能回收
  3. 循环引用:在某些支持垃圾回收的语言中,如果对象之间形成了循环引用关系,而这些对象已经不再需要,但由于彼此仍保持引用,可能会造成垃圾收集器无法正确回收它们的内存
  4. 单例和其他长生命周期对象持有无用对象引用:单例模式下,如果单例类持有对其他不再使用的对象的引用,由于单例在整个应用程序生命周期内不会被销毁,因此所引用的对象也无法被回收

知道了内存泄漏的定义,又知道了导致内存泄漏的原因,那我们到底该如何解决内存泄漏呢?常见方法有以下几种:

  1. 手动管理内存:在像C++这样的手动内存管理语言中,遵循“谁分配,谁释放”的原则,并确保每个new操作都有对应的delete操作
  2. 智能指针:在C++中使用智能指针(如std::unique_ptr、std::shared_ptr等)可以自动管理内存,当智能指针析构时,它会自动删除其所管理的对象,从而避免内存泄漏
  3. 内存泄漏检测工具:利用各种静态代码分析工具和动态分析工具检测内存泄漏,例如Valgrind、LeakCanary等
  4. 编程规范与设计模式:采用良好的编程习惯,如尽量减少动态内存分配,或者设计合适的对象生命周期管理策略

知道这些是不是就万事大吉了,不是的,要想预防内存泄漏,关键还得靠程序员,其要具有强烈的内存管理意识,并通过适当的编程技术来保证程序在使用完内存后能够将其正确释放回操作系统。

通过上面的描述,我们可以用一句简单的话来描述一下内存泄漏:所谓内存泄漏,就是申请的内存无法被及时回收,导致其不断积累,最终导致系统可用内存逐渐减少,从而导致系统崩溃

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2808372.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

函数——递归6(c++)

角谷猜想 题目描述 日本一位中学生发现一个奇妙的 定理&#xff0c;请角谷教授证明&#xff0c;而教授 无能为力&#xff0c;于是产生了角谷猜想。 猜想的内容&#xff1a;任给一个自然数&#xff0c; 若为偶数则除以2&#xff0c;若为奇数则乘 3加1&#xff0c;得到一个新的…

深入理解JS的执行上下文、词法作用域和闭包(下)

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

mvn版本导致的Failed to execute goal on project问题与解决

目录 一&#xff0c;报错情况与原因二&#xff0c;maven的下载与安装1&#xff0c;卸载maven2&#xff0c;安装mvn3&#xff0c;指定mvn的默认版本&#xff1a; 一&#xff0c;报错情况与原因 使用命令mvn package时会报如下错误&#xff1a; Failed to execute goal on proj…

prometheus+grafana监控nginx的简单实现

1.编译安装NGINX 加入编译安装nginx-module-vts模块,目的是为了获取更多的监控数据(虚拟主机&#xff0c;upstream等) nginx下载 http://nginx.org/download/nginx-1.20.2.tar.gz nginx-module-vts下载 https://github.com/vozlt/nginx-module-vts/archive/refs/tags/v0.2…

9.5K Star,又一款超棒开源轻量自动化运维平台

Hi&#xff0c;骚年&#xff0c;我是大 G&#xff0c;公众号「GitHub指北」会推荐 GitHub 上有趣有用的项目&#xff0c;一分钟 get 一个优秀的开源项目&#xff0c;挖掘开源的价值&#xff0c;欢迎关注。 一个好的运维平台就变得非常重要了&#xff0c;可以节省大量的人力和物…

冯诺依曼体系结构 与 操作系统

一、冯诺依曼体系结构 深入理解冯诺依曼体系结构 计算机的出现就是为了解决实际问题, 所以把问题交给计算机&#xff0c;计算机经过处理&#xff0c;得到一个结果反馈给我们&#xff0c;所以这中间就必然涉及到了输入设备&#xff0c;中央处理器(包括运算器和控制器)和输出设备…

【读后感】《枪炮、病菌与钢铁》人类社会的命运

初看这个书名其实感到困惑&#xff0c;风马牛不相及的三个名词怎么就凑到一起了&#xff0c;这书是讲什么的。 先说结论&#xff0c;讲的是人类历史&#xff0c;具体是1.3万年前开始至今的历史以及现代世界格局的形成&#xff0c;1.3万年前从这个时间节点以后&#xff0c;不论…

勇宝趣学JavaScript ES6第二章(解构赋值)

在ES6中&#xff0c;我们经常使用到解构赋值这个知识点&#xff0c;今天我们就来好好讲讲这一小部分知识点。咱们争取这回一次性讲明白。 今天是元宵节&#xff0c;祝大家节日快乐&#xff0c;只有我自己还在无聊的码字。 给我点个赞吧&#xff0c;嘿嘿&#xff01;&#xff01…

MySQL数据库进阶第四篇(视图/存储过程/触发器)

文章目录 一、视图简单介绍与基础语法二、视图的检查选项三、视图的更新四、视图的作用五、存储过程的概念与特点六、存储过程的 创建&#xff0c;调用&#xff0c;查看&#xff0c;删除七、存储过程 — 系统变量八、存储过程 — 用户定义变量九、存储过程 — 局部变量十、存储…

acwing算法学习笔记 ------ 双链表

1、定义 这里可以做一个投机取巧&#xff0c;我们不再像单链表去用head去存头和尾&#xff0c;直接让r[0] 1,l[1] 0; idx 2.进行初始化&#xff0c; 解释一下l[N] 和 r[N] l[N]:是表示指向左面下一个节点下标&#xff0c; r[N]:表示指向下一个节点的下标。大家不用担心i…

[VNCTF2024]-PWN:shellcode_master解析(orw,用mmap代替read读文件)

查看保护 查看ida 在sandbox函数中&#xff0c;函数先使用了seccomp_init初始化&#xff0c;允许了所有系统调用&#xff0c;再用seccomp_rule_add来禁用掉了部分系统调用&#xff0c;其中包括execve和read seccomp_init函数可以进行系统调用全禁用和全允许初始化 seccomp_ru…

《高质量的C/C++编程规范》学习

目录 一、编程规范基础知识 1、头文件 2、程序的板式风格 3、命名规则 二、表达式和基本语句 1、运算符的优先级 2、复合表达式 3、if语句 4、循环语句的效率 5、for循环语句 6、switch语句 三、常量 1、#define和const比较 2、常量定义规则 四、函数设计 1、参…

python_pyecharts绘制漏斗图

python-pyecharts绘制漏斗图 from pyecharts.charts import Funnel from pyecharts import options as opts# 数据 data [("访问", 100), ("咨询", 80), ("订单", 60), ("点击", 40), ("展现", 20)]# 创建漏斗图 funnel …

uvloop,一个强大的 Python 异步IO编程库!

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站零基础入门的AI学习网站~。 目录 ​编辑 前言 什么是uvloop库&#xff1f; 安装uvloop库 使用uvloop库 uvloop库的功能特性 1. 更…

【信息提取】FindSomething 浏览器插件

下载地址 FindSomething 浏览器插件 概述 在网页的源代码或js中找到一些有趣的东西 FindSomething 用于快速在网页的html源码或js代码中提取一些有趣的信息&#xff0c;包括可能请求的资源、接口的url&#xff0c;可能请求的ip和域名&#xff0c;泄漏的证件号、手机号、邮箱…

程序员可以做什么副业呢?

如果你经常玩知乎、看公众号&#xff08;软件、工具、互联网这几类的&#xff09;你就会发现&#xff0c;好多资源连接都变成了夸克网盘、迅雷网盘的资源链接。 例如&#xff1a;天涯神贴&#xff0c;基本上全是夸克、UC、迅雷网盘的资源链接。 有资源的前提下&#xff0c;迅雷…

2024年云南事业单位报名流程!明天就开始报名啦,千万不要错过哦

注意啦&#xff01;注意啦&#xff01;2024年云南事业单位报名即将开始&#xff01; ▶️公告已发布&#xff0c;2月26日上午9&#xff1a;00开始报名‼️ 相关时间节点 **报名时间&#xff1a;**2024年2月26日9:00至3月1日18:00 **资格初审时间&#xff1a;**2024年2月26日…

【Python】Windows本地映射远程Linux服务器上的端口(解决jupyter notebook无法启动问题)

创作日志&#xff1a; 学习深度学习不想在本地破电脑上再安装各种软件&#xff0c;我就用实验室的服务器配置环境&#xff0c;启动jupyter notebook时脑子又瓦特了&#xff0c;在自己Windows电脑上打开服务器提供的网址&#xff0c;那肯定打不开啊&#xff0c;以前在其它电脑上…

【服务发现--service】

1、service的定义 "Service"简写"svc”。Pod不能直接提供给外网访问&#xff0c;而是应该使用service。Service就是把Pod暴露出来提供服务&#xff0c;Service才是真正的“服务”&#xff0c;它的中文名就叫“服务”。可以说Service是一个应用服务的抽象&#…

校园微社区微信小程序源码/二手交易/兼职交友微信小程序源码

云开发校园微社区微信小程序开源源码&#xff0c;这是一款云开发校园微社区-二手交易_兼职_交友_项目微信小程序开源源码&#xff0c;可以给你提供快捷方便的校园生活&#xff0c;有很多有趣实用的板块和功能&#xff0c;如&#xff1a;闲置交易、表白交友、疑问互答、任务兼职…