ThreadLocal线程变量使用浅解

news2024/9/28 3:18:50

一、概述
   ThreadLocal一般称为线程本地变量,它是一种特殊的线程绑定机制,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本。通过ThreadLocal可以将对象的可见范围限制在同一个线程内,

在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
  需要重点强调的的是,不要拿ThreadLocal和synchronized做类比,sysnchronized是一种互斥同步机制,是为了保证在多线程环境下对于共享资源的正确访问,用于数据共享。而ThreadLocal从本质上讲,无非是提供了一个“线程级”的变量作用域,它是一种线程封闭(每个线程独享变量)技术,用于数据隔离。

二、实例
  ThreadLocal类使用的4个方法
void set(Object value);   // 设置当前线程的线程局部变量的值。
public Object get();    // 该方法返回当前线程所对应的线程局部变量。
public void remove();   //将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected Object initialValue();   //返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
  示例代码如下:

public class ThreadLocalTest {

    static ThreadLocal<String> localVar = new ThreadLocal<>();

    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }

    public static void main(String[] args) {
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar1");
                //调用打印方法
                print("thread1");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });

        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar2");
                //调用打印方法
                print("thread2");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });

        t1.start();
        t2.start();
    }
}

输出如下:

thread1 :localVar1
thread2 :localVar2
after remove : null
after remove : null

三、原理
  1、ThreadLocal使用
  下面是ThreadLocal的类图结构
  在这里插入图片描述

从图中可知:Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量。

ThreadLocalMap实际上类似于一个HashMap,在默认情况下,每个线程中的这两个变量都为null,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们。

除此之外,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量,即在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。。

set方法源码:

public void set(T value) {
    //(1)获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //(3)如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //(4)如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
}

get方法源码:

public T get() {
    //(1)获取当前线程
    Thread t = Thread.currentThread();
    //(2)获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    return setInitialValue();
}

private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
    return value;
}

ThreadLocalMap的数据结构,其实是个Entry类型的数组,每个Entry节点都保存一个键值对(key为ThreadLocal 实例的弱引用)。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量。

2、ThreadLocalMap使用
  类的构造函数有两个,一个为public方法,一个为private方法:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
     // 初始长度为16的Entry数组
     table = new Entry[INITIAL_CAPACITY];
     // 用每个key的threadLocalHashCode和(1111)按位做与操作得到Entry应该放的下标
     // 这样做的好处就是不管你threadLocalHashCode再大,计算结果也不会超过15
     int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
     // 初始化Entry,并放入数组对应位置
     table[i] = new Entry(firstKey, firstValue);
     // Entry数组大小更改为1
     size = 1;
     // 根据容量重设扩容阈值
     setThreshold(INITIAL_CAPACITY);
 }
 这个方法就是传入第一个ThreadLocal对象作为key,value作为值,构建一个ThreadLocalMap。
 需要注意的是,ThreadLocalMap是懒创建的,也就是说直到有Entry需要加入才会调用此方法。

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
 
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

这个构造方法由createInheritedMap方法调用,传入的参数是父线程的ThreadLocalMap。
将会创建一个ThreadLocalMap包括所有父map内的ThreadLocal

ThreadLocalMap扩容方法相关

// 设置扩容阈值为容量的三分之二
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}
 
// 下标i+1的方式从小往大下标方向递推
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
 
// 下标i-1的方式从大往小下标方向递推
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}
 
// 将Entry数组扩容为以前大小的两倍,顺便清理发生hash碰撞时key已为null的entry。
// 具体做法是
// 1.创建新的Entry[]
// 2.然后循环遍历老的Entry[]
// 3.如果Entry元素存在就去获取弱引用的ThreadLocal实例,
// 4.1此时如果ThreadLocal实例已经为空,代表已经被GC回收,那么直接把当前Entry置为null;
// 4.2.1否则通过threadLocalHashCode和(新的数组长度-1)按位与的方式得到新的数组下标
// 4.2.2接着判断该下标位置是否已存在元素,若存在就反复调用nextIndex方法求新的下标
// 4.2.3将元素entry放入新的数组中的下标位置,并将临时元素计数器加一
// 5.更新扩容阈值,并将临时变量赋值给对应的实例变量。扩容完成。
private void resize() {
    // 1.创建新的Entry[],长度为旧数组两倍
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    // 2.然后循环遍历老的Entry[]
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            // 3.如果Entry元素存在就去获取弱引用的ThreadLocal实例,
            ThreadLocal<?> k = e.get();
            if (k == null) {
                // 4.1此时如果ThreadLocal实例已经为空,代表已经被GC回收,那么直接把当前Entry置为null;
                e.value = null; // Help the GC
            } else {
                // 4.2.1否则通过threadLocalHashCode和(新的数组长度-1)按位与的方式得到新的数组下标
                int h = k.threadLocalHashCode & (newLen - 1);
                // 4.2.2接着判断该下标位置是否已存在元素,若存在就反复调用nextIndex方法求新的下标
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                // 4.2.3将元素entry放入新的数组中的下标位置,并将临时元素计数器加一
                newTab[h] = e;
                count++;
            }
        }
    }
    // 5.更新扩容阈值,并将临时变量赋值给对应的实例变量。扩容完成。
    setThreshold(newLen);
    size = count;
    table = newTab;
}

放入元素

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;
    int i = key.threadLocalHashCode & (len-1);
    // 寻找新entry放入的适合位置。
    // hash冲突时再hash方式为nextIndex从下标小的方式往大的方向递推
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // 如果计算出的下标位置存在entry
        // 且该ThreadLocal实例key和参数ThreadLocal相同,那就更新value。set结束
        if (k == key) {
            e.value = value;
            return;
        }
        // 如果计算出的下标位置存在entry且该ThreadLocal实例key为null
        // 此时说明该entry的弱引用已经失效,就用生成新的entry替换。set结束
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
        // 如果计算出的下标位置存在entry且该ThreadLocal实例key和参数ThreadLocal不同,就进入下一次循环
        // 否则说明该位置e = null ,跳出循环
    }
    // 此时e = null,也就是说该数组位置不存在entry
    // 用参数生成一个新的entry,放入此位置
    tab[i] = new Entry(key, value);
    // 大小加一
    int sz = ++size;
    // 如果 且 当前数组元素个数达到扩容阈值,就需要rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

取出元素

// 获取ThreadLocal实例对应的Entry
// 如果没能匹配到就调用getEntryAfterMiss
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实例对应的Entry时调用该方法
// 这个方法也会在查找过程中顺便清理无效的弱引用Entry
// i为数组下标,e为当前下标对应的Entry
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;
}

替换失效Entry

// 替换失效的数组位置上的Entry,在此过程中将遇到的失效弱引用Entry移除
// key和value为新的值,staleSlot为匹配到的第一个失效Entry下标
private void replaceStaleEntry(ThreadLocal<?> key, Object value,  int staleSlot) {
   Entry[] tab = table;
   int len = tab.length;
   Entry e;
 
   // 倒推以检查当前运行中的已失效Entry。 
   // 我们一次清理整个运行,以避免由于垃圾收集器释放串联的refs(即,每当收集器运行时)不断的增量重复。
   int slotToExpunge = staleSlot;
   // 这里得到的slotToExpunge是从后往前推的最后一个e!=null但是e.get()==null的下标
   for (int i = prevIndex(staleSlot, len);
        (e = tab[i]) != null;
        i = prevIndex(i, len))
       if (e.get() == null)
           slotToExpunge = i;
 
   // 找到的key或尾部的空元素
   for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null;  i = nextIndex(i, len)) {
       ThreadLocal<?> k = e.get();
      
       // 如果匹配到键,那么我们需要将它与失效Entry交换以维护哈希表顺序。 
       // 然后可以将新陈旧的插槽或其上方遇到的任何其他陈旧插槽发送到expungeStaleEntry以删除或重新运行运行中的所有其他条目。
       if (k == key) {
            // 如果存在该ThreadLocal实例的Entry,就覆盖该value
           e.value = value;
           // 这里staleSlot为匹配到的第一个失效Entry下标
           // 赋值后tab[i]!=null但是tab[i].get()==null
           tab[i] = tab[staleSlot];
           // 原staleSlot Entry替换为value更新后的e
           tab[staleSlot] = e;
 
           // slotToExpunge == staleSlot的情况是在前面倒推运算中没有找到失效的Entry
           if (slotToExpunge == staleSlot)
                // 注意这里i下标对应的元素是tab[staleSlot]
               slotToExpunge = i;
           // 移除slotToExpunge位置的Entry顺便移除一些失效Entry
           cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
           return;
       }
 
       // 该Entry弱引用失效,且在前面倒推查找中没有匹配到失效的Entry
       if (k == null && slotToExpunge == staleSlot)
             // 那就把当前这个对应失效弱引用的i给slotToExpunge,然后继续循环
           slotToExpunge = i;
   }
 
   // 走到这里说明没有匹配到ThreadLocal key
   // 把staleSlot处的value设为空(help gc)
   tab[staleSlot].value = null;
   // 该位置设为由新的ThreadLocal实例为key,新value的Entry
   tab[staleSlot] = new Entry(key, value);
 
   // 不相等说明找到了另外的失效的Entry位置,干掉他们并顺便清理出一点空间
   if (slotToExpunge != staleSlot)
       cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

自动清理过期

// 清理一些Entry
// 清理次数由当前数组大小是2的多少倍决定
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    // 找到下一个下标位置,如果失效entry就干掉
    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;
}
 
 
// 先清理所有失效的Entry
// 如果 此时size还是大于之前的四分之三,就扩容
private void rehash() {
    expungeStaleEntries();
 
    if (size >= threshold - threshold / 4)
        resize();
}
 
// 遍历数组,移除失去弱引用的旧Entry
private void expungeStaleEntries() {
     Entry[] tab = table;
     int len = tab.length;
     for (int j = 0; j < len; j++) {
         Entry e = tab[j];
         // 找到失去弱引用的Entry,将该Entry所在下标传入expungeStaleEntry将其干掉
         if (e != null && e.get() == null)
             expungeStaleEntry(j);
     }
}
// 此方法是真正移除失去弱引用的Entry的方法,顺便移除遇到的碰撞位置的失效Entry
// 参数staleSlot 是该Entry所在下标
// 返回下标i,此时tab[i]为null
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    // 数组原始长度
    int len = tab.length;
 
    // 移除Entry的value和本身的引用
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    // 数组大小减一
    size--;
 
    Entry e;
    int i;
    // 循环的方式去掉弱引用失效的entry
    // 循环开始条件是让该下标对原始数组长度求模
    // 循环结束条件是Entry为null
    // 每次循环完又再次nextIndex计算新下标
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // 找到弱引用失效的entry,干掉
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 此时e的弱引用存在
             
            // 我们已经熟悉了这种获取下标方式
            int h = k.threadLocalHashCode & (len - 1);
            // 不相等表示存在hash冲突,放入的位置是用N次nextIndex计算后的新位置
            // 这样做的目的我猜是因为这个位置已经发生冲突,所以认为此位置是易冲突位置
            // 所以干脆把该位置的entry放到新的null位置
            // 这样可以使下次放入元素时发生冲突的几率降低
            if (h != i) {
                // 将e的来自于数组的强引用去掉
                tab[i] = null;
 
                // 找到一个新的无entry的数组下标
                while (tab[h] != null)
                    h = nextIndex(h, len);
                // 将e移动到新的数组下标位置
                tab[h] = e;
            }
        }
    }
    // 此时tab[i]为空
    return i;
}

四、继承性
  同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量)。但是InheritableThreadLocal类则可以使子类访问父类的本地变量,下面是该类的源码:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

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

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
从上面代码可以看出,InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。

五、内存泄漏
  在前面的介绍中,可以知道ThreadLocal只是一个工具类,它为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量,首先了解下几个概念:

强引用: Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
软引用: 如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。
弱引用: 如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。
虚引用: 虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)
在这里插入图片描述

ThreadLocalMap的数据结构,其实是个Entry类型的数组,它是继承自WeakReference的一个类,每个Entry节点都保存一个键值对,该类中实际存放的key是指向ThreadLocal的弱引用和与之对应的value值(该value值就是通过ThreadLocal的set方法传递过来的值)

考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏。所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量

六、场景
  最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。实际上也是多线程的一种方式,是用空间换取时间,适合多线程但是线程变量不共享,每个线程的值各自独立的情况。ThreadLocal通常用private static修饰,可以将状态与该线程建立一对一的关系。

比如Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。

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

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

相关文章

探索8个顶级的绘图工具

在数字时代&#xff0c;绘画已经成为一种常见的表达方式&#xff0c;不仅广泛应用于艺术创作领域&#xff0c;而且在教育、设计和商业领域发挥着重要作用。随着技术的进步&#xff0c;越来越多的计算机绘图软件出现&#xff0c;为用户提供了更多的选择。本文将推荐8个计算机绘图…

Linux文件与目录的增删改查

一、增 1、mkdir命令 作用: 创建一个新目录。格式: mkdir [选项] 要创建的目录 常用参数: -p:创建目录结构中指定的每一个目录,如果目录不存在则创建,如果目录已存在也不会被覆盖。用法示例: 1、mkdir directory:创建单个目录 这个命令会在当前目录下创建一个名为…

ELK 处理 SpringCloud 日志

在排查线上异常的过程中&#xff0c;查询日志总是必不可缺的一部分。现今大多采用的微服务架构&#xff0c;日志被分散在不同的机器上&#xff0c;使得日志的查询变得异常困难。工欲善其事&#xff0c;必先利其器。如果此时有一个统一的实时日志分析平台&#xff0c;那可谓是雪…

【python】exec()内置函数释义

【python】exec内置函数释义 官方释义样例注意事项拓展感谢及参考博文 官方释义 官方Python API文档镇楼 exec(object, globalsNone, localsNone, /, *, closureNone) 支持动态执行 Python 代码&#xff0c; object 必须是字符串或者代码对象。 需要特别注意以下两点&#xff…

Multi Modal Smart Diagnosis of Pulmonary Diseases

方法 体会 作者图2的字太小&#xff0c;每个图都用一样形式的block&#xff0c;流程图乱画&#xff0c;且不给代码&#xff0c;看来InCACCT不怎么样

Spring 复习笔记

目录 第一步存 Bean第二步获取并使用 Bean依赖查找的方式ApplicationContext vs BeanFactory 更简单的存储 Bean1. 配合五大类注解使用2. 方法上添加注解 Bean 更简单的获取 Bean Spring IoC 容器管理的资源就是对象&#xff0c;这个对象也叫做 Bean。Spring 作为一个 IoC 容器…

VTable: 不只是高性能的多维数据分析表格

VTable&#xff0c;面向多维分析与可视化的高性能表格组件 导读 VTable: 不只是高性能的多维数据分析表格&#xff0c;更是行列间创作的方格艺术家&#xff01; VTable是字节跳动开源可视化解决方案 VisActor 的组件之一。 在现代应用程序中&#xff0c;表格组件是不可或缺的…

面试金典--面试题 17.21. 直方图的水量(不困难的困难题)

文章目录 题目描述思路分析完整代码 题目描述 给定一个直方图(也称柱状图)&#xff0c;假设有人从上面源源不断地倒水&#xff0c;最后直方图能存多少水量?直方图的宽度为 1。 上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的直方图&#xff0c;在这种情况下&#xff0c;可以接…

适用于医美行业的微信管理系统

在当今这个数字化时代&#xff0c;微信已经成为人们日常生活中必不可少的社交工具之一。对于医美行业来说&#xff0c;微信也是一个极为重要的营销渠道。 医美行业面临的一些困境 ①门槛低&#xff0c;竞争大&#xff0c;需要进行大量营销&#xff0c;来走出红海 ②医美种类繁…

第三章 栈、队列和数组

第三章 栈、队列、数组 栈栈的基本概念栈的顺序实现栈的链接实现栈的简单应用和递归 队列队列的基本概念队列的顺序实现队列的链接实现 数组数组的逻辑结构和基本运算数组的存储结构矩阵的压缩存储 小试牛刀 栈和队列可以看作是特殊的线性表&#xff0c;是运算受限的线性表 栈 …

Nmap扫描教程-01

Nmap扫描教程 SYN扫描操作及原理&#xff08;半连接扫描&#xff09; 1. 第一步打开wireshark选着你要监听网卡 2. 在kail中输入命令找到我们需要扫描主机的ip地址 arp-scan -l -I eth1 3. 在kail中输入命令进行SYN半连接扫描 nmap -sS -p80 --reason -vvv 172.30.1.128 -s…

这是要被奖金给砸晕啊......

嗨咯&#xff0c;大家好&#xff0c;我是K同学啊&#xff01; 由于最近训练营中经常有同学问我&#xff0c;有哪些比较好的知识变现且可以提升自己专业水平的渠道&#xff0c;这几天整理出了一个个人认为还不错的关于深度学习方面的大赛&#xff08;就奖金比较多而已&#xff…

服务器启用SGX(以PowerEdge R750为例)

一、检查处理器是否支持SGX 在shell中输入以下命令查看CPU型号 cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c在Product Specifications中找到对应的处理器参数信息&#xff0c;如果支持SGX&#xff0c;可以在Security & Reliability中看到如下信息 二、以“软…

【centos7安装ElasticSearch】

概述 最近工作中有用到ES &#xff0c;当然少不了自己装一个服务器捣鼓。本文的ElasticSearch 的版本&#xff1a; 7.17.3 一、下载 ElasticSearch 点此下载 下载完成后上传至 Linux 服务器&#xff0c;本文演示放在&#xff1a; /root/ 下&#xff0c;进行解压&#xff1…

R实现地图相关图形绘制

大家好&#xff0c;我是带我去滑雪&#xff01; 地图相关图形绘制具有许多优点&#xff0c;这些优点使其在各种领域和应用中非常有用。例如&#xff1a;地图相关图形提供了一种直观的方式来可视化数据&#xff0c;使数据更容易理解和分析。通过地图&#xff0c;可以看到数据的空…

来单提醒/客户催单 ----苍穹外卖day9

来单提醒 需求分析 代码开发 注意:前端请求的并不是8080端口;而是先请求Nginx,Nginx进行反向代理以后转发到8080端口 这段代码首先创建了一个orders类用于更新订单状态 并且在更新状态后使用websocket发送给后端提醒 将信息放在map后,使用json的string化方式传给一个接收对象,…

使用wireshark解密ipsec ISAKMP包

Ipsec首先要通过ikev2协议来协商自己后续协商所用的加解密key以及用户数据的esp包用的加解密包。 ISAKMP就是加密过的ike-v2的加密包&#xff0c;有时候我们需要解密这个包来查看协商数据。如何来解密这样的包? 首先导出strongswan协商生成的各种key. 要能导出这些key&#…

“元创新·智生成” 第15届企业数智化学习大会公布嘉宾阵容

2023年是AIGC爆发年&#xff0c;与AI相关的创新应用迅速向各行各业渗透。 在企业培训领域&#xff0c;数字人、元宇宙等正逐渐成为企业在开展人才发展、业务培训等工作的工具&#xff0c;其高效、便捷、在线化、场景化等优势受到企业的热捧。在需求的推动下&#xff0c;企业培…

超实用的微信机器人功能:自动通过好友,自动打招呼,自动回复!!

无需下载软件 多号聚合 高效管理 1 自动通过好友 有新的好友请求时&#xff0c;系统会快速自动通过好友&#xff0c;免得错过客户。 同时能够多个微信设置&#xff0c;以及设置自动通过的时间段&#xff0c;只要还没通过就会等到我们设置的时间段里自动通过 2 自动打招呼 …

图片大小转换(对于图片进行压缩)

传入的是图片途径 import java.io.*; import java.awt.image.BufferedImage; import javax.imageio.ImageIO; import java.util.Base64;// 限制图像大小为4MB public byte[] limitImageSize(File imageFile, int maxSizeInBytes) throws IOException {if (imageFile.length() …