【Java面试】谈一谈你对ThreadLocal的理解

news2024/9/22 23:33:59

文章目录

  • ThreadLocal原理
    • 大致设计
    • 底层理解
  • 【Java面试】说说你对ThreadLocal内存泄漏问题的理解
  • hash冲突的解决
  • get/set/remove方法的一些细节

在多线程情况下,对于一个共享变量或者资源对象进行读或者写操作时,就必须考虑线程安全问题。而ThreadLocal采用的是完全相反的方式来解决线程安全问题。他实现了对资源对象的线程隔离,让每个线程各自使用各自的资源对象,避免争用引发的线程安全问题。 ThreadLocal同时实现了线程内的资源共享。 例如方法1对ThreadLocal中的变量进行了设置,那么方法2中只要是同一个线程,那么他也能访问到线程1在ThreadLocal中设置的变量。

ThreadLocal原理

package com.example.scheduledlovetoobject.threadPoolTest;

import lombok.SneakyThrows;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * @author: Zhangjinbiao
 * @Date: 2022/12/7 10:33
 * @Connection: qq460219753 wx15377920718
 * Description:
 * Version: 1.0.0
 */
public class ThreadLocalTest {
    public static void main(String[] args) {
        test1();
        test2();
    }

    public static void test1() {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                System.out.println(Utils.getConnection());
            }, "t" + i).start();
        }
    }

    public static void test2() {
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + Utils.getConnection());
                System.out.println(Thread.currentThread().getName() + Utils.getConnection());
                System.out.println(Thread.currentThread().getName() + Utils.getConnection());
            }, "t" + i).start();
        }
    }

    static class Utils {
        public static final ThreadLocal<Connection> tl = new ThreadLocal<>();

        public static Connection getConnection() {
            Connection conn = tl.get();
            if (conn == null) {
                conn = innerGetConnection();
                tl.set(conn);
            }
            return conn;
        }

        private static Connection innerGetConnection() {
            try {
                return DriverManager.getConnection("jdbc:mysql://localhost:3306/yoshino?useSSL=false", "root", "root");
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }

}

在这里插入图片描述

在这里插入图片描述

大致设计

上面的代码很容易就看懂了,问题在于,ThreadLocal是怎么做到不同的线程存放不同的对象,而同一个线程能取出同样的对象的呢?
其实当你看到它使用的是set和get方法的时候,你就应该大致能猜到,他的底层应该有一个Map结构来存储线程以及这个线程中的资源,当然,这是JDK早期的设计了,下面是早期的设计
在这里插入图片描述
在JDK8中,ThreadLocal的结构已经改变了。
在这里插入图片描述
在这里插入图片描述
可以发现,我们并不是在ThreadLocal中存储我们的资源,而是在每一个线程中存储,由于每一个线程对象都有一个ThreadLocalMap的局部变量,因此每一个线程之间的ThreadLocalMap都是互相隔离的,所以同一个线程能访问到同一个ThreadLocalMap对象中的数据。我们使用ThreadLocal的set方法的时候其实是向Thread中的ThreadLocalMap设置值。那么下次我们调用ThreadLocal的get方法的时候,他其实会先获取当前线程对象,然后使用这个线程对象去访问ThreadLocalMap中的数据。

只有在你第一次使用这个Map集合的时候,他才会创建,也就是它使用的是懒加载。

因此ThreadLocal的原理是, 每一个线程对象中都有一个ThreadLocalMap类型的成员变量,用来存储资源对象。这个Map的key是ThreadLocal对象,value才是真正要存储的资源。
具体的过程是这样的:
( 1 ) 每个Thread线程内部都有一个Map (ThreadLocalMap)
( 2 ) Map里面存储ThreadLocal对象( key )和线程的变量副本 ( value )
( 3 ) Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
( 4 )对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

那么现在的设计的好处在哪里呢?

  • 每个Map存储的Entry变少,在早期的设计中key为Thread对象,而我们知道Thread对象的个数是很多的,而现在的设计key为ThreadLocal,我们一般设定ThreadLocal为static,所以能保证ThreadLocalMap存储的键值对更少。
  • 现在在Thread销毁之后,ThreadLocalMap也会自动销毁,减少对内存的时候。而早期的设计,即使线程消失了,ThreadLocalMap依旧存在,还是要维护它。

方法介绍

  • 调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中
  • 调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值
  • 调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值

set方法
执行流程:
首先获取当前线程,并根据当前线程获取一个Map
如果获取的Map不为空,则将次数设置到Map中
如果Map为空,则给该线程创建Map,并设置初始值

    public void set(T value) {
    //获取当前线程对象
        Thread t = Thread.currentThread();
        //获取此线程对象中的Map对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        //已经创建那么直接放入此entry
            map.set(this, value);
        } else {
        //不存在则创建 并将当前线程t和value值作为第一个entry存放到Map中
            createMap(t, value);
        }
    }
	//返回当前线程对应的Map
	 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    } 
    //这里的this是调用createMap的ThreadLocal对象
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

get方法
执行流程:
A.首先获取当前线程,根据当前线程获取一个Map
B.如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D
C.如果e不为null,则返回e.value,否则转到D
D.Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
总结:先获取当前线程的ThreadLocalMap变量,如果存在则返回值,不存在则创建并返回初始值。

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        //以当前ThreadLocal为key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //初始化,有两种情况会执行这个代码
        //map不存在,表示此线程没有维护ThreadLocalMap对象
        //map存在,但是没有查询到与当前ThreadLocal关联的entry
        return setInitialValue();
    }
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
 	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 T setInitialValue() {
	 //调用initialValue获取初始化的值
	 //子类可以重写,不重写返回null
        T value = initialValue();
        //获取当前线程对象
        Thread t = Thread.currentThread();
        //获取当前线程对象维护的Map
        ThreadLocalMap map = getMap(t);
        if (map != null) { //map存在设置实体entry
            map.set(this, value);
        } else {
        //1:当前线程不存在ThreadLocalMap对象
        //2:则调用这个方法创建Map
        //并且将t和value设置进去
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

remove方法
执行流程:
首先获取当前线程,并且根据该线程获取一个Map
如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry

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

initialValue方法
( 1 )这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
( 2 )这个方法缺省实现直接返回一个null。
( 3 )如果想要一个除null之外的初始值,可以重写此方法。(备注∶该方法是一个protected的方法显然是为了让子类覆盖而设计的)

返回当前线程对应的ThreadLocal的初始值
此方法的第一次调用发生在,当线程通过get方法访问此钱程的
ThreadLocal值时除非线程先调用了set方法,在这种情况下,
initialvalue 才不会被这个线程调用。通常情况下,每个线程最多调用一次这个方法。
这个方法仅仅简单的返回nu1l {@code nu11};
如果程序员想ThreadLoca1线程局部变量有一个除nu11以外的初始值,
必须通过子类继承{@code ThreadLocaT]的方式去重写此方法
通常,可以通过匿名内部类的方式实现

  protected T initialValue() {
        return null;
    }

底层理解

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map功能,其内部的Entry也是独立实现的。

在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。|
另外,Entry继承WeakReference,也就是key ( ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。

 static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
		//这里要注意的是它的entey的key必须是ThreadLocal
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        // 容量必须是2的n次幂
        private static final int INITIAL_CAPACITY = 16;
		//存放键值对的数组
        private Entry[] table;
		//table中元素个数
        private int size = 0;
		//扩容阈值
        private int threshold; // Default to 0
 }

下面是我对ThreadLocal的理解的结构图
在这里插入图片描述
首先我们知道,你使用ThreadLocal的时候,它的实例对象只有一个无参的get方法,也就是说你不能使用传统的Map结构去根据key获取value,而上面的ThreadLocalMap中的Entry结构也已经说明了,你使用那个ThreadLocal对象去调用这个get方法,那你就是获取那个ThreadLocal在某个Thread中的ThreadLocalMap中对应的value值。
如果你整个项目中只有一个ThreadLocal,但是有多个线程,那么这个ThreadLocal其实就是在不同的Thread中的ThreadLocalMap的key而已。
也就是其实ThreadLocal:Thread:ThreadLocalMap的关系为1:n:n,所以能做到在不同线程调用ThreadLocal对象的get或者set方法的时候,是对不同的Thread中的ThreadLocalMap对象获取和设置值。

如果还不理解,那么用下面的代码理解一下:

   static class T {
        public ThreadLocal<Object> tl1 = new ThreadLocal<>();
        public ThreadLocal<Object> tl2 = new ThreadLocal<>();
    }

    public static void test3() {
        T t = new T();
        new Thread(()->{
            t.tl1.set("t1线程设置的tl1");
            t.tl2.set("t1线程设置的tl2");
            System.out.println(t.tl1.get());
            System.out.println(t.tl2.get());
        },"t1").start();
        new Thread(()->{
            t.tl1.set("t2线程设置的tl1");
            t.tl2.set("t2线程设置的tl2");
            System.out.println(t.tl1.get());
            System.out.println(t.tl2.get());
        },"t2").start();

    }

在这里插入图片描述
可以发现我在同一个线程中使用了多个ThreadLocal,但是即使是同一个线程,使用不同的ThreadLocal去去获取数据,得到的数据也是不同的。这也就是因为对于同一个Thread,他其实只有一个Map,但是这个Map的结构要求他的key必须是ThreadLocal类型,而值随意。因此你能做到如果使用不同的ThreadLocal作为key,那么就能取得不同的值。此时我们就可以得到下面的图:
在这里插入图片描述
而如果我们使用不同的ThreadLocal作为key,那么他们也是通过hash方法来找到对应的数组下标的。

因此,我们使用线程池的时候,按照上面的原理,由于这些线程被复用,所以如果我们在使用线程池中的线程的时候,如果这个对象中的ThreadLocalMap没有被清理,就可能导致我们会得到上一次的值,也就是"前世"。所以我们在使用ThreadLocal的时候要求使用完毕应该调用remove方法来清空ThreadLocalMap中的数据。当然,我们也可以利用线程池的这一点,来减少我们new对象的次数,来复用这些对象。

【Java面试】说说你对ThreadLocal内存泄漏问题的理解

hash冲突的解决

hash冲突的解决是Map中的一个重要内容。
而ThreadLocal也是使用了Map结构,因此也需要解决hash冲突。
首先看一下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);
        }
	//获取hash值
   private final int threadLocalHashCode = nextHashCode();

  	//原子类
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

  	//hash值增量
    private static final int HASH_INCREMENT = 0x61c88647;

 	//返回下一个hash值
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

构造函数首先创建了一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并且设置size和thresholad。

这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT, HASH_INCREMENT等于0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里,也就是Entry[] table中,这样做可以尽量避免hash冲突。
关于 & (INITIAL_CAPACITY - 1)
计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证保证在索引不越界的前提下,使得hash发生冲突的次数减小。

 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();
				//如果已经有这个key则直接覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
				//key为null,但是值补位null,说明ThreadLocal对象已经被回收了
				//当前数组中的Entry是一个陈旧的stale元素
                if (k == null) {
                //用新元素替换旧的,这个方法做了很多垃圾回收操作,防止内存泄漏
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			//key不存在且没有旧元素,直接在空元素位置创建一个新的Entry
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //cleanSomeSlots用于清除e.get()==null的元素
            //这种数据key关联的对象已经被回收,所以此时可以把对应的位置设置为null
            //如果没有清除任何entry,表示当前使用量达到了负载因子,2/3,
            //那么就rehash(会执行一次全表的扫描操作)
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
    	//线性探测    
	 private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

代码执行流程:
A.首先还是根据key计算出索引 i ,然后查找 i 位置上的Entry ,
B.若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,
C.若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,
D.不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。
最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz是否>=threshold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。

重点分析:
ThreadLocalMap使用线性探测法来解决哈希冲突的。
该方法一次探测下一地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
按照上面的描述,可以把Entry[] table看成一个环形数组。

get/set/remove方法的一些细节

对于get方法,如果get的时候发现key是null,也就是此时ThreadLocal已经被回收了,那么此时就会把对应的value也设定为空来释放空间,但是会把key再次设定为当前ThreadLocal。

对于set方法,如果发现set的时候得到的索引处的key为null,那么说明已经被回收掉了,那么此时会把数据放入进去,然后使用一种启发式扫描,他会扫描邻近的key是否为null,如果为null就进行对value的清理。相比全表扫描效率更高。启发次数与元素个数,是否发现null key有关。

对于get和set方法,他们只有在没有引用key(null key)的时候才会触发垃圾回收。 但是我们使用ThreadLocal一般都是静态的,所以这个ThreadLocal一般不会被回收,也就是他是一个强引用,所以一般不会出现null key的情况。

因此一般我们都使用remove方法,在某一个key不使用的时候,手动使用remove方法来设定其为null。

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

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

相关文章

Three.js实例详解___旋转的精灵女孩(附完整代码和资源)(三)

Three.js实例详解___旋转的精灵女孩(附完整代码和资源)&#xff08;三&#xff09; 本篇目录&#xff1a; 六、完整构建整个[旋转的精灵女孩]实例 &#xff08;1&#xff09;、新建、启动webGL工程空间 &#xff08;2&#xff09;、构建项目的目录层次结构 &#xff08;2.1…

EVE-NG安装问题记录 重置root密码

目录EVE-NG安装问题记录 重置root密码一、下载iso安装虚拟机二、碰巧没安装成功EVE-NG组件&#xff0c;开启EVE-NG机后 root/eve默认密码不对。进不去系统。1、进入单用户模式重置root密码三、重新安装eve-ng组件EVE-NG安装问题记录 重置root密码 一、下载iso安装虚拟机 从官…

ssm+mysql实现进销存系统|仓库计算机专业毕业论文java毕业设计开题报告

&#x1f496;&#x1f496;更多项目资源&#xff0c;最下方联系我们✨✨✨✨✨✨ 目录 Java项目介绍 资料获取 Java项目介绍 《ssmmysql实现进销存系统》该项目采用技术&#xff1a;jsp springmvcspringmybatis cssjs等相关技术&#xff0c;项目含有源码、文档、配套开发软…

嵌入式软件开发知识点总结(二)中断Linux内核

【好文推荐】 路由选择协议——RIP协议 轻松学会linux下查看内存频率,内核函数,cpu频率 纯干货&#xff0c;linux内存管理——内存管理架构&#xff08;建议收藏&#xff09; 概述Linux内核驱动之GPIO子系统API接口 一篇长文叙述Linux内核虚拟地址空间的基本概括 中断 硬中断 …

应用开发这样做更节约成本!

现在app已逐渐取代网页端&#xff0c;成为一众手机用户的心头好。要想轻松拿下更多目标用户&#xff0c;开发一个属于自己的app显得尤为重要。那么&#xff0c;在应用开发方面&#xff0c;我们有什么妙招可以降低开发成本呢&#xff1f; 1、明确开发需求 做好我们的app定位&am…

【NumPy 数组索引、裁切,数据类型】

&#x1f935;‍♂️ 个人主页老虎也淘气 个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f44d;&#x1f3fb; 收藏…

6年测开经验,从功能测试到测试开发,我的每一步都经过了深思熟虑...

蓦然回首&#xff0c;软件测试风风雨雨的这几年&#xff0c;起初每天的工作只是鼠标点点点&#xff0c;我还真不知道怎么办&#xff0c;闲的时候真的怀疑自己的存在价值&#xff0c;每天拿着7000的工资&#xff0c;飘荡在繁华的深圳&#xff0c;吃不饱也饿不死&#xff0c;未来…

阿里 p8 私藏 MyBatis 笔记,从入门到精通,纵享源码细节

前言 越来越多的企业已经将 MyBatis 使用到了正式的生产环境&#xff0c;我认为流行的原因就在于绝大部分项目都是面向表结构编程的&#xff0c;把 Java 对象仅当成数据容器&#xff0c;查询和模型变更都设计在一张表上&#xff0c;所谓业务逻辑就是一堆增删改查的 sql 集合&a…

Cadence Allegro PCB设计88问解析(二十一) 之 Allegro中更新器件封装(Footprint)

一个学习信号完整性仿真的layout工程师 今天和大家简单介绍Allegro中如何更新PCB封装&#xff0c;在我们导入原理图网表&#xff0c;PCB的封装是必不可少的&#xff0c;但是可能有些为了前期布局评审&#xff0c;有的封装是临时的&#xff0c;那么就需要后续更新正确的封装。下…

02 Java起步

目录 第一章&#xff1a;注释 1.1 概述 1.2 Java 中的注释 第二章&#xff1a;关键字 2.1 概述 2.2 Java 中的关键字 第三章&#xff1a;标识符 3.1 概述 3.2 标识符的命名规则&#xff08;必须遵守&#xff09; 3.3 标识符的命名规范 第四章&#xff1a;数据类型以…

第十四届蓝桥杯集训——JavaC组首篇——环境搭建(win10)

还有9天就截止报名了&#xff0c;我们也算正式开始培训了&#xff0c;今年希望能取得更好的成绩。 今年的蓝桥杯从环境开始——本博客为win10电脑的Java_JDK环境搭建&#xff1a; 学生机环境-Java编程环境&#xff08;第十四届大赛&#xff09; 链接: https://pan.baidu.com…

软件测试行业真的饱和了吗?一线大厂年薪100W的测开岗位还很缺人!

在一线大厂&#xff0c;没有测试这个岗位&#xff0c;只有测开这个岗位。这几年&#xff0c;各互联网大厂技术高速更新迭代&#xff0c;软件测试行业也正处于转型期。传统的功能测试技术逐步淘汰&#xff0c;各种新的测试技术层出不穷&#xff0c;测试人员的薪资也水涨船高。与…

【Docker】基础

文章目录Docker什么是Docker虚拟机和容器Docker 底层原理Docker 优点关键技术Docker 推荐视频课程配套食用&#xff1a;https://www.bilibili.com/video/BV1gr4y1U7CY/?vd_source6d95a05fa9867baaa8ea7c5c52faf4e6 什么是Docker Docker&#xff0c;容器虚拟化技术&#xff…

从浪潮登顶NuScenes榜首解读自动驾驶AI感知技术的发展

导读&#xff1a; “自动驾驶是集感知、决策、交互于一体的技术 环境感知能力作为自动驾驶的第一个环节&#xff0c;是车辆与环境交互的纽带 通过“摄像头、毫米波雷达、超声波雷达、激光雷达”等各类传感器设备&#xff0c;感知环境的手段日趋多元化 同时&#xff0c;在平…

STM32 EXT for GPIO

EXT 描述&#xff1a; EXTI&#xff08;中断/事件控制器&#xff09;包括20个相互独立的边沿检测电路并且能够向处理器内核产生中断 请求或唤醒事件。EXTI有三种触发类型&#xff1a;上升沿触发、下降沿触发和任意沿触发。EXTI中的每 一个边沿检测电路都可以独立配置和屏蔽 E…

java-文件上传-excel存入数据库全代码及流程(附前端代码)

今天给大家带来的是文件上传中比较常用的&#xff0c;上传excel文件&#xff0c;将表格中的数据存入数据库中的一个转化的工具类&#xff1b;大致的流程是&#xff1a;前端点击上传按钮-->选择需要上传的excel表格-->确认上传--> 文件传到后台-->后台处理file文…

[附源码]计算机毕业设计酒店在线预约咨询小程序Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

解决穿山甲Gromore广告在Oppo上报广告病毒Android.Virus.AdCheat.AdCut.A

描述 在上架Oppo的时候审核打回了发现了病毒。提示如下&#xff1a; 自动化检测“安全”项未通过,病毒名称&#xff1a;Android.Virus.AdCheat.AdCut.A,病毒描述&#xff1a;该病毒安装启动后&#xff0c;会展示非官方广告&#xff0c;造成盗源消耗和信息泄露&#xff0c;请卸…

[附源码]Python计算机毕业设计Django行程规划app

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

JUC07-ConcurrentHashMap

一、HashMap中的死链问题 多个线程对hashmap进行扩容时需要将原有数据转移到新的table数组中&#xff0c;这个过程中会重新计算每个元素对应的数组下标从而改变元素的next指针&#xff0c;而另一个线程重复对该链表进行迁移时可能会导致循环链表的产生 原始链表&#xff0c;格…