ThreadLocal源码分析

一、简介

  1.   我们知道多个线程同时读写同一共享变量存在并发问题,为此我们可以突破共享变量,没有共享变量就不会有并发问题,可以使用局部变量。正所谓没有共享,就没有伤害,本质上就是避免共享,除了局部变量,Java语言提供的线程本地存储(ThreadLocal)就能做到。


       其实每个线程内部都会维护一个ThreadLocalMap属性,每份线程独自的数据都存放在ThreadLocalMap中Entry[] table属性里,Entry对象的key就是ThreadLocal,value就是自己设置的值,如果程序里有多个ThreadLoca属性,每个线程在运行时会将用到ThreadLocal,生成Entry保存到table中,有点需要注意的是同一个Entry中value重新设置会被替换,如Entry<ThreadLocal,value>中value被设置成value2,变成Entry<ThreadLocal,value2>,和Map类似,如果想保存多个值,可以将value封装成对象。

二、属性

//ThreadLocal中的hash值,目的是为了定位存放在ThreadLocalMap中Entry[] table的那个位置
private final int threadLocalHashCode = nextHashCode();
//原子类,对hashCode进行累加HASH_INCREMENT
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;复制代码

三、内部类

1、SuppliedThreadLocal

//目的是为了在初始化调用ThreadLocal.get()方法时,能根据传入的函数式接口supplier的get方法获取默认值
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {    
    private final Supplier<? extends T> supplier;    
    SuppliedThreadLocal(Supplier<? extends T> supplier) 
    {        
        this.supplier = Objects.requireNonNull(supplier);    
    }   
     @Override 
    protected T initialValue() {        
    return supplier.get();    
    }
}复制代码

2、ThreadLocalMap

//每个线程Thread,内部都维护了一个ThreadLocal.ThreadLocalMap threadLocals = null属性,每个线程的//数据都各自保存在各自的ThreadLocalMap的table属性中,table中的每个元素都是Entry<ThreadLocal,Value>,
//每个ThreadLocal,都会作为Entry的key,比如有个Test类,内部有两个ThreadLocal,如果A线程在运行时,使用到两个ThreadLocal,那线程A中的ThreadLocalMap中的table[Entry0<ThreadLocal,value>,Entry1<ThreadLocal1,value1>]
static class ThreadLocalMap {    
    static class Entry extends WeakReference<ThreadLocal<?>> {        
        Object value;        
        Entry(ThreadLocal<?> k, Object v) {            
           super(k);            
           value = v;        
       }    
   }
    //table初始容量    
     private static final int INITIAL_CAPACITY = 16;
    //保存每个线程中的使用到的ThreadLocal作为key的多个Entry    
    private Entry[] table;    
    //table中的元素
    private int size = 0;
    //扩容的阈值    
    private int threshold; 复制代码

四、构造ThreadLocal对象

  1. 静态方法withInitial构造

    //传入函数式接口,构造ThreadLocal的静态内部类子类SuppliedThreadLocal,ThreadLocal.get()还未设置值时获取null,SuppliedThreadLocal.get()获取的值是传入的函数式接口supplier的get方法的值
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {    
         return new SuppliedThreadLocal<>(supplier);
    }复制代码

  2. 构造函数构造

    public ThreadLocal() {}复制代码

五、get方法

public T get() {
    //获取当前执行的线程    
    Thread t = Thread.currentThread();
    //从Thread中获取threadLocals属性,即Thread中的ThreadLocal.ThreadLocalMap threadLocals = null;    ThreadLocalMap map = getMap(t);//看下面的getMap方法
    //如果当前线程的ThreadLocalMap不为null
    if (map != null) {
        //getEntry看下面介绍的ThreadLocalMap的getEntry方法        
        ThreadLocalMap.Entry e = map.getEntry(this);        
        if (e != null) {            
            @SuppressWarnings("unchecked")            
            T result = (T)e.value;            
            return result;        
       }    
    }    
    return setInitialValue();}复制代码

getMap方法,从Thread中获取threadLocal属性

ThreadLocalMap getMap(Thread t) {    
    return t.threadLocals;
}复制代码

setInitialValue方法,初次构造ThreadLocalMap对象,并设置firstValue

private T setInitialValue() {
    //如果是通过构造函数构造ThreadLocal,initialValue()方法返回null,如果是通过withInitial静态方法构造ThreadLocal,initialValue()方法返回supplier.get();
    T value = initialValue();
    //获取当前线程    
    Thread t = Thread.currentThread();
    //从Thread中获取threadLocal属性    
    ThreadLocalMap map = getMap(t);    
    if (map != null)
        //如果map不为null,将其value设置进去,key为当前的ThreadLocal,set会在下面set方法中介绍        
        map.set(this, value);   
     else
        //构造ThreadLocalMap,并且将其ThreadLocalMap赋值给当前线程的threadLocals属性        
        createMap(t, value);//t.threadLocals = new ThreadLocalMap(this, firstValue);
    return value;}复制代码

ThreadLocalMap构造方法

  1. 公共的构造方法

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                //创建个大小为16的Entry数组
                table = new Entry[INITIAL_CAPACITY];
                //根据ThreadLocal的threadLocalHashCode属性值&上table数组长度-1,计算出该ThreadLocal的Entry的存放位置
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                //将其Entry存入数组中,Entry继承于弱应用,这样的话当ThreadLocal被赋值为null,gc可以回收掉ThreadLocal,而不至于Entry对ThreadLocal的引用,导致gc回收不了,防止内存泄漏    
                table[i] = new Entry(firstKey, firstValue);    
                //table数组中存在的元素
                size = 1;
                //设置扩容的阈值大小 threshold = len * 2 / 3 table数组长度乘于2除以3           
                setThreshold(INITIAL_CAPACITY);
    }复制代码

  2. 私有的构造方法,

    //线程间的InheritableThreadLocal传递,这个会在详细介绍InheritableThreadLocal中介绍
    private ThreadLocalMap(ThreadLocalMap parentMap) {
        //父线程的ThreadLocalMap    
        Entry[] parentTable = parentMap.table;
        //长度    
        int len = parentTable.length;
        //设置当前线程的阈值    
        setThreshold(len);
        //创建大小和父线程相同的Entry数组    
        table = new Entry[len];
        //将其父线程中的ThreadLocalMap中Entry和Entry的key(ThreadLocal)不为空的Entry的元素添加到子线程的Entry的table中    
        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) {
                    //childValue()方法只有在ThreadLocalMap的子类InheritableThreadLocal中才能使用,否则在ThreadLocal中调用会报UnsupportedOperationException异常
                    Object value = key.childValue(e.value);//return parentValue
                    Entry c = new Entry(key, value);                
                    //计算Entry的存放位置
                    int h = key.threadLocalHashCode & (len - 1);                
                    //如果当前位置有存在,则存放下一个位置,只到下一个位置没有元素
                    while (table[h] != null)                    
                        h = nextIndex(h, len);                
                    table[h] = c;
                    //当前线程的Entry的table数组的元素大小加1                
                    size++;            
                }        
            }    
        }
    }复制代码

       createInheritedMap方法,调用ThreadLocalMap的私有构造方法

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {    
    return new ThreadLocalMap(parentMap);
}复制代码

      createInheritedMap方法的在哪里被调用

//Thread类中的init方法
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {
    //.....省略n行代码,inheritThreadLocals默认为true,如果创建当前线程的父线程inheritableThreadLocal不为空,将其inheritableThreadLocals的ThreadLocalMap中的Entry的table数组赋值给当前线程    //比如在main线程中创建了test线程,并且main中的属性inheritableThreadLocals不为空,会将其inheritableThreadLocals中的的ThreadLocalMap中的Entry的table数组赋值给test线程,inheritableThreadLocals属性的类只能是InheritableThreadLocal
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);       
}复制代码

ThreadLocalMap的getEntry方法

private Entry getEntry(ThreadLocal<?> key) {
    //ThreadLocal的threadLocalHashCode & 上 ThreadLocalMap的Entry数组table的长度-1,定位元素所在的下标     
    int i = key.threadLocalHashCode & (table.length - 1);    
    Entry e = table[i];
    //entry不等于null并且e.get和当前的ThreadLocal相等,返回entry    
    if (e != null && e.get() == key)        
        return e;    
    else
        //如果entry等于null,获取e.get()和当前的ThreadLocal不相等        
        return getEntryAfterMiss(key, i, e);}复制代码

ThreadLocalMap的getEntryAfterMiss方法

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    //ThreadLocalMap的Entry数组table    
    Entry[] tab = table;
    //数组长度    
    int len = tab.length;
    //如果Entry e为null,直接返回空,e不为空,找到key和当前ThreadLocal相等的Entry,否则只到遍历完table中的size    
    while (e != null) {
        //获取从entry中获取key        
        ThreadLocal<?> k = e.get();
        //如果key和当前的ThreadLocal相等,则直接返回entry        
        if (k == key)            
            return e;
        //如果当前的entry的key为null,即外部将其ThreadLocal的属性赋值为null,获取被gc回收        
        if (k == null) 
            //清除当前key为null的Entry,并且重新计算后面的每个Entry元素的存放位置,说白了就是所有元素的位置都向前移动           
            expungeStaleEntry(i);        
        else 
            //获取下一个ThreadLocal为key的Entry           
            i = nextIndex(i, len);        
        e = tab[i];    
    }    
    return null;
}复制代码

ThreadLocalMap的expungeStaleEntry方法

private int expungeStaleEntry(int staleSlot) {    //当前线程的ThreadLocalMap的Entry数组table属性
    Entry[] tab = table;
    //table数组的长度    
    int len = tab.length;
    //将其当前key为null的entry的value也设置为null,加快value的gc回收     
     tab[staleSlot].value = null;
    //将其整个entry设置为null    
     tab[staleSlot] = null;
    //table数组中存在的元素减1     
     size--;    
     Entry e;    
     int i;
    //从当前key为null的Entry的存放位置statleSlot位置开始往后面开始找到key为null的entry并且清除掉,否则的话,重新计算key不为null的Entry元素的存放位置    
      for (i = nextIndex(staleSlot, len); (e = tab[i]) != null;i = nextIndex(i, len)) {
          ThreadLocal<?> k = e.get();
          //当前位置的entry的key为null,清除value和entry,并且table数组元素减1        
          if (k == null) {            
             e.value = null;            
             tab[i] = null;            
             size--;        
          } else {
            //重新计算当前位置的entry的存放位置            
            int h = k.threadLocalHashCode & (len - 1);            
            if (h != i) {
                //将以前位置的entry置为null                
                tab[i] = null;
                //重新找到新的存放位置                
                while (tab[h] != null)                    
                   h = nextIndex(h, len);                
                tab[h] = e;            
            }        
         }    
     }
    //返回当前操作过的i    
    return i;
}


复制代码

六、set方法

public void set(T value) {
    //获取当前线程    
    Thread t = Thread.currentThread();
    //从当前线程中获取ThreadLocalMap    
    ThreadLocalMap map = getMap(t);    
    if (map != null)        
        map.set(this, value);    
    else
        //如果ThreadLocalMap不存在,构造ThreadLocalMap,并且将其ThreadLocalMap赋值给当前线程的threadLocals属性 ,ThreadLocalMap的构造函数可以看上面的介绍
        createMap(t, value);//t.threadLocals = new ThreadLocalMap(this, firstValue);       
}复制代码

ThreadLocalMap的set(this,value)方法

private void set(ThreadLocal<?> key, Object value) {    
    Entry[] tab = table;    
    int len = tab.length;    
    int i = key.threadLocalHashCode & (len-1);
    //从当前算出来的位置i从后开始遍历    
    for (Entry e = tab[i]; e != null;e = tab[i = nextIndex(i, len)]) {        
        ThreadLocal<?> k = e.get();
        //寻找到k和当前的ThreadLocal相等,替换值,为此一个ThreadLocal只能存放线程的一个值        
        if (k == key) {
            //将当前值设置到value中            
            e.value = value;            
            return;        
        }
        //如果在遍历的过程中,存在某一项key为null的Entry        
        if (k == null) {            
            //替换元素,并且做一些清除和重新计算元素的存放位置的操作,可以看下面replaceStaleEntry方法的介绍
            replaceStaleEntry(key, value, i);            
            return;        
        }    
    }
    //当前位置i上原本就没元素,或者在上面的遍历中,没有找到相同的key,或者某一项key的值为null的entry替换,而是从遍历中找到位置上没有元素的i     
    tab[i] = new Entry(key, value);
    //table的元素加1    
    int sz = ++size;
    //如果没有清除一些entry(key为null),并且ThreadLocalMap中的元素个数大于阈值,则对table进行扩容    
    if (!cleanSomeSlots(i, sz) && sz >= threshold)        
    rehash();
}

复制代码

replaceStaleEntry方法

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {    
    Entry[] tab = table;    
    int len = tab.length;    
    Entry e;
    //将key为null的entry的位置staleSlot赋值给新的变量    
    int slotToExpunge = staleSlot;
    //从slotToExpunge位置向前遍历,找到比此值更前的key为null的entry的在table中的下标    
    for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))
        //如果在遍历前面table元素的时候找到key为null的entry,对slotToExpunge的值进行重新赋值        
        if (e.get() == null)            
            slotToExpunge = i;
    //从当前的staleSlot向后遍历    
    for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {        
        ThreadLocal<?> k = e.get();
        //如果后面找到和当前ThreadLocal相等key的Entry        
        if (k == key) {
            //赋予新值            
            e.value = value;
            将参数传进来的staleSlot(key为null的entry下标)赋值给当前ThreadLocal的Entry的所在位置,说白了就是两个互换位置            
            tab[i] = tab[staleSlot];            
            tab[staleSlot] = e;
            //如果前面没有找到比当前staleSlot更前的key为null的Entry位置,将当前i赋值给slotToExpunge            
             if (slotToExpunge == staleSlot)                
                 slotToExpunge = i;
            //cleanSomeSlots方法可以看下面的cleanSomeSlots介绍,expungeStaleEntry方法可以看上面expungeStaleEntry方法的介绍            
            cleanSomeSlots(expungeStaleEntry(slotToExpunge),len);            return;        }
        //找到后面第一项key为null的entry的下标i,赋值给slotToExpunge,目的是为了后面执行expungeStaleEntry方法,清除key为null的entry,并且重新移动后面元素的存储位置        
        if (k == null && slotToExpunge == staleSlot)            
            slotToExpunge = i;    
    }
    //将其当前传进来key为null的entry的value置空    
    tab[staleSlot].value = null;
    //重新创建Entry对象存入当前位置    
    tab[staleSlot] = new Entry(key, value);
    //如果slotToExpunge和staleSlot不相等,说明上面有找到其他key为null的entry    
    if (slotToExpunge != staleSlot)
        //cleanSomeSlots和expungeStaleEntry配合使用,expungeStaleEntry清除后面key为null的entry和对key不为null的entry重新向前移动存储位置,cleanSomeSlots是为了清除前半部分key为null的entry数据        
        cleanSomeSlots(expungeStaleEntry(slotToExpunge),len);}复制代码

cleanSomeSlots方法

private boolean cleanSomeSlots(int i, int n) {    
    boolean removed = false;    
    Entry[] tab = table;    
    int len = tab.length;    
    do { 
        //获取table中的下一个元素        
        i = nextIndex(i, len);        
        Entry e = tab[i];
        //如果当前entry不为null,key为null        
        if (e != null && e.get() == null) {            
            n = len;            
            removed = true;
            //调用上面介绍的expungeStaleEntry方法,寻找后面的key为null的entry,将值和entry置为空,并且对后面key不为null的entry重新赋值到新位置            
            i = expungeStaleEntry(i);        
        }    
    } while ( (n >>>= 1) != 0);    
    return removed;
}复制代码


七、remove方法

public void remove() {
    //从当前线程中获取ThreadLocalMap    
    ThreadLocalMap m = getMap(Thread.currentThread());
    //如果当前的ThreadLocalMap不为null,调用下面的那个remove方法,说明如果当前线程没有设置ThreadLocal.ThreadLocalMap threadLocals = null属性,调用remove没影响
    if (m != null)        
        m.remove(this);
}

private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            //从当前位置开始往后找,找到key和当前ThreadLocal相等的Entry
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    //Entry元素的key设置为null
                    e.clear();
                    //从当前位置开始查询key为null的entry,和重新计算后面存在的entry元素的位置,看上面expungeStaleEntry方法的介绍
                    expungeStaleEntry(i);
                    return;
                }
            }
        }复制代码

有不清楚的地方,可以留言一起探讨


转载于:https://juejin.im/post/5ce7e0596fb9a07ee742ba79

深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
【5层】公司办公楼全套设计+++(3156平,含计算书、建筑图,结构图、实习报告,PKPM,答辩PPT) 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值