广告
返回顶部
首页 > 资讯 > 后端开发 > Python >深入浅出解析Java ThreadLocal原理
  • 694
分享到

深入浅出解析Java ThreadLocal原理

JavaThreadLocal原理Java线程 2022-11-12 18:11:58 694人浏览 安东尼

Python 官方文档:入门教程 => 点击学习

摘要

目录1.了解ThreadLocal简介使用2.源码解析 – 探究实现思路threadLocals变量与ThreadLocalMapset(T value) 方法get() 方法rem

分享一下最近看的ThreadLocal的源码的一些体会。

1.了解ThreadLocal

简介

  • ThreadLocal是jdk中java.lang包下提供的类。
  • ThreadLocal是线程安全的,并且没有使用到
  • 常用来存放线程独有变量,解决参数传递问题。
  • 当我们创建一个ThreadLocal包装的变量后,每个访问这个变量的线程会在自己的线程空间创建这个变量的一个副本,在每次操作这个变量的时候,都是在自己的线程空间内操作,解决了线程安全问题。

在这里插入图片描述

使用

  • (是线程安全的) 在这个demo中,localStr是共享的,随后在每个线程中给localStr设置值为自己线程的名字,然后再将当前线程的日志输出。
  • sleep5毫秒是为了体现出是否存在线程安全问题。
  • 从运行结果可以看到,是不存在线程安全问题的:


@Slf4j
public class ThreadLocalTest {

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

    public static void main(String[] args) {
        List<Thread> list = new LinkedList<>();
        for(int i = 0; i < 1000; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    localStr.set(Thread.currentThread().getName() + " localStr");
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug(localStr.get());
                }
            }, "t" + String.valueOf(i));
            list.add(t);
        }
        for (Thread t : list) {
            t.start();
        }

    }
}

在这里插入图片描述

而对于普通变量来说,很明显是存在线程安全问题的:



@Slf4j
public class ThreadLocalTest {

    static ThreadLocal<String> localStr = new ThreadLocal<>();
    static String shareStr;

    public static void main(String[] args) {
        List<Thread> list = new LinkedList<>();
        for(int i = 0; i < 1000; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    shareStr = Thread.currentThread().getName() + "  shareStr";
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug(shareStr);
                }
            }, "t" + String.valueOf(i));
            list.add(t);
        }
        for (Thread t : list) {
            t.start();
        }

    }
}

在这里插入图片描述

2.源码解析 – 探究实现思路

threadLocals变量与ThreadLocalMap

  • 每个线程的本地变量并不存放于ThreadLocal对象中,而是存在调用线程的threadLocals变量中。因为是线程对象的成员变量,所以生命周期等同于线程的生命周期。

在这里插入图片描述

  • 而threadLocals是ThreadLocalMap类的实例。
  • ThreadLocalMap实际上是一个类似HashMap的实现,是ThreadLocal的静态内部类。
  • 看下Doug Lea写的注释: ThreadLocalMap是一个定制的hash map,仅适用于维护线程本地值。在ThreadLocal类之外没有暴露任何的操作。这个类是私有的,允许在类线程中声明字段。为了处理非常大并长期存在(对象)的用法,哈希表的entries使用weakReference作为键。但是,由于没有使用引用队列,因此只有当表开始耗尽空间时,才能保证删除过时的entries。

在这里插入图片描述

  • 暂不探究ThreadLocalMap的内部实现细节,暂时只需要知道实现了一个hash map,并且Entry的key是弱引用即可,具体的set() get() remove() 方法在下文中会有。

set(T value) 方法

  • 进入set(T value) 方法后,先尝试获取map,如果获取到了map,直接设置值,否则新建一个map。

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

get() 方法

  1. 进入get()方法后,首先获取当前线程,然后进入getMap(Thread t)中获取ThreadLocalMap对象,直接返回t.threadLocals。
  2. 如果map不为空,直接返回map中当前ThreadLocal作为键对应的值。
  3. 如果map为空,需要先进行初始化。调用setInitialValue()方法进行初始化。
  • setInitialValue()中先获取一个初始值,默认为null。
  • 如果map存在当前线程中,直接设置初始值。
  • 如果map不存在当前线程中,需要先创建一个map。
  • createMap(Thread t, T firstValue)中就是new了一个ThreadLocalMap对象,并且初始化了一个entry对。

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

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

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    protected T initialValue() {
        return null;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

remove() 方法

  • remove() 方法中,先判断map是否存在,不存在直接将map中this作为键的entry删掉。

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

实现思路总结

  • ThreadLocal搭配线程的threadLocals变量实现,当调用set(T value) 和 get() 方法时,如果线程中的threadLocals仍然为null,会为其初始化。
  • ThreadLocal对象往threadLocals存储具体变量时,key是ThreadLocal对象的自身引用,value是真正的变量,且key是弱引用。

在这里插入图片描述

3.InheritableThreadLocal与继承性

InheritableThreadLocal英语翻译一下就是可继承的ThreadLocal,让我们看下它和ThreadLocal的继承性体现在哪。

这里的继承性指的是:子线程是否能访问父线程的变量。

ThreadLocal的不可继承性

threadLocals是当前线程的成员变量,在子线程中不可见



@Slf4j
public class InheritableThreadLocalTest {

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

    public static void main(String[] args) {
        localStr.set("main线程为其设置的值");
        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug("访问localStr : " + localStr.get());
            }
        }).start();
        System.out.println(localStr.get());
    }
}

在这里插入图片描述

InheritableThreadLocal实现继承性的源码剖析

看一下InheritableThreadLocal的源码:

在这里插入图片描述

源码非常简短,下面简单分析一下:

  • InheritableThreadLocal类继承自ThreadLocal类,重写了childValue(T parentValue)、getMap()、createMap(Thread t, T firstValue) 三个方法。
  • createMap(Thread t, T firstValue)会在初始化的时候调用,重写createMap(Thread t, T firstValue) 意味着,InheritableThreadLocal的实例使用的是线程对象中的inheritableThreadLocals,而不再是原来的threadLocals。
  • getMap() 方法也是确保使用的是inheritableThreadLocals。
  • childValue(T parentValue) 方法中,直接返回了parentValue,这个方法会在ThreadLocal的构造方法中被调用,为了弄清这个意图,我们有必要看看Thread类初始化方法的源码。

从Thread的构造方法看,发现所有的构造方法都会调用init()方法进行初始化,init()方法有两个重载形式。

在这里插入图片描述

我们进入参数较多的init方法查看一下:


    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
				// 新线程还未创建出来,当前线程就是即将要创建线程的父线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            

            
            if (security != null) {
                g = security.getThreadGroup();
            }

            
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        
        g.checkAccess();

        
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        // 如果父线程的inheritThreadLocals 不为空
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        		 // 设置子线程中的inheritableThreadLocals设置为父线程的inheritableThreadLocals
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
        this.stackSize = stackSize;

        
        tid = nextThreadID();
    }

我们重点看一下和inheritThreadLocals相关的地方(含注释的地方)

  • 在进入init方法后,先获取了父线程,然后再下面判断了父线程的inheritThreadLocals 是否为空,不为空就调用ThreadLocal.createInheritedMap方法,参数就是父线程的inheritThreadLocals 。

再看下ThreadLocal.createInheritedMap方法:

  • 调用了自身的构造方法,将parentMap传入。

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

看下这个构造方法:

  • 发现主要是用parentMap的所有entry初始化当前的map。
  • 在注释处,调用了inheritThreadLocals重写的childValue方法,而重写后,直接返回的是parentValue,也就是将父线程的inheritThreadLocal里面的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) {
                        // 调用inheritThreadLocals重写的childValue方法
                        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++;
                    }
                }
            }
        }

如何理解这个继承性

通过上面的源码分析,可以发现,InheritableThreadLocal的继承性主要体现在:创建子线程时,会将父线程的inheritThreadLocals里面所有entry拷贝一份给子进程。

那么当子进程被创建出来之后,父进程又修改了inheritThreadLocals里面的值,这个操作是否对子线程可见,通过上面的源码可知,这个操作明显是不可见的,下面有个demo可以证实。

  • sleep操作是为了控制两个线程的执行流程。


@Slf4j
public class InheritableThreadLocalTest {

    static ThreadLocal<String> localStr = new ThreadLocal<>();
    static InheritableThreadLocal<String> inheritableLocalStr = new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        inheritableLocalStr.set("main线程第一次为inheritableLocalStr设置的值");
        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug("子线程第一次访问inheritableLocalStr : " + inheritableLocalStr.get());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("子线程第二次访问inheritableLocalStr : " + inheritableLocalStr.get());
            }
        }).start();
        Thread.sleep(500);
        inheritableLocalStr.set("main线程第二次为inheritableLocalStr设置的值");
        log.debug("main线程第二次为inheritableLocalStr赋值");
        Thread.sleep(1000);
    }
}

看下输出:

在这里插入图片描述

可以发现,子线程创建出来后,对父线程中inheritThreadLocals的修改操作,对子线程不可见。

总结

  • ThreadLocal不可继承,threadLocals是当前线程的成员变量,在子线程中不可见。
  • InheritableThreadLocal可继承,原理是:在新建子线程的时候,将父线程中inheritThreadLocals所有的entry拷贝给了子线程。
  • 子线程创建出来后,对父线程中inheritThreadLocals的修改操作,对子线程不可见。

4.存在的内存泄露问题

要充分理解ThreadLocal中存在的内存泄露问题,需要有以下JVM对内存管理的前置知识(这里篇幅问题就不补充了):

  • 什么是内存泄露?
  • 什么是强引用?
  • 什么是弱引用?
  • 何时GC
  • 强引用和弱引用GC时的区别?

在分析上述ThreadLocalMap源码的时候,注意到有一个小细节,ThreadLocalMap的Entry继承了WeakReference<ThreadLocal<?>>,也就是说Entry的key是一个对ThreadLocal<?>的弱引用。问题来了,为什么这里要使用弱引用呢?

在这里插入图片描述

使用强引用会如何?

现在假设Entry的key是一个对ThreadLocal的强引用,当ThreadLocal对象使用完后,外部的强引用不存在,但是因为当前线程对象中的threadLocals还持有ThreadLocal的强引用,而threadLocals的生命周期是和线程一致的,这个时候,如果没有手动删除,整个Entry就发生了内存泄露。

使用弱引用会如何?

现在假设Entry的key是一个对ThreadLocal的弱引用,当ThreadLocal对象使用完后,外部的强引用不存在,此时ThreadLocal对象只存在Entry中key对它的弱引用,在下次GC的时候,这个ThreadLocal对象就会被回收,导致key为null,此时value的强引用还存在,但是value已经不会被使用了,如果没有手动删除,那么这个Entry中的key就会发生内存泄露。

使用弱引用还有一些好处,那就是,当key为null时, ThreadLocalMap中最多存在一个key为null,并且当调用set(),get(),remove()这些方法的时候,是会清除掉key为null的entry的。

set()、get()、remove() 方法中相关实现

  • 从下可以发现,set方法首先会进入一个循环。
  • 在这个循环中,会遍历整个Entry数组。直到遇到一个空的entry,退出循环。
  • 当遇到已存在的key'时,会直接替换value,然后返回。
  • 当遇到key为空的entry的时候,会直接将当前的entry存在这个过时的entry中,然后返回。

通过这个方法的源码可以看出,key为null的那个entry实际上迟早会被替换成新的entry。


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

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }
							 // 发现key为空
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
        
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

同理,可以看到在get方法中也存在:


        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 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;
                // 替换过时的entry
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

remove() 方法中也是一样:


        private void remove(ThreadLocal<?> key) {
            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)]) {
                // 清除过时的key
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

总结

  • ThreadLocal如果对ThreadLocalMap的key使用强引用,那么会存在整个entry发生内存泄露的问题,如果不手动清除,那么这个不被使用的entry会一直存在。
  • ThreadLocal如果对ThreadLocalMap的key使用弱引用,那么可能会存在一个entry的value发生内存泄露,但是在调用set(),get(),remove() 方法时,key为null的entry会被清除掉。
  • 发生内存泄露最根本的原因是:threadLocals的生命周期是和线程一致的。
  • 每次使用完ThreadLocal对象后,必须调用它的remove()方法清除数据。

5.ThreadLocal应用

ThreadLocal把数据存放到线程本地,解决了线程安全问题,没有使用锁,直接访问线程本地变量,效率较高(空间换时间。)
同时threadLocals的生命周期是和线程一致的,可以解决很多参数传递问题。

  • Session管理(Mabaties使用ThreadLocal存储session),数据库连接。
  • 如果需要跟踪请求的整个流程,可以使用ThreadLocal来传递参数。

ATFWUS 2021-11-11

以上就是深入浅出解析Java ThreadLocal原理的详细内容,更多关于Java ThreadLocal原理的资料请关注编程网其它相关文章!

--结束END--

本文标题: 深入浅出解析Java ThreadLocal原理

本文链接: https://www.lsjlt.com/news/157031.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 深入浅出解析Java ThreadLocal原理
    目录1.了解ThreadLocal简介使用2.源码解析 – 探究实现思路threadLocals变量与ThreadLocalMapset(T value) 方法get() 方法rem...
    99+
    2022-11-12
    Java ThreadLocal原理 Java 线程
  • 深入浅析Java中输出HelloWorld的原理
    今天就跟大家聊聊有关深入浅析Java中输出HelloWorld的原理,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。我们初学java的第一个程序是"hello world&q...
    99+
    2023-05-31
    java helloworld ava
  • Java深入浅出分析Synchronized原理与Callable接口
    目录一、基本特点二、加锁工作过程偏向锁轻量级锁重量级锁三、其他的优化操作锁消除锁粗化四、Callable 接口一、基本特点 1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁....
    99+
    2022-11-13
    Java Synchronized Java Callable
  • 深入浅析Java中 JVM的原理
    这篇文章将为大家详细讲解有关深入浅析Java中 JVM的原理,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真...
    99+
    2023-05-31
    java jvm ava
  • 《深入浅出深度学习:原理剖析与pytho
    8.1 生物神经元(BN)结构 1、人脑中有100亿-1000亿个神经元,每个神经元大约会和其他1万个神经元相连 2、细胞体:神经元的主体,细胞体=细胞核+细胞质+细胞膜,存在膜电位 3、树突:从细胞体向外延伸出许多突起的神经纤维。输入...
    99+
    2023-01-31
    深入浅出 深度 原理
  • 深入浅出分析C++ string底层原理
    目录一、深浅拷贝 浅拷贝:深拷贝二、string迭代器原理 三、string的传统写法 1.构造实现 2.其他接口 一、深浅拷贝 浅拷贝: 在实现string时要是不实先strin...
    99+
    2022-11-12
    C++ String底层原理 C++ 数据结构
  • 深入浅析java中反射的原理
    深入浅析java中反射的原理?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。1.Class类任何一个类都是Class的实例对象,这个实例对象有三种表示方式//第一种表示方式---...
    99+
    2023-05-31
    java 反射 ava
  • 深入浅析JAVA中封装的原理
    本篇文章为大家展示了深入浅析JAVA中封装的原理,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。第一节 什么是JAVA中的封装面向对象的三大特性:封装、继承、多态。概念:将类的某些信息隐藏在类的内部,...
    99+
    2023-05-31
    java 封装 ava
  • 浅谈MySQL之浅入深出页原理
    目录一、页的概览二、Infimum 和 Supremum三、使用Page Directory四、页的真实面貌4.1、File Header4.2、Page Header4.3、Infimum & Suprem...
    99+
    2022-05-19
    MySQL 页原理
  • 深入浅析Java中线程池的原理
    这篇文章将为大家详细讲解有关深入浅析Java中线程池的原理,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。ThreadPoolExecutor简介ThreadPoolExecutor是线程池类...
    99+
    2023-05-31
    java ava 线程池
  • 深入浅析java中堆排序的原理
    本篇文章为大家展示了深入浅析java中堆排序的原理,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。从堆排序的简介到堆排序的算法实现等如下:1. 简介  堆排序是建立在堆这种数据结构基础上的选择排序,是...
    99+
    2023-05-31
    java 堆排序 ava
  • 深入浅析java 中volatile与lock的原理
    深入浅析java 中volatile与lock的原理?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。java 中volatile和lock原理分析volatile和lock是...
    99+
    2023-05-31
    java volatile lock
  • 深入浅析java 中HashMap的实现原理
    这篇文章将为大家详细讲解有关深入浅析java 中HashMap的实现原理,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。1. HashMap的数据结构数据结构中有数组和链表来实现对数据的存储,...
    99+
    2023-05-31
    java hashmap ava
  • 深入浅出探索Java分布式锁原理
    目录什么是分布式锁?它能干什么?分布式锁实现方案基于数据库的分布式锁实现方案实现原理方案分析基于Redis的分布式锁实现方案基于sentnx命令的实现原理方案分析基于Redisson...
    99+
    2022-11-13
    Java 分布式锁 Java 架构设计
  • Java深入浅出讲解代理模式
    目录1、动态代理模式2、JDK动态代理3、JDK动态代理代码演示1、动态代理模式 动态代理的特点: 当代理对象的时候,不需要实现接口代理对象的生成,是利用JDK的API,动态的在内存...
    99+
    2022-11-13
    Java 代理模式 Java 代理模式实现
  • 带你深入了解Vue.$nextTick(原理浅析)
    白话一点就是说,其实这是和JS当中的事件循环是息息相关的,就是Vue不可能对每一个数据变化都做一次渲染,它会把这些变化先放在一个异步的队列当中,同时它还会对这个队列里面的操作进行去重,比如你修改了这个数据三次,它只会保留最后一次。这些变化是...
    99+
    2023-05-14
    $nextTick 前端 Vue.js
  • 深入浅出理解PHP原理之变量赋值
    PHP的变量赋值 这个标题估计很多人会不屑一顾,变量赋值?excuse me?我们学开发的第一课就会了好不好。但是,就是这样基础的东西,反而会让很多人蒙圈,比如,值和引用的关系。今天...
    99+
    2022-11-12
    PHP 变量赋值 PHP 变量
  • 深入浅析java 1.8 中动态代理的原理
    这篇文章将为大家详细讲解有关深入浅析java 1.8 中动态代理的原理,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。JDK8动态代理源码分析动态代理的基本使用就不详细介绍了:例子:class...
    99+
    2023-05-31
    java 动态代理 ava
  • 深入浅出理解Java泛型的使用
    目录一、泛型的意义二、泛型的使用三、自定义泛型类1.关于自定义泛型类、泛型接口:2.泛型在继承方面的体现3.通配符的使用一、泛型的意义 二、泛型的使用 1.jdk 5.0新增特性 ...
    99+
    2022-11-12
    Java泛型 Java泛型的使用
  • 深入浅出解析正则表达式-替换原则
    正则表达式的替换原则是指在使用正则表达式进行替换操作时,根据指定的正则表达式模式匹配到的内容将被替换为指定的替换字符串。在正则表达式...
    99+
    2023-08-11
    正则表达式
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作