三個(gè)爛慫八股文,變成兩個(gè)場(chǎng)景題,打得我一臉懵逼.
你好呀,我是歪歪。
這篇文章來盤一下我最近遇到的兩個(gè)有意思的代碼案例,有意思的點(diǎn)在于,拿到代碼后,你一眼望去,沒有任何毛病。然后一頓分析,會(huì)發(fā)現(xiàn)破綻藏的還比較的深。
幾個(gè)基礎(chǔ)招式的一套組合拳下來,直接把我打懵逼了。
你也來看看,是不是你跺你也麻。

第一個(gè)場(chǎng)景
首先第一個(gè)是這樣的:
一個(gè)讀者給我發(fā)來的一個(gè)關(guān)于線程池使用的疑問,同時(shí)附上了一個(gè)可以復(fù)現(xiàn)問題的 Demo。
我打開 Demo 一看,一共就這幾行代碼,結(jié)合問題描述來看想著應(yīng)該不是啥復(fù)雜的問題:
我拿過來 Demo,根本就沒看代碼,直接扔到 IDEA 里面跑了兩次,想著是先看看具體報(bào)錯(cuò)是什么,然后再去分析代碼。
但是兩次程序都正常結(jié)束了。
好吧,既然沒有異常,我也大概的瞅了一眼 Demo,重點(diǎn)關(guān)注在了 CountDownLatch 的用法上。
我是橫看豎看也沒看出問題,因?yàn)槲乙恢倍际沁@樣用的,這就是正確的用法啊。
于是從拿到 Demo 到定位問題,不到兩分鐘,我直接得出了一個(gè)大膽的結(jié)論,那就是:常規(guī)用法,沒有問題:
然后我們就結(jié)束了這次對(duì)話。
過了一會(huì),我準(zhǔn)備關(guān)閉 IDEA 了。鬼使神差的,我又點(diǎn)了一次運(yùn)行。
你猜怎么著?
居然真的報(bào)錯(cuò)了,拋出了 rejectedExecution 異常,意思是線程池滿了。
哦喲,這就有點(diǎn)意思了。
帶大家一起盤一盤。
首先我們還是過一下代碼,為了減少干擾項(xiàng),便于理解,我把他給我的 Demo 稍微簡化了一點(diǎn),但是整體邏輯沒有發(fā)生任何變化。
簡化后的完整代碼是這樣的,你直接粘過去,引入一個(gè) guava 的包就能跑:
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class Test {
private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(64, 64, 0, TimeUnit.MINUTES, new ArrayBlockingQueue<>(32));
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 400; i++) {
list.add(i);
}
for (int i = 0; i < 100; i++) {
List<List<Integer>> sublist = Lists.partition(list, 400 / 32);
int n = sublist.size();
CountDownLatch countDownLatch = new CountDownLatch(n);
for (int j = 0; j < n; j++) {
threadPoolExecutor.execute(() -> {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("===============> 詳情任務(wù) - 任務(wù)處理完成");
}
System.out.println("都執(zhí)行完成了");
}
}
/**
* <dependency>
* <groupId>com.google.guava</groupId>
* <artifactId>guava</artifactId>
* <version>31.1-jre</version>
* </dependency>
*/
一起分析一波代碼啊。
首先定義了一個(gè)線程池:
private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(64, 64, 0, TimeUnit.MINUTES, new ArrayBlockingQueue<>(32));
該線程池核心大小數(shù)和最大線程數(shù)都是 64,隊(duì)列長度為 32,也就是說這個(gè)線程池同時(shí)能容納的任務(wù)數(shù)是 64+32=96。
main 方法里面是這樣的:
在實(shí)際代碼中,肯定是有具體的業(yè)務(wù)含義的,這里為了脫敏,就用 List 來表示一下,這個(gè)點(diǎn)你知道就行。
編號(hào)為 ① 的地方,是在給往 list 里面放 400 個(gè)數(shù)據(jù),你可以認(rèn)為是 400 個(gè)任務(wù)。
編號(hào)為 ② 的地方,這個(gè) List 是 guava 的 List,含義是把 400 個(gè)任務(wù)拆分開,每一組有 400/32=12.5 個(gè)任務(wù),向下取整,就是 12 個(gè)。
具體是什么個(gè)意思呢,我給你看一下 debug 的截圖你就知道了:
400 個(gè)任務(wù)分組,每一組 12 個(gè)任務(wù),那就可以拆出來 34 組,最后一組只有 4 個(gè)任務(wù):
但是這都不重要,一點(diǎn)都不重要好吧。
因?yàn)楹罄m(xù)他根本就沒有用這個(gè) list ,只是用到了 size 的大小,即 34 。
所以你甚至還能拿到一個(gè)更加簡潔的代碼:
為什么我最開始的時(shí)候不直接給你這個(gè)最簡化的代碼,甚至還讓你多引入一個(gè)包呢?
因?yàn)橥釒煾稻褪窍塍w現(xiàn)這個(gè)簡化代碼的過程。
按照我寫文章的經(jīng)驗(yàn),在定位問題的時(shí)候,一定要盡量多的減少干擾項(xiàng)。排除干擾項(xiàng)的過程,也是梳理問題的過程,很多問題在排除干擾項(xiàng)的時(shí)候,就逐漸的能摸清楚大概是怎么回事兒。
如果你遇到一個(gè)讓你摸不著頭腦的問題,那就先從排除干擾項(xiàng)做起。
好了,說回我們的代碼。現(xiàn)在我們的代碼就只有這幾行了,核心邏輯就是我圈起來的這個(gè)方法:
而圈起來這個(gè)部分,主要是線程池結(jié)合 CountDownLatch 的使用。
對(duì)于 CountDownLatch 我一般只關(guān)注兩個(gè)地方。
第一個(gè)是 new 的時(shí)候傳入的“令牌數(shù)”和調(diào)用 countDown 方法的次數(shù)能不能匹配上。只有保持一致,程序才能正常運(yùn)行。
第二個(gè)地方就是 countDown 方法的調(diào)用是不是在 finally 方法里面。
這兩個(gè)點(diǎn),在 Demo 中都是正確的。
所以現(xiàn)在從程序分析不出來問題,我們?cè)趺崔k?
那就從異常信息往回推算。
我們的異常信息是什么?
觸發(fā)了線程池拒絕策略:
什么時(shí)候會(huì)出現(xiàn)線程池拒絕策略呢?
核心線程數(shù)用完了,隊(duì)列滿了,最大線程數(shù)也用完了的時(shí)候。
但是按理來說,由于有 countDownLatch.await() 的存在,在執(zhí)行完 for 循環(huán)中的 34 次 countDownLatch.countDown() 方法之前,主線程一定是阻塞等待的。
而 countDownLatch.countDown() 方法在 finally 方法中調(diào)用,如果主線程繼續(xù)運(yùn)行,執(zhí)行外層的 for 循環(huán),放新的任務(wù)進(jìn)來,那說明線程池里面的任務(wù)也一定執(zhí)行完成了。
線程池里面的任務(wù)執(zhí)行完成了,那么核心線程就一定會(huì)釋放出來等著接受下一波循環(huán)的任務(wù)。
這樣捋下來,感覺還是沒毛病啊?
除非線程池里面的任務(wù)執(zhí)行完成了,核心線程就一定會(huì)釋放出來等著接受下一波循環(huán)的任務(wù),但是不會(huì)立馬釋放出來。
什么意思呢?
就是當(dāng)一個(gè)核心線程執(zhí)行完成任務(wù)之后,到它進(jìn)入下一次可以開始處理任務(wù)的狀態(tài)之間,有時(shí)間差。
而由于這個(gè)時(shí)間差的存在,導(dǎo)致第一波的核心線程雖然全部執(zhí)行完成了 countDownLatch.countDown(),讓主線程繼續(xù)運(yùn)行下去。但是,在線程池中還有少量線程未再次進(jìn)入“可以處理任務(wù)”的狀態(tài),還在進(jìn)行一些收尾的工作。
從而導(dǎo)致,第二波任務(wù)進(jìn)來的時(shí)候,需要開啟新的核心線程數(shù)來執(zhí)行。
放進(jìn)來的任務(wù)速度,快于核心線程的“收尾工作”的時(shí)間,最終導(dǎo)致線程池滿了,觸發(fā)拒絕策略。
需要說明的是,這個(gè)原因都是基于我個(gè)人的猜想和推測(cè)。這個(gè)結(jié)論不一定真的正確,但是偉人曾經(jīng)說過:大膽假設(shè),小心求證。
所以,為了證明這個(gè)猜想,我需要找到實(shí)錘證據(jù)。
從哪里找實(shí)錘呢?
源碼之下,無秘密。
當(dāng)我有了這個(gè)猜想之后,我立馬就想到了線程池的這個(gè)方法:
java.util.concurrent.ThreadPoolExecutor#runWorker
標(biāo)號(hào)為 ① 的地方是執(zhí)行線程 run 方法,也就是這一行代碼執(zhí)行完成之后,一個(gè)任務(wù)就算是執(zhí)行完成了。對(duì)應(yīng)到我們的 Demo 也就是這部分執(zhí)行完成了:
這部分執(zhí)行完成了,countDownLatch.countDown() 方法也執(zhí)行完成了。
但是這個(gè)核心線程還沒跑完呢,它還要繼續(xù)往下走,執(zhí)行標(biāo)號(hào)為 ② 和 ③ 處的收尾工作。
在核心線程執(zhí)行“收尾工作”時(shí),主線程又咔咔就跑起來了,下一波任務(wù)就扔進(jìn)來了。
這不就是時(shí)間差嗎?
另外,我再問一個(gè)問題:線程池里面的一個(gè)線程是什么時(shí)候處于“來吧,哥們,我可以處理任務(wù)了”的狀態(tài)的?
是不是要執(zhí)行到紅框框著的這個(gè)地方 WAITING 著:
java.util.concurrent.ThreadPoolExecutor#getTask
那在執(zhí)行到這個(gè)紅框框之前,還有一大坨代碼呢,它們不是收尾工作,屬于“就緒準(zhǔn)備工作”。
現(xiàn)在我們?cè)俎垡晦郯 ?/p>
線程池里面的一個(gè)線程在執(zhí)行完成任務(wù)之后,到下一次可以執(zhí)行任務(wù)的狀態(tài)之間,有一個(gè)“收尾工作”和“就緒準(zhǔn)備工作”,這兩個(gè)工作都是非常快就可以執(zhí)行完成的。
但是這“兩個(gè)工作”和“主線程繼續(xù)往線程池里面扔任務(wù)的動(dòng)作”之間,沒有先后邏輯控制。
從程序上講,這是兩個(gè)獨(dú)立的線程邏輯,誰先誰后,都有可能。
如果“兩個(gè)工作”先完成,那么后面扔進(jìn)來的任務(wù)一定是可以復(fù)用線程的,不會(huì)觸發(fā)新開線程的邏輯,也就不會(huì)觸發(fā)拒絕策略。
如果“主線程繼續(xù)往線程池里面扔任務(wù)的動(dòng)作”先完成,那么就會(huì)先開啟新線程,從而有可能觸發(fā)拒絕策略。
所以最終的執(zhí)行結(jié)果可能是不報(bào)錯(cuò),也可能是拋出異常。
同時(shí)也回答了這個(gè)問題:為什么提高線程池的隊(duì)列長度,就不拋出異常了?
因?yàn)殛?duì)列長度越長,核心線程數(shù)不夠的時(shí)候,任務(wù)大不了在隊(duì)列里面堆著。而且只會(huì)堆一小會(huì)兒,但是這一小會(huì),給了核心線程足夠的時(shí)間去完成“兩個(gè)工作”,然后就能開始消耗隊(duì)列里面的任務(wù)。
另外,提出問題的小伙伴說換成 tomcat 的線程池就不會(huì)被拒絕了:
也是同理,因?yàn)?tomcat 的線程池重寫了拒絕策略,一個(gè)任務(wù)被拒絕之后會(huì)進(jìn)行重試,嘗試把任務(wù)仍回到隊(duì)列中去,重試是有可能會(huì)成功的。
對(duì)應(yīng)的源碼是這個(gè)部分:
org.apache.tomcat.util.threads.ThreadPoolExecutor#execute(java.lang.Runnable, long, java.util.concurrent.TimeUnit)
這就是我從源碼中找到的實(shí)錘。
但是我覺得錘的還不夠死,我得想辦法讓這個(gè)問題必現(xiàn)一下。
怎么弄呢?
如果要讓問題必現(xiàn),那么就是延長“核心線程完成兩個(gè)工作”的時(shí)間,讓主線程扔任務(wù)的動(dòng)作”的動(dòng)作先于它完成。
很簡單,看這里,afterExecute 方法:
線程池給你留了一個(gè)統(tǒng)計(jì)數(shù)據(jù)的口子,我們就可以基于這個(gè)口子搞事情嘛,比如睡一下下:
private static final ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(64, 64, 0, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(32)) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
由于收尾任務(wù)的時(shí)間過長,這樣“主線程扔任務(wù)的動(dòng)作”有極大概率的是先執(zhí)行的,導(dǎo)致觸發(fā)拒絕策略:
到這里,這個(gè)問題其實(shí)就算是分析完成了。
但是我還想分享一個(gè)我在驗(yàn)證過程中的一個(gè)驗(yàn)證思路,雖然這個(gè)思路最終并沒有得到我想要的結(jié)論,但是技多不壓身,你抽空學(xué)學(xué),以后萬一用得上呢。
前面說了,在我重寫了 afterExecute 方法之后,一定會(huì)觸發(fā)拒絕策略。
那么我在觸發(fā)拒絕策略的時(shí)候,dump 一把線程,通過 dump 文件觀察線程狀態(tài),是不是就可以看到線程池里面的線程,可能還在 RUNNING 狀態(tài),但是是在執(zhí)行“兩個(gè)工作”呢?
于是就有了這樣的代碼:
我自定義了一個(gè)拒絕策略,在觸發(fā)拒絕策略的時(shí)候,dump 一把線程池:
但是很不幸,最終 dump 出來的結(jié)果并不是我期望的,線程池里面的線程,不是在 TIMED_WAITING 狀態(tài)就是在 WAITING 狀態(tài),沒有一個(gè)是 RUNNING 的。
為什么?
很簡單,因?yàn)樵谟|發(fā)拒絕策略之后,dump 完成之前,這之間代碼執(zhí)行的時(shí)間,完全夠線程池里面的線程完成“兩個(gè)工作”。
雖然你 dump 了,但是還是晚了一點(diǎn)。
這一點(diǎn),可以通過在 dump 前面輸出一點(diǎn)日志進(jìn)行觀察驗(yàn)證:
雖然我沒有通過 dump 文件驗(yàn)證到我的觀點(diǎn),但是你可以學(xué)習(xí)一下這個(gè)手段。
在正常的業(yè)務(wù)邏輯中觸發(fā)拒絕策略的時(shí)候,可以 dump 一把,方便你分析。
那么問題就來了?
怎么去 dump 呢?
關(guān)鍵代碼就這一行:
JVMUtil.jstack(jStackStream);
這個(gè)方法其實(shí)是 Dubbo 里面的一個(gè)工具,我只是引用了一下 Dubbo 的包:
但是你完全可以把這個(gè)工具類粘出去,粘到你的項(xiàng)目中去。
你的代碼很好,現(xiàn)在它是我的了。
最后,我還是必須要再補(bǔ)充一句:
以上從問題的定位到問題的復(fù)現(xiàn),都是基于我個(gè)人的分析,從猜測(cè)出發(fā),最終進(jìn)行驗(yàn)證的。有可能我猜錯(cuò)了,那么整個(gè)論證過程可能都是錯(cuò)的。你可以把 Demo 粘過去跑一跑,帶著懷疑一切的眼光去審視它,如果你有不同的看法,可以告訴我,我也學(xué)習(xí)一下。
最后,你想想整個(gè)過程。
拆開了看,無非是線程池和 CountDownLatch 的八股文的考察,這兩個(gè)玩意都是面試熱點(diǎn)考察部分,大家應(yīng)該都背的滾瓜爛熟。
在實(shí)際工作中,這兩個(gè)東西碰撞在一起也是經(jīng)常有的寫法,但是沒想到的是,在套上一層簡單的 for 循環(huán)之后,完全就變成了一個(gè)復(fù)雜的問題了。
這玩意著實(shí)是把我打懵逼了。以后把 CountDownLatch 放在 for 循環(huán)里面的場(chǎng)景,都需要多多注意一下了。
第二個(gè)場(chǎng)景
這個(gè)場(chǎng)景就簡單很多了。
當(dāng)時(shí)有個(gè)小伙伴在群里扔了一個(gè)截圖:
需要注意的是, if(!lock) 他截圖的時(shí)候是給錯(cuò)了,真實(shí)的寫法是 if(lock),lock 為 true 的時(shí)候就是加鎖成功,進(jìn)入 if。
同時(shí)這個(gè)代碼這一行是有事務(wù)的:
寫一個(gè)對(duì)應(yīng)的偽代碼是這樣的:
if(加鎖成功){
try{
//save有事務(wù)注解,并且確認(rèn)調(diào)用的service對(duì)象是被代理的對(duì)象,即事務(wù)的寫法一定是正確的
return service.save();
} catch(Exception e){
//異常打印
} finally {
//釋放鎖
unlock(lockKey);
}
}
就上面這個(gè)寫法,先加鎖,再開啟事務(wù),執(zhí)行事務(wù)方法,接著提交事務(wù),最后解鎖,反正歪師傅橫看豎看是沒有發(fā)現(xiàn)有任何毛病的。
但是提供截圖的小伙伴是這樣描述的。
當(dāng)他是這樣寫的時(shí)候,從結(jié)果來看,程序是先加鎖,再開啟事務(wù),執(zhí)行事務(wù)方法,然后解鎖,最后才提交事務(wù):
當(dāng)時(shí)我就覺得:這現(xiàn)象完全超出了我的認(rèn)知,絕不可能。
緊接著他提供了第二張截圖:
他說這樣拆開寫的時(shí)候,事務(wù)就能正常生效了:
這兩個(gè)寫法的唯一區(qū)別就是一個(gè)是直接 return,一個(gè)是先返回了一個(gè) resultModel 然后在 return。
在實(shí)際效果上,我認(rèn)為是沒有任何差異的。
但是他說這樣寫會(huì)導(dǎo)致鎖釋放的時(shí)機(jī)不一樣。
我還是覺得:
然而突然有人冒出來說了一句:try 帶著 finally 的時(shí)候,在執(zhí)行 return 語句之前會(huì)先執(zhí)行 finally 里面的邏輯。會(huì)不會(huì)是這個(gè)原因?qū)е碌哪兀?/p>
按照這個(gè)邏輯推,先執(zhí)行了 finally 里面的釋放鎖邏輯,再執(zhí)行了 return 語句對(duì)應(yīng)的表達(dá)式,也就是事務(wù)的方法。那么確實(shí)是會(huì)導(dǎo)致鎖釋放在事務(wù)執(zhí)行之前。
就是這句話直接給我干懵逼了,CPU 都快燒了,感覺哪里不對(duì),又說不上來為什么。
雖然很反直覺,但是我也記得八股文就是這樣寫的啊,于是我開始覺得有點(diǎn)意思了。
所以我搞了一個(gè) Demo,準(zhǔn)備本地復(fù)現(xiàn)一下。
當(dāng)時(shí)想著,如果能復(fù)現(xiàn),這可是一個(gè)違背直覺的巨坑啊,是一個(gè)很好的寫作素材。
可惜,沒有復(fù)現(xiàn):
最后這個(gè)哥們也重新去定位了原因,發(fā)現(xiàn)是其他的 BUG 導(dǎo)致的。
另外,關(guān)于前面“try 帶著 finally”的說法其實(shí)說的并不嚴(yán)謹(jǐn),應(yīng)該是當(dāng) try 中帶有 return 時(shí),會(huì)先執(zhí)行 return 前的代碼,然后把需要 return 的信息暫存起來,接著再執(zhí)行 finally 中的代碼,最后再通過 return 返回之前保存的信息。
這才是寫在八股文里面的正確答案。
要永遠(yuǎn)牢記另一位偉人說過:實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)。
遇事不決,就搞個(gè) Demo 跑跑。
關(guān)于這個(gè)場(chǎng)景,拆開來看就是關(guān)于事務(wù)和鎖碰撞在一起時(shí)的注意事項(xiàng)以及 try-return-finally 的執(zhí)行順序這兩個(gè)基礎(chǔ)八股而已。
但是當(dāng)著兩個(gè)糅在一起的時(shí)候,確實(shí)有那么幾個(gè)瞬間讓我眼前一黑,又打得我一臉懵逼。
最后,事務(wù)和鎖碰撞在一起的情況,上個(gè)偽代碼:
@Service
public class ServiceOne{
// 設(shè)置一把可重入的公平鎖
private Lock lock = new ReentrantLock(true);
@Transactional(rollbackFor = Exception.class)
public Result func(long seckillId, long userId) {
lock.lock();
// 執(zhí)行數(shù)據(jù)庫操作——查詢商品庫存數(shù)量
// 如果 庫存數(shù)量 滿足要求 執(zhí)行數(shù)據(jù)庫操作——減少庫存數(shù)量——模擬賣出貨物操作
lock.unlock();
}
}
如果你五秒鐘沒看出這個(gè)代碼的問題,秒殺這個(gè)問題的話,那歪師傅推薦你個(gè)假粉絲看看這篇文章:《幾行爛代碼,我賠了16萬。》
好了,就醬,打完收工~
好啦,本文的技術(shù)部分就到這里了。
下面這個(gè)環(huán)節(jié)叫做[荒腔走板],技術(shù)文章后面我偶爾會(huì)記錄、分享點(diǎn)生活相關(guān)的事情,和技術(shù)毫無關(guān)系。我知道看起來很突兀,但是我喜歡,因?yàn)檫@是一個(gè)普通博主的生活氣息。
荒腔走板

1 月 17 日的時(shí)候,手機(jī)上某 APP 突然給我彈了一個(gè)消息,說“還記得十年前的今天在干什么嗎”。
于是我點(diǎn)進(jìn)去一看,就是上面這張照片。
10 年前,我還在讀大二,1 月 17 日應(yīng)該是學(xué)校放寒假了,這張照片是我從成都做大巴車回老家的時(shí)候拍得。
那一年從成都到老家還沒有通動(dòng)車,每次回家都是買大巴車的票回去,一路上要坐超過 3 個(gè)小時(shí)的車,遇上堵車的情況,那就不知道什么時(shí)候能回家了。
記得有一年小長假,我和 Max 同學(xué)一起從成都回老家,當(dāng)時(shí)也不能提前在網(wǎng)上購票,都是需要到現(xiàn)場(chǎng)窗口購買。結(jié)果小長假車站的人流量非常可怕,我們一大早就到了,但是只買到了當(dāng)天下午很晚的一班車。
那個(gè)時(shí)候購車票我記得甚至不需要實(shí)名制,于是我就在退票的地方和等車的地方像個(gè)社牛一樣大聲呼喊,有沒有要換 xx 班次車的。
沒想到還真的從一個(gè)大哥手上換到了兩張上午的車票,他想下午再走。
Max 同學(xué)一路上都在夸我:真機(jī)智。
這些記憶隨著動(dòng)車的開通,也就慢慢的成為了遙遠(yuǎn)的歷史。
2019 年 10 月底,老家的車站也搬走了。
那一年我 25 歲,這個(gè)車站是我在老家居住時(shí),每天的必經(jīng)之路,我?guī)缀踉谶@個(gè)車站門口來來回回走了 25 年。小的時(shí)候,父母就是做長途汽車回家過年的,我記得每次去接他們的時(shí)候,都是在晚上,站在出站口,很冷,伸著脖子看著每一輛進(jìn)站的大巴車。長大一點(diǎn)了,我就在這個(gè)車站坐車去鄉(xiāng)下讀書,周末放假又從鄉(xiāng)下坐車回到這個(gè)車站。寒來暑往,25 年。
2019 年,隨著老家動(dòng)車的開通,它從老城區(qū)遷走了,換到了城郊動(dòng)車站附近。
看著自己熟悉的東西漸漸的消失在自己的視野中。
我很懷念它們。
·············· END ··············

推薦?? : 盤一盤這個(gè)沒資格出現(xiàn)在面試環(huán)節(jié)的場(chǎng)景題。
推薦?? : 從一道關(guān)于定時(shí)任務(wù)的面試題說起
推薦?? : Spring解決泛型擦除的思路不錯(cuò),現(xiàn)在它是我的了。
推薦?? : 一個(gè)普通程序員磕磕絆絆,又閃閃發(fā)光的十年。
你好呀,我是歪歪。我沒進(jìn)過一線大廠,沒創(chuàng)過業(yè),也沒寫過書,更不是技術(shù)專家,所以也沒有什么亮眼的title。
當(dāng)年高考,隨緣調(diào)劑到了某二本院校計(jì)算機(jī)專業(yè)。純屬誤打誤撞,進(jìn)入程序員的行列,之后開始了運(yùn)氣爆棚的程序員之路。
說起程序員之路還是有點(diǎn)意思,可以 點(diǎn)擊藍(lán)字,查看我的程序員之路。
