线程池2

简介: 本文深入解析Java线程池核心机制,涵盖ScheduledThreadPoolExecutor的周期调度、延迟执行原理,剖析siftUp/siftDown堆操作、DelayedWorkQueue队列特性,并详解Executors工厂类各线程池的适用场景。同时探讨ThreadLocal与InheritableThreadLocal的实现原理及父子线程数据传递,帮助读者全面掌握线程池底层逻辑与实际应用。

上面是生产元素,下面来看一下消费数据。在上面我们提到的take方法中,使用了一个方法如下:
这个方法中调用了一个方法siftDown,这个方法如下:
对其的解释就是:
总结一下,当我们向queue插入任务的时候,会发生siftUp方法的执行,这个时候会把任务尽量往根部移动,而当我们完成任务调度之后,会发生siftDown方法的执行,与siftUp相反,siftDown方法会将任务尽量移动到queue的末尾。总之,大概的意思就是queue通过compareTo实现了类似于优先级队列的功能。
下面我们来看一下第二个问题:延迟时间的来龙去脉。在上面的take方法里面,首先获取了delay,然后再使用available来做延迟效果,那这个delay从哪里来的呢?通过上面的类图RunnableScheduledFuture的类图我们知道,RunnableScheduledFuture类实现了Delayed接口,而Delayed接口里面的唯一方法是getDelay,我们到RunnableScheduledFuture里面看一下这个方法的具体实现:
time是我们设定的下次执行的时间,所以延迟就是(time - now()),没毛病!
第三个问题:available变量的来龙去脉,至于这个问题,我们看下面的代码:
这是一个条件变量,take方法里面使用这个变量来做延迟效果。Condition可以在多个线程间做同步协调工作,更为具体细致的关于Condition的内容,可以参考更多的资料来学习,本文对此知识点点到为止。
到此为止,我们梳理了ScheduledThreadPoolExecutor是如何实现周期性调度的,首先分析了它的循环性,然后分析了它的延迟效果,对于线程池的学习现在才刚刚起步,需要更多更专业的知识类帮我理解更为底层的内容,当然,为了更进一步理解线程池的实现细节,首先需要对线程间通信有足够的把握,其次是要对各种数据结构有清晰的认识,比如队列、优先级队列、堆等高级的数据结构,以及java语言对于这些数据结构的实现,更为重要的是要结合实际情况分析问题,在工作和平时的学习中不断总结,不断迭代对于线程、线程池的认知
五、Executors工厂类详解
首先列出了Executors这个类提供的一些方法。
Executors方法
本文需要对以上12个类做一些区分,从其特点出发,然后分析其应用场景。
public static ExecutorService newFixedThreadPool(int nThreads)
使用这个方法会产生这样一个线程池:线程池最多会保持nThreads个线程处于活动状态,如果当前所有任务都处于活动状态,那么新提交的任务会被添加到任务阻塞队列中去。总结一下就是:使用固定大小的线程池,并发数是固定的。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
相比于newFixedThreadPool(int nThreads), 你可以使用这个方法来传递你自己的线程工厂,线程工厂是用来干嘛的?就是用来生成线程的,你可以使用线程工厂做一些个性化的线程特性定制。
public static ExecutorService newWorkStealingPool(int parallelism)
在了解或者使用这个方法之前,你你该对java的Fork/Join并行框架有一些了解,如果你想要快速了解一下该部分的内容,可以参考这篇文章:Java Fork/Join并行框架。
从名字上我们就知道这个方法生产出来的线程池具有某种“小偷”的行为,在Fork/Join里面,线程的工作模式为“盗窃算法”,也就是在自己的任务队列消费完了之后不是进入等到状态,而是会主动去偷窃别的线程的任务来做,其实是没有一种奖励机制来鼓励这些线程去帮助别的线程去消费任务的,所以可以认为这些线程都是好人,都为了快速完成任务协调作战。这种工作方式的重点在于,每个线程都将有一个任务队列,线程之间通过“偷窃”的方式互相帮助来完成任务的消费。
可以看下这个方法的实现:
可以发现,这个方法不是使用我们在第一篇文章中分析了ThreadPoolExecutor来生成线程池。而是使用了ForkJoinPool,也就是Fork/Join里面的线程池,关于ForkJoinPool更为深入的分析不再本文的涉及范围内,你只要知道Fork/Join框架的一般运行原理就可以了,下面的描述可以帮助你决策你是否需要该方法提供的线程池来工作:
public static ExecutorService newWorkStealingPool()
参考newWorkStealingPool(int parallelism)。
public static ExecutorService newSingleThreadExecutor()
下面是对该方法的描述:
可以从方法的名字上知道,该方法产生的线程池仅仅有一个Worker,任何时刻都将只有一个Worker在工作,添加的任务有很大概率被放在阻塞任务队列中等待执行。这些任务会被顺序执行,这个方法的返回值其实是对ThreadPoolExecutor的一层包装,下面的代码展示了最终执行任务的类:
从上面的代码可以看出,这个类其实就是使用了构造时传递的参数e来完成,更像是代理。而e是什么?看下面的代码:
其实就是一个只有一个线程的ThreadPoolExecutor。
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
参考newSingleThreadExecutor(),多了一个线程工厂参数
public static ExecutorService newCachedThreadPool()
首先看它的方法体内容:
可以看到,核心线程数量为0,而上限为Integer.MAX_VALUE,而且keepAliveTime为60秒,那么这个线程池的工作模式为:只要有任务呗提交,而且当前没有空闲的线程可用,那么就会创建一个新的Worker来工作,一个线程工作完了之后会缓存(idle)60秒,如果60秒之内有新的任务提交,则会被唤醒进入工作模式,否则60秒后就会被回收。可以参考下面的描述:
从描述上,我们可以想到,其实这种类型的线程池比较适合于短期高流量的场景,也就是我们所说的“秒杀”场景,在那样的场景下,需要的线程数量较多,那么使用该类型的线程池可以满足,而且该线程池还有自动收缩的功能,在不需要那么多线程的时候,会自动回收线程,释放资源。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
参考newCachedThreadPool()。
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
只有一个线程的调度线程池,类似于newSingleThreadExecutor,但是该方法生产的线程池具备调度功能,下面是对该方法的描述:
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory)
参考newSingleThreadExecutor和newSingleThreadScheduledExecutor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
参考newFixedThreadPool,但是返回类型不一样。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
参考newFixedThreadPool。
通过上面的分析,我们应该对java线程池的理解更为深入,再次说明,第五节是对前面四节内容的补充,你应该首先前四节之后再来阅读第五节,那样内容上更完整,但是单独阅读本文一样具备独立性,但是收获肯定没有同时阅读那样多。
六、ScheduleExecutorService
如果在一个ScheduleExecutorService中提交一个任务,这个任务的调度周期设置
的时间比任务本身执行的时间短的话会出现什么情况?也就是在线程调度时间已经到了
但是上次的任务还没有做完的情况下,ScheduleExecutorService是怎么处理的?
这个问题曾经困扰了我很久,我们都知道,ScheduleExecutorService是一个支持周期调度的线程池,我们可以设置调度的周期period,ScheduleExecutorService会按照设定好的周期调度我们的任务,如果我们设定的调度周期小于任务运行时间,那么很好理解,比如说我们设置的调度周期为1秒,而任务实际只需要10毫秒就可以执行完成一次,那么执行完成之后放到调度队列即可,下次调度时间到了再次调度执行。那么,如果我们的任务执行时间大于我们设定的调度时间会怎么样?比如我们设定的调度周期为1秒,但是我们的任务每次需要执行2秒,这个情况是不是很奇怪呢?
对于ScheduleExecutorService来说,你给我设定的调度周期是1秒,那么我当然1秒就会去运行一次你,但是运行1秒后发现你还在运行,那我是再次运行你还是等你运行完成再调度你运行?
当然,这都是我的主观臆断来猜测ScheduleExecutorService的原理,ScheduleExecutorService的真正原理需要去阅读源码来理解,下面带着这个问题,以解决这个问题为目标去看一下ScheduleExecutorService的源码吧。
首先,我们使用下面的代码作为测试:
我们设定了调度周期为100毫秒,但是blockRunner实际上需要执行2秒才能返回。关于java的线程池,已经在前面写到了。
先来看一下scheduleAtFixedRate这个方法:
我们的任务command被包装了两次,一次变成了一个ScheduledFutureTask类型的对象,然后又变成了RunnableScheduledFuture类型的对象。然后执行了一个方法delayedExecute,这个方法字面意思上看起来像是延时执行的意思,看一下它的代码:
它的执行逻辑是:如果线程池被关闭了,那么拒绝提交的任务,否则,将该任务添加队列中去。这个队列就是ThreadPoolExecutor中的workQueue,而这个workQueue是在ThreadPoolExecutor的构造函数中被初始化的,也就是下面这关键的一句:
也就是说,我们的任务被添加到了一个DelayedWorkQueue队列中去了,而DelayedWorkQueue我们在Java阻塞队列详解中已经分析过,它是一个可以延迟消费的阻塞队列。而延时的时间是通过接口Delayed的getDelay方法来获得的,我们最后找到ScheduledFutureTask实现了Delayed的getDelay方法。
time变量是什么?原来是delay,好像和period无关啊!!分析了这么久,发现这是第一次执行任务的逻辑啊,我想知道的是第二次、第三次以后和初始的delay无关之后的周期调度的情况啊,继续找吧!
然后发现了ScheduledFutureTask的run方法,很明显这就是任务调度被执行的关键所在,看下代码:
最为关键的地方在于:
首先是:runAndReset()这个方法,然后是setNextRunTime()这个方法,然后是reExecutePeriodic(outerTask)这个方法。
第一个方法runAndReset()貌似是执行我们的提交的任务的,我们看下代码:
关键的地方是c.call()这一句,这个c就是我们提交的任务。
第二个方法setNextRunTime()的意思是设置下次执行的时间,下面是他的代码细节:
我们只需要看p>0这个分支就可以了,其实这是两种策略。我们的示例对应了第一个分支的策略,所以很显然,time这个变量会被加p,而p则是我们设定好的period。下面我们找一下这个time是在哪里初始化的,回忆一下scheduleAtFixedRate这个方法的内,我们说我们的任务被包装了两次,而time就是在这里被初始化的:
无论如何,我们知道一个任务会被运行完一次之后再次设置时间,然后线程池会获取任务来执行,而任务队列是一个延时阻塞队列,所以也就造成了周期性运行的假象。可以看下下面获取任务的take方法:
可以看到,如果delay小于等于0,那么就是说需要被立即调度,否则延时delay这样一段时间。也就是延时消费。
结论就是:一个任务会被重复添加到一个延时任务队列,所以同一时间任务队列中会有多个任务待调度,线程池会首先获取优先级高的任务执行。如果我们的任务运行时间大于设置的调度时间,那么效果就是任务运行多长时间,调度时间就会变为多久,因为添加到任务队列的任务的延时时间每次都是负数,所以会被立刻执行
上面列出了最近写的关于java线程池ScheduleExecutorService的内容,可以作为参考,本文是对ScheduleExecutorService学习和总结的一个收尾,对java线程池技术更为深入的学习和总结将在未来适宜的时候进行。
6.1 ScheduleExecutorService提交死循环任务
首先提出一个问题,如果向一个调度线程池提交一个死循环任务会发生什么?为了内容的完整性,本文会提到一些在上面列出的文章中已经涉及到的内容。
比如我们运行下面的代码:
loopRunner里面只有一个死循环什么也不做,当然这是极端情况,更为一般的情况为在for(;;)里面做一些某种驱动类型的工作,比如Netty的EventLoop一样,那样的循环更有意义,但是本文只是为了学习当向一个调度线程池提交了一个死循环任务之后的运行情况。
下面我们就分析一下scheduleAtFixedRate方法的调用链路:
1、将loopRunner包装成一个ScheduledFutureTask对象,ScheduledFutureTask这个类对于调度线程池至关重要
2、再次包装变为RunnableScheduledFuture对象
3、delayedExecute方法运行,确保任务被正确处理,如果线程池已经被关闭了,那么拒绝任务的提交,否则将任务添加到一个延时队列(workQueue)中去,这是一个具有延时功能的阻塞队列,初始容量为16,每次扩容增加50%的容量,最大容量为Integer.MAX_VALUE
4、运行方法ensurePrestart,确保线程池已经开始工作了,如果线程池里面的线程数量还没有达到设定的corePoolSize,那么就添加一个新的Worker,然后让这个Worker去延时队列去获取任务来执行
5、方法addWorker执行,添加一个Worker,然后让他执行我们提交的任务,下面摘取一段addWorker的方法内容:
6、第五步中最为重要的一句话就是t.start(),这句话的执行会发生什么?首先看这个t是什么东西:
而this就是Worker自身,而Worker是实现了Runnable的,也就是说,t.start()这句话会执行worker自身的run方法
7、我们已经知道现在会执行Worker的run方法,下面是run方法的内容:
首先从Worker中获取其负责的task,如果task为空,那么就去延时队列获取任务,如果没有获取到任务那么线程就可以休息了,如果获取到,那么继续执行下面的内容。主要的就是一句:task.run(),那这句话会发生什么呢?
8、想要知道task.run()之后会发生什么,就需要知道task是个什么东西,第二步的时候说过,也就是我们的任务,只是被包装成了一个RunnableScheduledFuture对象,那现在就去看RunnableScheduledFuture这个方法里面的run会发生什么,下面展示了其run方法的具体细节:
如果这不是一个周期性的任务,那么就执行super的run方法,否则执行runAndReset方法,介于本文是问题导向的文章,所以在此不对super的run方法和runAndReset方法做分析,只要知道这就是执行我们实际提交的任务就好了。也就是说,走到这一步,我们的任务开始运行起来了,也就是我们的那个loopRunner开始无限循环了,下面的代码将永远得不到执行。所以,到这一步就可以解决问题了,向一个调度线程池提交一个死循环的任务,那么这个任务会霸占一个线程一直不会释放,如果很不幸线程池里面只允许有一个线程的话,那么其他提交的任务都将得不到调度执行。
9、为了走通整个流程,我们假设我们提交的不是一个死循环任务,那么提交的任务总是会被执行完的,线程总是会被释放的,那么就会执行setNextRunTime这个方法,下面是这个方法的细节:
p > 0代表的是scheduleAtFixedRate,p < 0代表的是scheduleWithFixedDelay,两者的区别在于前者总是按照设定的轨迹来设定下次应该调度的时间,而后者总是在任务执行完成之后再根据周期设定下一次应该执行的时间。我们只分析前者。对于第一次提交的任务,time等于当前时间 + 首次延时执行的时间,对于delay等于0的情况下,首次提交任务的time就是当前时间,然后 + p代表的是下一次应该被调度的时间。
10、我们发现,每个任务都是在执行完一次之后再设定下次执行任务的时间的,这也特别关键。设定好下次调度的时间,那么就要开始去准备执行它吧,也就是reExecutePeriodic方法会执行,下面是reExecutePeriodic方法的内容:
这个方法只是将任务重新提交到了延时队列而已,一次完整的流程到底也就结束了,为了内容的完整性,再来分析一下一个Worker从延时队列获取任务时的情况。回到第七步,我们有一个方法没有提到,那就是getTask():
我们主要来看两个方法: poll/take,这两个方法都是从延时队列获取一个任务,下面是poll的代码,take会阻塞一直到获取到内容,而poll则不会阻塞,take的代码就不粘了:
poll的代码最为核心的内容就是,获取队列首部的任务,然后获取其延时时间,这个时间是我们在完成一次调度之后设置的下次调度时间,如果任务的运行时间大于我们设定的周期的话,这个延时时间就是负数的,那么就会被立即执行,否则会等到设定的时间,时间到了再返回给Worker执行。
最后把getDelay方法的细节粘出来,这样内容就完整了,其中的time是我们设定的:
七、ThreadLocal
ThreadLocal从字面理解就是线程本地变量,貌似是一种线程私有的缓存变量的容器。为了说明ThreadLocal的特点,举个例子:比如有三个人,每个人比作一个线程,它们都需要一个袋子来装捡到的东西,也就是每个线程都希望自己有一个容器,当然,自己的捡到的东西肯定不希望和别人分享啊,也就是希望这个容器对其他人(线程)是不可见的,如果现在只有一个袋子,那怎么办?
每个人在捡东西之前一定会先抢到那个唯一的袋子,然后再捡东西,如果使用袋子的时间到了,就会马上把里面的东西消费掉,然后把袋子放到原来的地方,然后再次去抢袋子。这个方案是使用锁来避免线程竞争问题的,三个线程需要竞争同一个共享变量。
我们假设现在不是只有一个袋子了,而是有三个袋子,那么就可以给每个人安排一个袋子,然后每个人的袋子里面的对象是对其他人不可见的,这样的好处是解决了多个人竞争同一个袋子的问题。这个方案就是使用ThreadLocal来避免不必要的线程竞争的。
大概了解了ThreadLocal,下面来看看它的使用方法:
上面的例子仅仅是为了说明ThreadLocal可以为每个线程保存一个本地变量,这个变量不会受到其他线程的干扰,你可以使用多个ThreadLocal来让线程保存多个变量,下面我们分析一下ThreadLocal的具体实现细节,首先展示了ThreadLocal提供的一些方法,我们重点关注的是get、set、remove方法。
ThreadLocal方法
首先,我们需要new一个ThreadLocal对象,那么ThreadLocal的构造函数做了什么呢?
很遗憾它什么都没做,那么初始化的过程势必是在首次set的时候做的,我们来看一下set方法的细节:
看起来首先根据当前线程获取到了一个ThreadLocalMap,getMap方法是做了什么?
非常的简洁,是和Thread与生俱来的,我们看一下Thread中的相关定义:
获得了线程的ThreadLocalMap之后,如果不为null,说明不是首次set,直接set就可以了,注意key是this,也就是当前的ThreadLocal啊不是Thread。如果为空呢?说明还没有初始化,那么就需要执行createMap这个方法:
没什么特别的,就是初始化线程的threadLocals,然后设定key-value。
下面分析一下get的逻辑:
和set一样,首先根据当前线程获取ThreadLocalMap,然后判断是否为null,如果为null,说明ThreadLocalMap还没有被初始化啊,那么就返回方法setInitialValue的结果,这个方法做了什么?
最后会返回null,但是会做一些初始化的工作,和set一样。在get里面,如果返回的ThreadLocalMap不为null,则说明ThreadLocalMap已经被初始化了,那么就可以正常根据ThreadLocal作为key获取了。
当线程退出时,会清理ThreadLocal,可以看下面的代码:
这里做了大量“Help GC”的工作。包括我们本节所讲的threadLocals和下一小节要讲的inheritableThreadLocals都会被清理。
如果我们想要显示的清理ThreadLocal,可以使用remove方法:
逻辑较为直接,很好理解。
八、InheritableThreadLocal
ThreadLocal固然很好,但是子线程并不能取到父线程的ThreadLocal的变量,比如下面的代码:
使用ThreadLocal不能继承父线程的ThreadLocal的内容,而使用InheritableThreadLocal时可以做到的,这就可以很好的在父子线程之间传递数据了。下面我们分析一下InheritableThreadLocal的实现细节,下面展示了InheritableThreadLocal提供的方法:
InheritableThreadLocal方法
InheritableThreadLocal继承了ThreadLocal,然后重写了上面三个方法,所以除了上面三个方法之外,其他所有对InheritableThreadLocal的调用都是对ThreadLocal的调用,没有什么特别的。我们上文中提到了Thread类,里面有我们本文关心的两个成员,我们来看一下再Thread中做了哪些工作,我们跟踪一下new一个Thread的调用路径:
Java
运行代码
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
new Thread()
init(ThreadGroup g, Runnable target, String name, long stackSize)

init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals)
->
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
createInheritedMap(ThreadLocalMap parentMap)
ThreadLocalMap(ThreadLocalMap parentMap)
上面列出了最为关键的代码,可以看到,最后会调用ThreadLocal的createInheritedMap方法,而该方法会新建一个ThreadLocalMap,看一下构造函数的内容:
Java
运行代码
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 key = (ThreadLocal) 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++;
}
}
}
}
parentMap就是父线程的ThreadLocalMap,这个构造函数的意思大概就是将父线程的ThreadLocalMap复制到自己的ThreadLocalMap里面来,这样我们就可以使用InheritableThreadLocal访问到父线程中的变量了。
对ThreadLocal更为具体和深入的分析将在其他的篇章中进行,本文点到即可,为了深入理解ThreadLocal,可以阅读ThreadLocalMap的源码,以及可以在项目中多思考是否可以使用ThreadLocal来做一些事情,比如,如果我们具有这样一种线程模型,一个任务从始至终只会被一个线程执行,那么可以使用ThreadLocal来计算运行该任务的时间。
转载自作者:一字马胡
链接:https://www.jianshu.com/p/8c4c160ebdf7

相关文章
|
19小时前
|
NoSQL 关系型数据库 MySQL
Docker-compose容器编排
Docker-Compose是Docker官方开源工具,通过docker-compose.yml文件定义多容器应用,实现一键编排、启动、停止。支持服务管理与依赖配置,简化微服务部署流程,提升开发运维效率。
|
19小时前
|
存储 Java 编译器
Java泛型类型擦除以及类型擦除带来的问题
Java泛型在编译时会进行类型擦除,即泛型信息被移除,替换为原始类型(如Object或限定类型)。擦除后,List&lt;String&gt;和List&lt;Integer&gt;均变为List,导致反射可绕过类型限制。类型检查发生在编译期,针对引用而非对象本身。获取泛型值时自动强转,避免手动转换。但擦除也引发多态冲突、静态成员限制、instanceof失效等问题,需通过桥方法等机制解决。基本类型不可作为泛型参数,静态上下文中的泛型使用也受约束。
|
20小时前
|
Ubuntu Shell Linux
Docker常用命令
本文介绍了Docker常用命令,涵盖启动、停止、重启、状态查看及开机自启等基础操作,以及版本、信息和帮助文档查询。重点讲解镜像相关命令:列出、搜索、下载、删除镜像,虚悬镜像处理,支持命令自动补全配置方法,并演示如何后台运行Linux镜像及利用yum下载依赖包技巧。
|
19小时前
|
Java 大数据
ArrayList扩容机制
ArrayList的add方法添加元素时,先调用ensureCapacityInternal()确保容量。首次添加时,minCapacity为1,经Math.max()后扩容至10。通过grow()方法实现动态扩容,每次扩容为原容量的1.5倍(oldCapacity + (oldCapacity &gt;&gt; 1))。当元素数超过当前容量时触发grow(),如第11个元素加入时,容量从10扩至15。注意:length用于数组,length()用于字符串,size()用于集合。
|
19小时前
|
Java 应用服务中间件 网络安全
Eclipse运行SSM/SSH项目教程
本教程介绍如何在Eclipse中配置Java Web开发环境,涵盖JDK、Tomcat安装与项目导入,支持非Maven及Maven项目。详细演示Eclipse绑定Tomcat、部署项目并启动访问,附在线考试、图书馆管理等实战案例及SSM配置讲解视频,助你快速搭建Java Web项目。
|
19小时前
|
存储 缓存 安全
One Trick Per Day
初始化Map应避免直接指定大小,建议用Guava或手动计算容量;禁用Executors创建线程池,防止OOM,推荐自定义ThreadPoolExecutor;Arrays.asList返回不可变列表,禁止修改操作;遍历Map优先使用entrySet或forEach;SimpleDateFormat非线程安全,建议用ThreadLocal或Java8新时间API;并发修改记录需加锁,推荐乐观锁配合version机制。
|
20小时前
|
关系型数据库 MySQL Shell
Docker下载加速
本文介绍Docker下载加速的两种方法:一是使用网易数帆、阿里云等镜像仓库,如`docker pull hub.c.163.com/library/mysql`;二是配置阿里云镜像加速器,通过修改`/etc/docker/daemon.json`添加加速地址,实现快速拉取官方镜像。此外,若使用代理上网,还可配置HTTP/HTTPS代理,提升镜像下载效率。
|
19小时前
|
数据采集 领域建模 数据库
领域模型图(数据架构/ER图)
数据架构核心输出为ER图,包含实体、关系与属性。通过四色原型法进行领域建模:红色MI表时标事件,绿色PPT为业务对象,黄色Role示参与角色,蓝色DESC提供描述信息。以风控系统为例,从业务流程提炼MI骨架,逐步补充PPT、Role与DESC,最终提取出ER图,明确实体间一对一、一对多或多对多关系,指导数据模型设计。(239字)
|
19小时前
|
uml C语言
系统时序图
时序图(Sequence Diagram)是UML中描述对象间消息传递时间顺序的交互图。横轴为对象,纵轴为时间,展现动态协作过程。它突出交互的时间顺序,直观表达并发行为。主要元素包括角色、对象、生命线、控制焦点和消息(同步、异步、返回及自关联消息),广泛用于系统设计与分析。
|
19小时前
|
存储 消息中间件 开发框架
应用架构图
在业务架构基础上,技术架构将需求转化为技术实现,涵盖分层设计、技术选型与关键组件关系。单体架构通常分为表现、业务、数据和基础四层;分布式架构则体现应用间服务调用与外部系统集成,明确内外边界,形成完整技术体系。