我的编程空间,编程开发者的网络收藏夹
学习永远不晚

java编程Reference核心原理示例源码分析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

java编程Reference核心原理示例源码分析

带着问题,看源码针对性会更强一点、印象会更深刻、并且效果也会更好。所以我先卖个关子,提两个问题(没准下次跳槽时就被问到)。

  • 我们可以用ByteBuffer的allocateDirect方法,申请一块堆外内存创建一个DirectByteBuffer对象,然后利用它去操作堆外内存。这些申请完的堆外内存,我们可以回收吗?可以的话是通过什么样的机制回收的?
  • 大家应该都知道WeakHashMap可以用来实现内存相对敏感的本地缓存,为什么WeakHashMap合适这种业务场景,其内部实现会做什么特殊处理呢?

GC可到达性与JDK中Reference类型

上面提到的两个问题,其答案都在JDK的Reference里面。JDK早期版本中并没有Reference相关的类,这导致对象被GC回收后如果想做一些额外的清理工作(比如socket、堆外内存等)是无法实现的,同样如果想要根据堆内存的实际使用情况决定要不要去清理一些内存敏感的对象也是法实现的。

为此JDK1.2中引入的Reference相关的类,即今天要介绍的Reference、SoftReference、WeakReference、PhantomReference,还有与之相关的Cleaner、ReferenceQueue、ReferenceHandler等。与Reference相关核心类基本都在java.lang.ref包下面。其类关系如下:

其中,SoftReference代表软引用对象,垃圾回收器会根据内存需求酌情回收软引用指向的对象。普通的GC并不会回收软引用,只有在即将OOM的时候(也就是最后一次Full GC)如果被引用的对象只有SoftReference指向的引用,才会回收。WeakReference代表弱引用对象,当发生GC时,如果被引用的对象只有WeakReference指向的引用,就会被回收。PhantomReference代表虚引用对象(也有叫幻象引用的,个人认为还是虚引用更加贴切),其是一种特殊的引用类型,不能通过虚引用获取到其关联的对象,但当GC时如果其引用的对象被回收,这个事件程序可以感知,这样我们可以做相应的处理。最后就是最常见强引用对象,也就是通常我们new出来的对象。在继续介绍Reference相关类的源码前,先来简单的看一下GC如何决定一个对象是否可被回收。其基本思路是从GC Root开始向下搜索,如果对象与GC Root之间存在引用链,则对象是可达的,GC会根据是否可到达与可到达性决定对象是否可以被回收。而对象的可达性与引用类型密切相关,对象的可到达性可分为5种。

  • 强可到达,如果从GC Root搜索后,发现对象与GC Root之间存在强引用链则为强可到达。强引用链即有强引用对象,引用了该对象。
  • 软可到达,如果从GC Root搜索后,发现对象与GC Root之间不存在强引用链,但存在软引用链,则为软可到达。软引用链即有软引用对象,引用了该对象。
  • 弱可到达,如果从GC Root搜索后,发现对象与GC Root之间不存在强引用链与软引用链,但有弱引用链,则为弱可到达。弱引用链即有弱引用对象,引用了该对象。
  • 虚可到达,如果从GC Root搜索后,发现对象与GC Root之间只存在虚引用链则为虚可到达。虚引用链即有虚引用对象,引用了该对象。
  • 不可达,如果从GC Root搜索后,找不到对象与GC Root之间的引用链,则为不可到达。
    看一个简单的列子:

ObjectA为强可到达,ObjectB也为强可到达,虽然ObjectB对象被SoftReference ObjcetE 引用但由于其还被ObjectA引用所以为强可到达;而ObjectC和ObjectD为弱引用达到,虽然ObjectD对象被PhantomReference ObjcetG引用但由于其还被ObjectC引用,而ObjectC又为弱引用达到,所以ObjectD为弱引用达到;而ObjectH与ObjectI是不可到达。引用链的强弱有关系依次是 强引用 > 软引用 > 弱引用 > 虚引用,如果有更强的引用关系存在,那么引用链到达性,将由更强的引用有关系决定。

Reference核心处理流程

JVM在GC时如果当前对象只被Reference对象引用,JVM会根据Reference具体类型与堆内存的使用情况决定是否把对应的Reference对象加入到一个由Reference构成的pending链表上,如果能加入pending链表JVM同时会通知ReferenceHandler线程进行处理。ReferenceHandler线程是在Reference类被初始化时调用的,其是一个守护进程并且拥有最高的优先级。Reference类静态初始化块代码如下:

static {
    //省略部分代码...
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start();
    //省略部分代码...
}

而ReferenceHandler线程内部的run方法会不断地从Reference构成的pending链表上获取Reference对象,如果能获取则根据Reference的具体类型进行不同的处理,不能则调用wait方法等待GC回收对象处理pending链表的通知。ReferenceHandler线程run方法源码:

public void run() {
    //死循环,线程启动后会一直运行
    while (true) {
        tryHandlePending(true);
    }
}

run内部调用的tryHandlePending源码:

static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
        synchronized (lock) {
            if (pending != null) {
                r = pending;
                //instanceof 可能会抛出OOME,所以在将r从pending链上断开前,做这个处理
                c = r instanceof Cleaner ? (Cleaner) r : null;
                //将将r从pending链上断开
                pending = r.discovered;
                r.discovered = null;
            } else {
                //等待CG后的通知
                if (waitForNotify) {
                    lock.wait();
                }
                //重试
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        //当抛出OOME时,放弃CPU的运行时间,这样有希望收回一些存活的引用并且GC能回收部分空间。同时能避免频繁地自旋重试,导致连续的OOME异常
        Thread.yield();
        //重试
        return true;
    } catch (InterruptedException x) {
         //重试
        return true;
    }
    //如果是Cleaner类型的Reference调用其clean方法并退出
    if (c != null) {
        c.clean();
        return true;
    }
    ReferenceQueue<? super Object> q = r.queue;
    //如果Reference有注册ReferenceQueue,则处理pending指向的Reference结点将其加入ReferenceQueue中
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

上面tryHandlePending方法中比较重要的点是c.clean()与q.enqueue®,这个是文章最开始提到的两个问题答案的入口。Cleaner的clean方法用于完成清理工作,而ReferenceQueue是将被回收对象加入到对应的Reference列队中,等待其他线程的后继处理。更具体地关于Cleaner与ReferenceQueue后面会再详细说明。Reference的核心处理流程可总结如下:

对Reference的核心处理流程有整体了解后,再来回过头细看一下Reference类的源码。


public abstract class Reference<T> {
// Reference 引用的对象
private T referent;

volatile ReferenceQueue<? super T> queue;
// 可理解为注册的queue中的下一个结点的引用。其取值会根据Reference不同状态发生改变,具体取值见上面的分析
volatile Reference next;

transient private Reference<T> discovered;

private static Reference<Object> pending = null;
// 可理解为注册的queue中的下一个结点的引用。其取值会根据Reference不同状态发生改变,具体取值见上面的分析
volatile Reference next;
//用于CG同步Reference成员变量值的对象。
static private class Lock { }
private static Lock lock = new Lock();
//省略部分代码...
}

上面解释了Reference中的主要成员的作用,其中比较重要是Reference内部维护的不同状态,其状态不同成员变量queue、pending、discovered、next的取值都会发生变化。Reference的主要方法如下:

//构造函数,指定引用的对象referent
Reference(T referent) {
    this(referent, null);
}
//构造函数,指定引用的对象referent与注册的queue
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
//获取引用的对象referent
public T get() {
    return this.referent;
}
//将当前对象加入创建时注册的queue中
public boolean enqueue() {
    return this.queue.enqueue(this);
}

ReferenecQueue与Cleaner源码分析

先来看下ReferenceQueue的主要成员变量的含义。

//代表Reference的queue为null。Null为ReferenceQueue子类
static ReferenceQueue<Object> NULL = new Null<>();
//代表Reference已加入当前ReferenceQueue中。
static ReferenceQueue<Object> ENQUEUED = new Null<>();
//用于同步的对象
private Lock lock = new Lock();
//当前ReferenceQueue中的头节点
private volatile Reference<? extends T> head = null;
//ReferenceQueue的长度
private long queueLength = 0;

ReferenceQueue中比较重要的方法为enqueue、poll、remove方法。

//入列队enqueue方法,只被Reference类调用,也就是上面分析中ReferenceHandler线程为调用
boolean enqueue(Reference<? extends T> r) {
	//获取同步对象lock对应的监视器对象
    synchronized (lock) {
        //获取r关联的ReferenceQueue,如果创建r时未注册ReferenceQueue则为NULL,同样如果r已从ReferenceQueue中移除其也为null
        ReferenceQueue<?> queue = r.queue;
        //判断queue是否为NULL 或者 r已加入ReferenceQueue中,是的话则入队列失败
        if ((queue == NULL) || (queue == ENQUEUED)) {
            return false;
        }
        assert queue == this;
        //设置r的queue为已入队列
        r.queue = ENQUEUED;
        //如果ReferenceQueue头节点为null则r的next节点指向当前节点,否则指向头节点
        r.next = (head == null) ? r : head;
        //更新ReferenceQueue头节点
        head = r;
        //列队长度加1
        queueLength++;
        //为FinalReference类型引用增加FinalRefCount数量
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(1);
        }
        //通知remove操作队列有节点
        lock.notifyAll();
        return true;
    }
}

poll方法源码相对简单,其就是从ReferenceQueue的头节点获取Reference。

public Reference<? extends T> poll() {
    //头结点为null直接返回,代表Reference还没有加入ReferenceQueue中
    if (head == null)
        return null;
    //获取同步对象lock对应的监视器对象
    synchronized (lock) {
        return reallyPoll();
    }
}
//从队列中真正poll元素的方法
private Reference<? extends T> reallyPoll() {
    Reference<? extends T> r = head;
    //double check 头节点不为null
    if (r != null) {
    	//保存头节点的下个节点引用
        Reference<? extends T> rn = r.next;
        //更新queue头节点引用
        head = (rn == r) ? null : rn;
        //更新Reference的queue值,代表r已从队列中移除
		r.queue = NULL;
		//更新Reference的next为其本身
        r.next = r;
        queueLength--;
        //为FinalReference节点FinalRefCount数量减1
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(-1);
        }
        //返回获取的节点
        return r;
    }
    return null;
}

remove方法的源码如下:

public Reference<? extends T> remove(long timeout) throws IllegalArgumentException, InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("Negative timeout value");
    }
    //获取同步对象lock对应的监视器对象
    synchronized (lock) {
    	//获取队列头节点指向的Reference
        Reference<? extends T> r = reallyPoll();
        //获取到返回
        if (r != null) return r;
        long start = (timeout == 0) ? 0 : System.nanoTime();
        //在timeout时间内尝试重试获取
        for (;;) {
        	//等待队列上有结点通知
            lock.wait(timeout);
            //获取队列中的头节点指向的Reference
            r = reallyPoll();
            //获取到返回
            if (r != null) return r;
            if (timeout != 0) {
                long end = System.nanoTime();
                timeout -= (end - start) / 1000_000;
                //已超时但还没有获取到队列中的头节点指向的Reference返回null
                if (timeout <= 0) return null;
                start = end;
            }
        }
    }
}

简单的分析完ReferenceQueue的源码后,再来整体回顾一下Reference的核心处理流程。JVM在GC时如果当前对象只被Reference对象引用,JVM会根据Reference具体类型与堆内存的使用情况决定是否把对应的Reference对象加入到一个由Reference构成的pending链表上,如果能加入pending链表JVM同时会通知ReferenceHandler线程进行处理。ReferenceHandler线程收到通知后会调用Cleaner#clean或ReferenceQueue#enqueue方法进行处理。如果引用当前对象的Reference类型为WeakReference且堆内存不足,那么JMV就会把WeakReference加入到pending-Reference链表上,然后ReferenceHandler线程收到通知后会异步地做入队列操作。而我们的应用程序中的线程便可以不断地去拉取ReferenceQueue中的元素来感知JMV的堆内存是否出现了不足的情况,最终达到根据堆内存的情况来做一些处理的操作。实际上WeakHashMap低层便是过通上述过程实现的,只不过实现细节上有所偏差,这个后面再分析。再来看看ReferenceHandler线程收到通知后可能会调用的另外一个类Cleaner的实现。
同样先看一下Cleaner的成员变量,再看主要的方法实现。

//继承了PhantomReference类也就是虚引用,PhantomReference源码很简单只是重写了get方法返回null
public class Cleaner extends PhantomReference<Object> {
	
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
    //Cleaner链表的头结点
    private static Cleaner first = null;
    //当前Cleaner节点的后续节点
    private Cleaner next = null;
    //当前Cleaner节点的前续节点
    private Cleaner prev = null;
    //真正执行清理工作的Runnable对象,实际clean内部调用thunk.run()方法
    private final Runnable thunk;
    //省略部分代码...
}

从上面的成变量分析知道Cleaner实现了双向链表的结构。先看构造函数与clean方法。

//私有方法,不能直接new
private Cleaner(Object var1, Runnable var2) {
    super(var1, dummyQueue);
    this.thunk = var2;
}
//创建Cleaner对象,同时加入Cleaner链中。
public static Cleaner create(Object var0, Runnable var1) {
    return var1 == null ? null : add(new Cleaner(var0, var1));
}
//头插法将新创意的Cleaner对象加入双向链表,synchronized保证同步
private static synchronized Cleaner add(Cleaner var0) {
    if (first != null) {
        var0.next = first;
        first.prev = var0;
    }
    //更新头节点引用
    first = var0;
    return var0;
}

public void clean() {
	//从Cleaner链表中先移除当前节点
    if (remove(this)) {
        try {
        	//调用thunk.run()方法执行对应清理逻辑
            this.thunk.run();
        } catch (final Throwable var2) {
           //省略部分代码..
        }

    }
}

可以看到Cleaner的实现还是比较简单,Cleaner实现为PhantomReference类型的引用。当JVM GC时如果发现当前处理的对象只被PhantomReference类型对象引用,同之前说的一样其会将该Reference加pending-Reference链中上,只是ReferenceHandler线程在处理时如果PhantomReference类型实际类型又是Cleaner的话。其就是调用Cleaner.clean方法做清理逻辑处理。Cleaner实际是DirectByteBuffer分配的堆外内存收回的实现,具体见下面的分析。

DirectByteBuffer堆外内存回收与WeakHashMap敏感内存回收

绕开了一大圈终于回到了文章最开始提到的两个问题,先来看一下分配给DirectByteBuffer堆外内存是如何回收的。在创建DirectByteBuffer时我们实际是调用ByteBuffer#allocateDirect方法,而其实现如下:

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

DirectByteBuffer(int cap) {
    //省略部分代码...
    try {
    	//调用unsafe分配内存
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
       //省略部分代码...
    }
    //省略部分代码...
    //前面分析中的Cleaner对象创建,持有当前DirectByteBuffer的引用
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

里面和DirectByteBuffer堆外内存回收相关的代码便是Cleaner.create(this, new Deallocator(base, size, cap))这部分。还记得之前说实际的清理逻辑是里面和DirectByteBuffer堆外内存回收相关的代码便是Cleaner里面的Runnable#run方法吗?直接看Deallocator.run方法源码:

public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    //通过unsafe.freeMemory释放创建的堆外内存
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}

终于找到了分配给DirectByteBuffer堆外内存是如何回收的的答案。再总结一下,创建DirectByteBuffer对象时会创建一个Cleaner对象,Cleaner对象持有了DirectByteBuffer对象的引用。当JVM在GC时,如果发现DirectByteBuffer被地方法没被引用啦,JVM会将其对应的Cleaner加入到pending-reference链表中,同时通知ReferenceHandler线程处理,ReferenceHandler收到通知后,会调用Cleaner#clean方法,而对于DirectByteBuffer创建的Cleaner对象其clean方法内部会调用unsafe.freeMemory释放堆外内存。最终达到了DirectByteBuffer对象被GC回收其对应的堆外内存也被回收的目的。
再来看一下文章开始提到的另外一个问题WeakHashMap如何实现敏感内存的回收。实际WeakHashMap实现上其Entry继承了WeakReference。


//Entry继承了WeakReference, WeakReference引用的是Map的key
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;
    
    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
    //省略部分原码...
}

往WeakHashMap添加元素时,实际都会调用Entry的构造方法,也就是会创建一个WeakReference对象,这个对象的引用的是WeakHashMap刚加入的Key,而所有的WeakReference对象关联在同一个ReferenceQueue上。我们上面说过JVM在GC时,如果发现当前对象只有被WeakReference对象引用,那么会把其对应的WeakReference对象加入到pending-reference链表上,并通知ReferenceHandler线程处理。而ReferenceHandler线程收到通知后,对于WeakReference对象会调用ReferenceQueue#enqueue方法把他加入队列里面。现在我们只要关注queue里面的元素在WeakHashMap里面是在哪里被拿出去啦做了什么样的操作,就能找到文章开始问题的答案啦。最终能定位到WeakHashMap的expungeStaleEntries方法。

private void expungeStaleEntries() {
    //不断地从ReferenceQueue中取出,那些只有被WeakReference对象引用的对象的Reference
    for (Object x; (x = queue.poll()) != null; ) {
        synchronized (queue) {
            //转为 entry
            Entry<K,V> e = (Entry<K,V>) x;
            //计算其对应的桶的下标
            int i = indexFor(e.hash, table.length);
            //取出桶中元素
            Entry<K,V> prev = table[i];
            Entry<K,V> p = prev;
            //桶中对应位置有元素,遍历桶链表所有元素
            while (p != null) {
                Entry<K,V> next = p.next;
                //如果当前元素(也就是entry)与queue取出的一致,将entry从链表中去除
                if (p == e) {
                    if (prev == e)
                        table[i] = next;
                    else
                        prev.next = next;
                    // Must not null out e.next;
                    //清空entry对应的value
                    e.value = null;
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

现在只看一下WeakHashMap哪些地方会调用expungeStaleEntries方法就知道什么时候WeakHashMap里面的Key变得软可达时我们就可以将其对应的Entry从WeakHashMap里面移除。直接调用有三个地方分别是getTable方法、size方法、resize方法。 getTable方法又被很多地方调用如get、containsKey、put、remove、containsValue、replaceAll。最终看下来,只要对WeakHashMap进行操作就行调用expungeStaleEntries方法。所有只要操作了WeakHashMap,没WeakHashMap里面被再用到的Key对应的Entry就会被清除。再来总结一下,为什么WeakHashMap适合作为内存敏感缓存的实现。当JVM 在GC时,如果发现WeakHashMap里面某些Key没地方在被引用啦(WeakReference除外),JVM会将其对应的WeakReference对象加入到pending-reference链表上,并通知ReferenceHandler线程处理。而ReferenceHandler线程收到通知后将对应引用Key的WeakReference对象加入到 WeakHashMap内部的ReferenceQueue中,下次再对WeakHashMap做操作时,WeakHashMap内部会清除那些没有被引用的Key对应的Entry。这样就达到了每操作WeakHashMap时,自动的检索并清量没有被引用的Key对应的Entry的目地。

总结

本文通过两个问题引出了JDK中Reference相关类的源码分析,最终给出了问题的答案。但实际上一般开发规范中都会建议禁止重写Object#finalize方法同样与Reference类关系密切(具体而言是Finalizer类)。受篇幅的限制本文并未给出分析,有待各位自己看源码啦。半年没有写文章啦,有点对不住关注的小伙伴。希望看完本文各位或多或少能有所收获。如果觉得本文不错就帮忙转发记得标一下出处,谢谢。后面我还会继续分享一些自己觉得比较重要的东西给大家。由于个人能力有限,文中不足与错误还望指正。

以上就是java编程Reference核心原理示例源码分析的详细内容,更多关于Reference核心原理的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

java编程Reference核心原理示例源码分析

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

下载Word文档

猜你喜欢

Java SpringBoot核心源码的示例分析

本篇文章给大家分享的是有关Java SpringBoot核心源码的示例分析,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。SpringBoot源码主线分析我们要分析一个框架的源码
2023-06-22

简易vuex4核心原理及实现源码分析

这篇文章主要为大家介绍了简易vuex4核心原理及实现源码分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-12

Java并发编程之ConcurrentLinkedQueue源码的示例分析

这篇文章给大家分享的是有关Java并发编程之ConcurrentLinkedQueue源码的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。一、ConcurrentLinkedQueue介绍并编程中,一般需
2023-06-15

ReactContext原理深入理解源码示例分析

这篇文章主要为大家介绍了ReactContext原理深入理解源码示例分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-03

JVMCPUProfiler技术原理及源码的示例分析

JVMCPUProfiler技术原理及源码的示例分析,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。引言研发人员在遇到线上报警或需要优化系统性能时,常常需要分析程
2023-06-03

java线程池的实现原理源码分析

这篇文章主要介绍“java线程池的实现原理源码分析”,在日常操作中,相信很多人在java线程池的实现原理源码分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”java线程池的实现原理源码分析”的疑惑有所帮助!
2023-06-30

Spring源码解析之编程式事务的示例分析

这篇文章主要为大家展示了“Spring源码解析之编程式事务的示例分析”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Spring源码解析之编程式事务的示例分析”这篇文章吧。一、前言在Spring中
2023-06-15

Java的中文编程与配置心得的示例分析

这篇文章主要为大家展示了“Java的中文编程与配置心得的示例分析”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Java的中文编程与配置心得的示例分析”这篇文章吧。Java的中文编程与配置心得
2023-06-03

java编程之AC自动机工作原理的示例分析

这篇文章将为大家详细讲解有关java编程之AC自动机工作原理的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。1.应用场景—多模字符串匹配我们现在考虑这样一个问题,在一个文本串text中,我们想找出
2023-05-30

Java多线程定时器Timer原理的示例分析

这篇文章给大家分享的是有关Java多线程定时器Timer原理的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。Timer的schedule(TimeTask task, Date time)的使用该方法的作
2023-05-30

java编程无向图结构的存储及DFS操作代码的示例分析

这篇文章将为大家详细讲解有关java编程无向图结构的存储及DFS操作代码的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。图的概念图是算法中是树的拓展,树是从上向下的数据结构,结点都有一个父结点(根
2023-05-30

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录