雙非Java的學習之旅以及秋招路程

個人信息:
趁着中秋寫個帖子記錄一下吧。渣渣本,無實習,無高質量證書,走了很多彎路,最後選擇的Java後端。現在算是半躺平了,接了幾個中小廠的offer保底,20w多的薪資,後面還有幾家公司接着面。不是大佬,還有很多比我厲害的雙非戰神!感謝很多前輩還有網友,讓我白嫖了那麼多的資源,趁着中秋假期,寫篇文章總結一下。
 
大一大二划水:
大一學Unity3D做遊戲開發,大二學CTF、滲透測試挖洞,都是跟着社團一起學的。我是在大二的暑假,意識到了自己的一些問題,決定選擇Java後端的方向。這個時候我Java只會用eclipse寫寫for循環。
 
大三上學期:
大三上在b站看了黑馬的999集JavaSE、JavaWEB、狂神的SSM、尚硅谷的SSM、Redis等視頻。然後寒假投了一波小公司的簡歷,面試發現知識遠遠不夠。大三上的寒假繼續學了尚硅谷的SpringBoot、Dubbo、Spring註解驅動原理等,也是這個時候學長告訴我,春招對大三學生來說也非常重要,很多大佬都是大二就出去實習了,我還一直以為大四才用找工作。也是這個時候,我才知道了一個非常友好的平台,牛客網。可以經常看看別人的面試經歷,調整自己的學習重點。
大三下學期:
過完年的寒假投實習崗,阿里、騰訊、位元組、京東、攜程各大廠的一面,沒有一家大廠能進二面的。算法不會,項目沒含金量,我都替面試官尷尬。。意識到了算法的重要性,開始刷劍指offer和leetcode,看博客和視頻跟着做項目。今年5月份開始,繼續投簡歷,繼續面試,開始拿到了一些offer,拿到第一份月薪過萬的工作還挺激動。
但我很清楚自己的弱點,計算機網絡、操作系統、jvm、juc、mysql原理,都不紮實,而這些又是大公司愛問的。就開始瘋狂補這些方面知識。黑馬的jvm和juc就挺好。學了後繼續面試,每一場面試我都會復盤總結,記錄下來,形成自己的小題庫。不會就去學,學不會就去背!其實高頻被問的就那麼些。8月中旬開始,狀態越來越好,只要能進面試,至少不會一輪遊了,都能接着二面,面試過程也能侃侃而談,面試官提出的問題都能答個七七八八。
碎碎念:
最後,秋招算是落下帷幕,有點遺憾但要保持熱愛。學習Java剛好有一年的時間了,當然沒有一些幾個月零基礎上岸大佬的學習效率高,中間也三天打魚兩天晒網過,但總體保持着一個較好的學習狀態。秋招就是一個長跑的過程,投了近百家公司,最累的時候一天四場面試兩場筆試。找工作焦慮很正常,經常睡不着。但一直堅持,相信會有好的結果。麵包會有的,offer也會有的!
 
 
 
附上一點點自己經常被問到的題目,超高頻(答案不一定全對):
1、ArrayList和LinkedList的區別是什麼?
ArrayList的底層是數組實現,初始化的時候數據量是零,當第一次add的時候默認變成10。擴容是每次到之前的1.5倍。特性是查詢速度快,增刪效率低。
擴容條件:每超出數組長度就會進行擴容
擴容分為兩個步驟:1.把原來的數組複製到一個更大的數組中2.把新元素添加到擴容的數組裡。
 
LinkedList的底層是帶有頭節點和尾節點的雙向鏈表,實現了Deque接口所以還可以當雙向隊列使用。特性是適合插入刪除,查詢速度慢。
線程都不安全。
 
如果想要線程安全,又要用List,會怎麼用?
古老的Vector類,底層結構和ArrayList一樣都是數組。與ArrayList的區別是,大部分方法都被synchronized關鍵字修飾,所以是一個線程安全的。擴容和ArrayList有所區別,每次擴容為之前的2倍。
 
2、hashmap的數據結構是什麼?
hashmap在1.7和1.8版本底層數據結構不同:1.7是數組加鏈表,1.8的數據結構是數組加鏈表/紅黑樹的方式。
鏈表和紅黑樹之間的轉換:當鏈表長度大於等於閾值8,並且數組長度大於等於64,將單鏈錶轉化為紅黑樹。紅黑樹節點數量小於等於6的時候,又會重新轉換為單鏈表。
擴容機制:hashmap初始化時創建一個空的數組,在第一次put值時數組大小默認變成16。hashmap的負載因子是0.75,這樣閾值就是16*0.75=12。
hashmap元素個數大於等於閾值時,調用resize()觸發擴容。
resize():創建新的數組代替原有容量小的數組,每次擴容為原來的2倍。擴容後的對象要麼放在原來位置,要麼移動到原偏移量的兩倍的位置。
線程不安全:jdk1.7,添加數據遇到hash碰撞,採用的是頭插法,在多線程環境下會造成循環鏈表死循環。所以jdk1.8改用了尾插法。雖然避免了死循環,但是在多線程情況下,有數據覆蓋或者多次擴容發生。
線程不安全的替代品:ConCurrentHashMap
 
簡述從hashmap中get元素的過程?
先對key進行hash計算,得到的hash值跟數組長度-1進行與運算,得到數組下標。如果命中了桶的第一個節點,直接返回;發生hash衝突,通過key.equals()去找到對應的值。
簡述從hashmap中put元素的過程?
實際調用了putval()方法:
①先調用hash()方法,對key進行hash計算,得到的hash值跟數組長度-1進行與運算,得到數組下標。
②如果桶裏面為null,直接新建節點進行添加;
③如果桶里不為空(發生了hash碰撞),有兩種情況:
如果桶里首個元素和key相同(equals),則直接覆蓋value;
如果key不同,判斷是否為treeNode紅黑樹,如果是則直接在樹中插入鍵值對;否則就是鏈表,遍歷鏈表判斷key是否存在,存在就直接覆蓋value,不存在就插入節點。鏈表插入節點後判斷鏈表長度是否大於8,大於8就鏈錶轉換為紅黑樹。最後,++size,判斷實際大小是否大於閾值,大於就要resize()擴容。
hashmap的hashcode方法如何實現?
將對象的物理地址轉化為一個整數,將整數通過hash計算得到hashcode。
 
3、1)介紹JVM幾種垃圾收集算法?
標記-清除算法Mark Sweep:
分為「標記」和「清除」兩個階段,首先標記出所有存活的對象,標記完成後統一回收所有沒有被標記的對象。它是最基礎的收集算法,後續的算法都
是在對其不足進行改進得到。
產生問題:1.效率問題 2.空間問題(產生大量不連續碎片)
複製算法Copy:
為了解決效率問題。將內存分為相同兩塊,每次使用其中一塊,使用完後將還存活的對象複製到另一個內存塊去,然後把原來的內存塊全部清理
掉。這樣每次都是對一半內存區的回收。
標記-整理算法Mark Compact:
先標記存活的對象,然後讓存活對象向一端移動,然後直接清理掉存活對象區域外的內存。
分代收集算法(當前虛擬機都使用):
根據對象存活周期的不同,將內存分為新生代和老年代。比如在新生代,每次收集都會有大量對象死去,採用複製算法,只需要付出少量對象的複製成本,就可以完成每次垃圾收集。而老年代對象存活幾率比較高,選擇「標記-清除」或「標記-整理」算法進行垃圾收集。
2)新生代和老年代的垃圾收集器有哪些?
Serial收集器:單線程,只使用一個垃圾收集的線程,而且還會stop the world直到它收集結束。
ParNew收集器:Serial的多線程版本。
Parallel Scavenge收集器:關注的是吞吐量(高效率的利用CPU)。jdk1.8默認。
CMS收集器:關注的是用戶線程的停頓時間短。四個步驟:
初始標記:暫停所有的其他線程,把直接與root相連的對象記錄下來,速度很快。
並發標記:同時開啟GC和用戶線程,。。。
重新標記:修正並發標記期間,因為用戶線程繼續運行導致標記變動的記錄。
並發清除:開啟用戶線程,同時GC線程對未標記區域進行清除。
G1收集器:面向服務器的GC。並發與並行,分代收集、空間整合、可預測的停頓。大致四個步驟:
初始標記:
並發標記:
重新標記:
篩選回收:
 
3)總結一下發生full GC的條件有哪些?
 
4)JVM的內存區域(運行時數據區)?這幾個區有什麼作用?為什麼分區?堆的內部結構?
堆:
存放對象實例,幾乎所有的對象實例以及數組都在這裡分配內存。(JDK1.7開始默認開啟了逃逸分析,如果對象引用沒有被外面使用,也就是未逃逸出去,那麼對象可以直接在棧上分配內存)
堆還可以細分為:
JDK1.7:新生代(Eden、From Survivor、To Survivor),老年代,永生代。
JDK1.8:永久代被移除,取而代之的是元空間,元空間使用的是直接內存。(物理上永久代或元空間屬於堆,邏輯上屬於方法區)
方法區(1.8不同):
存儲被虛擬機加載的類信息、常量、靜態變量、即使編譯器編譯後的代碼等數據。為永久代或元空間的邏輯部分。
虛擬機棧:
每次方法調用的數據,都是通過虛擬機棧來傳遞。虛擬機棧由一個個棧幀組成,每次調用方法都有一個對應的棧幀被壓入棧,方法結束棧幀被彈出。每個棧幀都有局部變量表、操作數棧、動態鏈接、方法出口信息。
本地方法棧:
和虛擬機棧類似,區別是,虛擬機使用Native本地方法,就會在本地方法棧創建一個棧幀。
程序計數器:
1.在位元組碼解釋器通過改變程序計數器來依次讀取指令,來實現流程控制,如:順序執行、循環、異常處理。
2.多線程情況下,會發生上下文切換,程序計數器用於記錄當前線程執行的位置,在切換回來的時候知道線程上次運行到哪兒了。
 
4、JAVA線程池實現原理?(不會,底層原理挺難的)
線程池作用:減少每次獲取資源的消耗,提高對資源的利用率;提高響應速度;更加方便進行管理。
阿里開發手冊強制線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式創建。
線程池四大方法:Executors.newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、ScheduledThreadPool。
Executors創建FixedThreedPool創建固定線程池和SingleThreadExecutor:LinkedBlockingQueue,允許請求的隊列長度為 Integer.MAX_VALUE ,可能堆積大量的請求,從而導致 OOM。
Executors創建CachedThreadPool 和 ScheduledThreadPool:允許創建的線程數量為 Integer.MAX_VALUE,可能會創建大量線程,從而導致 OOM。
七大參數:
建議通過ThreadPoolExecutor 的構造方法創建。
ThreadPoolExecutor(int corePoolSize,核心線程數,線程數定義了最小可以同時運行的線程數量。 int maximumPoolSize,當隊列中存放的任務達到隊列容量的時候,當前可以同時運行的線程數量變為最大線程數。 long keepAliveTime,當線程池中的線程數量大於 corePoolSize 的時候,如果這時沒有新的任務提交,核心線程外的救急線程不會立即銷毀,而是會等待,直到等待的時間超過了 keepAliveTime才會被回收銷毀; TimeUnit unit,keepAliveTime 參數的時間單位。 BlockingQueue<Runnable> workQueue,當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,如果達到的話,新任務就會被存放在隊列中。 ThreadFactory threadFactory,executor 線程工廠,創建新線程可以起名字、是否守護線程。 RejectedExecutionHandler handler)拒絕策略。
四種拒絕策略:
 
Redis的數據結構?
String:由多個位元組組成,每個位元組8個bit,也是bitmap的數據結構。
List列表:相當於Java的LinkedList,雙向鏈表(當棧和隊列使用),插入、刪除快,查找(lindex)慢。
Hash:相當於Java的HashMap(數組+鏈表),無序字典。
Set集合:相當於Java的HashSet,無序且唯一。
Sorted set有序集合:相比於set增加了一個權重參數score,使集合中的元素可以有序排列。有點像HashMap和TreeSet的結合。zadd,zcard,zscore,zrange,zrevrange,zrem。
持久化:
快照(RDB):可以通過創建快照來獲得存儲在內存裏面的數據在某個時間點上的副本。數據體積小,從硬盤恢復到內存速度快。因為是一下子把內存中的數據存到硬盤上,比較耗時,產生阻塞,對其他業務有影響。(不適合實時去做,時候幾個小時進行一次備份)
日誌(AOF):每執行一個命令,把redis命令存儲。可以實時存,不斷追加,體積比較大。從硬盤恢復到內存速度慢。
如何解決緩存一致性問題?
對於緩存和數據庫的操作,主要有兩種方式。
方式一:先刪除緩存,再更新數據庫(較多)
有臟數據問題:線程1緩存刪除後,在更新數據庫前。線程2來讀緩存,緩存不存在,讀數據庫,此時數據庫讀到的是舊值,然後把舊值寫入緩存,所以緩存不一致。
解決方案:延時雙刪:先刪除緩存,在更新數據庫時,其他線程發現沒有緩存會讀數據庫舊值,然後把舊值添加到緩存。所以在更新完數據庫後,sleep一段時間(大於其他線程讀寫緩存的時間),然後再次刪除緩存。
方案缺點:影響性能,sleep時間短第二次刪除還是會失敗。
方式二:先更新數據庫,再刪除緩存。保證了最終一致性。(項目)使用緩存的策略:Cache Aside Pattern(旁路緩存模式)
並發問題:更新數據庫成功,刪除緩存失敗,其他線程從緩存讀的是舊值。
解決方案:消息隊列:先更新數據庫,成功後往消息隊列發消息,消費到消息後再刪除緩存,藉助消息隊列的重試機制來實現,達到最終一致性的效果。
方案缺點:問題變得更複雜。而且怎麼保證消息不丟失。
Tags: