Android面试题详细整理系列(三)
??以下這些面試題都是筆者在(2017年2月-2017年3月)這段時間所面試Android工程師的總結而來,面試的公司包括巨頭xx等,還有新貴公司如dd在線科技,gg金融,zk網,momo科技,zbj等,還有小型活力公司如軟都科技,星云顏值,英克科技等,不足之處,還望各位不吝賜教。
1.java中線程同步的方法有哪些?
為什么要線程同步
?java允許多線程并發控制,當多個線程同時操作一個可共享的資源變量時(如數據的增刪改查),將會導致數據不準確,相互之間產生沖突,因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調用,從而保證了該變量的唯一性和準確性。
1.同步方法? 即有synchronized關鍵字修飾的方法。? 由于java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,? 內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處于阻塞狀態。 代碼如:? public synchronized void save(){} 注: synchronized關鍵字也可以修飾靜態方法,此時如果調用該靜態方法,將會鎖住整個類 2.同步代碼塊? 即有synchronized關鍵字修飾的語句塊。? 被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步 代碼如:? synchronized(object){? } 注:同步是一種高開銷的操作,因此應該盡量減少同步的內容。? 通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼即可。? 代碼實例: /*** 線程同步的運用* * @author XIEHEJUN* */public class SynchronizedThread {class Bank {private int account = 100;public int getAccount() {return account;}/*** 用同步方法實現* * @param money*/public synchronized void save(int money) {account += money;}/*** 用同步代碼塊實現* * @param money*/public void save1(int money) {synchronized (this) {account += money;}}}class NewThread implements Runnable {private Bank bank;public NewThread(Bank bank) {this.bank = bank;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {// bank.save1(10);bank.save(10);System.out.println(i + "賬戶余額為:" + bank.getAccount());}}}/*** 建立線程,調用內部類*/public void useThread() {Bank bank = new Bank();NewThread new_thread = new NewThread(bank);System.out.println("線程1");Thread thread1 = new Thread(new_thread);thread1.start();System.out.println("線程2");Thread thread2 = new Thread(new_thread);thread2.start();}public static void main(String[] args) {SynchronizedThread st = new SynchronizedThread();st.useThread();}}
?3.使用特殊域變量(volatile)實現線程同步(提供了可見性) a.volatile關鍵字為域變量的訪問提供了一種免鎖機制,? b.使用volatile修飾域相當于告訴虛擬機該域可能會被其他線程更新,? c.因此每次使用該域就要重新計算,而不是使用寄存器中的值? d.volatile不會提供任何原子操作,它也不能用來修飾final類型的變量? 例如:? 在上面的例子當中,只需在account前面加上volatile修飾,即可實現線程同步。? 代碼實例: ? //只給出要修改的代碼,其余代碼與上同class Bank {//需要同步的變量加上volatileprivate volatile int account = 100;public int getAccount() {return account;}//這里不再需要synchronized public void save(int money) {account += money;}}
?注:多線程中的非同步問題主要出現在對域的讀寫上,如果讓域自身避免這個問題,則就不需要修改操作該域的方法。? 用final域,有鎖保護的域和volatile域可以避免非同步的問題。
? 4.使用重入鎖實現線程同步 在JavaSE5.0中新增了一個java.util.concurrent包來支持同步。? ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,? 它與使用synchronized方法和快具有相同的基本行為和語義,并且擴展了其能力 ReenreantLock類的常用方法有: ReentrantLock() : 創建一個ReentrantLock實例? lock() : 獲得鎖? unlock() : 釋放鎖? 注:ReentrantLock()還有一個可以創建公平鎖的構造方法,但由于能大幅度降低程序運行效率,不推薦使用? 例如:? 在上面例子的基礎上,改寫后的代碼為:? 代碼實例:? //只給出要修改的代碼,其余代碼與上同class Bank {private int account = 100;//需要聲明這個鎖private Lock lock = new ReentrantLock();public int getAccount() {return account;}//這里不再需要synchronized public void save(int money) {lock.lock();try{account += money;}finally{lock.unlock();}}}
? ? 注:關于Lock對象和synchronized關鍵字的選擇:? a.最好兩個都不用,使用一種java.util.concurrent包提供的機制, 能夠幫助用戶處理所有與鎖相關的代碼。? b.如果synchronized關鍵字能滿足用戶的需求,就用synchronized,因為它能簡化代碼? c.如果需要更高級的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現死鎖,通常在finally代碼釋放鎖
5.使用局部變量實現線程同步?
如果使用ThreadLocal管理變量,則每一個使用該變量的線程都獲得該變量的副本,副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響。
ThreadLocal 類的常用方法 ThreadLocal() : 創建一個線程本地變量? get() : 返回此線程局部變量的當前線程副本中的值? initialValue() : 返回此線程局部變量的當前線程的"初始值"? set(T value) : 將此線程局部變量的當前線程副本中的值設置為value 例如:? 在上面例子基礎上,修改后的代碼為:? 代碼實例:? /只改Bank類,其余代碼與上同public class Bank{//使用ThreadLocal類管理共享變量accountprivate static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){@Overrideprotected Integer initialValue(){return 100;}};public void save(int money){account.set(account.get()+money);}public int getAccount(){return account.get();}}???注:ThreadLocal與同步機制? a.ThreadLocal與同步機制都是為了解決多線程中相同變量的訪問沖突問題。? b.前者采用以"空間換時間"的方法,后者采用以"時間換空間"的方式
2.HashMap的實現原理
1. ? HashMap概述:
???HashMap是基于哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,并允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恒久不變。
?
2. ?HashMap的數據結構:
???在java編程語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指針(引用),所有的數據結構都可以用這兩個基本結構來構造的,HashMap也不例外。HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。
從上圖中可以看出,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。
3. ? HashMap的存取實現:
? ?
public V put(K key, V value) { // HashMap允許存放null鍵和null值。 // 當key為null時,調用putForNullKey方法,將value放置在數組第一個位置。 if (key == null) return putForNullKey(value); // 根據key的keyCode重新計算hash值。 int hash = hash(key.hashCode()); // 搜索指定hash值在對應table中的索引。 int i = indexFor(hash, table.length); // 如果 i 索引處的 Entry 不為 null,通過循環不斷遍歷 e 元素的下一個元素。 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 如果i索引處的Entry為null,表明此處還沒有Entry。 modCount++; // 將key、value添加到i索引處。 addEntry(hash, key, value, i); return null; }? ? ? 從上面的源代碼中可以看出:當我們往HashMap中put元素的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那么在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。
4.HashMap、HashTable的區別,以及ConcurrentHashMap的了解
? ??HashTable的應用非常廣泛,HashMap是新框架中用來代替HashTable的類,也就是說建議使用HashMap,不要使用HashTable。
?1.HashTable的方法是同步的,HashMap未經同步,所以在多線程場合要手動同步HashMap這個區別就像Vector和ArrayList一樣。
?2.HashTable不允許null值(key和value都不可以),HashMap允許null值(key和value都可以)。
?3.HashTable有一個contains(Object value),功能和containsValue(Object value)功能一樣。
?4.HashTable使用Enumeration,HashMap使用Iterator。 以上只是表面的不同,它們的實現也有很大的不同。
?5.HashTable中hash數組默認大小是11,增加的方式是old*2+1。HashMap中hash數組的默認大小是16,而且一定是2的指數。
? ? 6.哈希值的使用不同,HashTable直接使用對象的hashCode,而HashMap重新計算hash值,而且用與代替求模。
? HashMap不是線程安全的,因此多線程操作時需要格外小心。在JDK1.5中,偉大的Doug Lea給我們帶來了concurrent包,從此Map也有安全的ConcurrentHashMap。
從ConcurrentHashMap代碼中可以看出,它引入了一個“分段鎖”的概念,具體可以理解為把一個大的Map拆分成N個小的HashTable,根據key.hashCode()來決定把key放到哪個HashTable中。
在ConcurrentHashMap中,就是把Map分成了N個Segment,put和get的時候,都是現根據key.hashCode()算出放到哪個Segment中:
5.java的內存回收機制以及類的加載機制
在Java中,當沒有對象引用指向原先分配給某個對象的內存時,該內存便成為垃圾。JVM的一個系統級線程會自動釋放該內存塊。垃圾收集意味著程序不再需要的對象是"無用信息",這些信息將被丟棄。當一個對象不再被引用的時候,內存回收它占領的空間,以便空間被后來的新對象使用。事實上,除了釋放沒用的對象,垃圾收集也可以清除內存記錄碎片。由于創建對象和垃圾收集器釋放丟棄對象所占的內存空間,內存會出現碎片。碎片是分配給對象的內存塊之間的空閑內存洞。碎片整理將所占用的堆內存移到堆的一端,JVM將整理出的內存分配給新的對象。?
垃圾收集能自動釋放內存空間,減輕編程的負擔。這使Java 虛擬機具有一些優點。首先,它能使編程效率提高。在沒有垃圾收集機制的時候,可能要花許多時間來解決一個難懂的存儲器問題。在用Java語言編程的時候,靠垃圾收集機制可大大縮短時間。其次是它保護程序的完整性, 垃圾收集是Java語言安全性策略的一個重要部份。?
垃圾收集的算法分析?
Java語言規范沒有明確地說明JVM使用哪種垃圾回收算法,但是任何一種垃圾收集算法一般要做2件基本的事情:(1)發現無用信息對象;(2)回收被無用對象占用的內存空間,使該空間可被程序再次使用。?
大多數垃圾回收算法使用了根集(root set)這個概念;所謂根集就量正在執行的Java程序可以訪問的引用變量的集合(包括局部變量、參數、類變量),程序可以使用引用變量訪問對象的屬性和調用對象的方法。垃圾收集首選需要確定從根開始哪些是可達的和哪些是不可達的,從根集可達的對象都是活動對象,它們不能作為垃圾被回收,這也包括從根集間接可達的對象。而根集通過任意路徑不可達的對象符合垃圾收集的條件,應該被回收。下面介紹幾個常用的算法。?
1、? 引用計數法(Reference Counting Collector)?
引用計數法是唯一沒有使用根集的垃圾回收的法,該算法使用引用計數器來區分存活對象和不再使用的對象。一般來說,堆中的每個對象對應一個引用計數器。當每一次創建一個對象并賦給一個變量時,引用計數器置為1。當對象被賦給任意變量時,引用計數器每次加1當對象出了作用域后(該對象丟棄不再使用),引用計數器減1,一旦引用計數器為0,對象就滿足了垃圾收集的條件。?
基于引用計數器的垃圾收集器運行較快,不會長時間中斷程序執行,適宜地必須 實時運行的程序。但引用計數器增加了程序執行的開銷,因為每次對象賦給新的變量,計數器加1,而每次現有對象出了作用域生,計數器減1。?
2、tracing算法(Tracing Collector)?
tracing算法是為了解決引用計數法的問題而提出,它使用了根集的概念。基于tracing算法的垃圾收集器從根集開始掃描,識別出哪些對象可達,哪些對象不可達,并用某種方式標記可達對象,例如對每個可達對象設置一個或多個位。在掃描識別過程中,基于tracing算法的垃圾收集也稱為標記和清除(mark-and-sweep)垃圾收集器.?
3、compacting算法(Compacting Collector)?
為了解決堆碎片問題,基于tracing的垃圾回收吸收了Compacting算法的思想,在清除的過程中,算法將所有的對象移到堆的一端,堆的另一端就變成了一個相鄰的空閑內存區,收集器會對它移動的所有對象的所有引用進行更新,使得這些引用在新的位置能識別原來 的對象。在基于Compacting算法的收集器的實現中,一般增加句柄和句柄表。 ?
4、copying算法(Coping Collector)?
該算法的提出是為了克服句柄的開銷和解決堆碎片的垃圾回收。它開始時把堆分成 一個對象 面和多個空閑面, 程序從對象面為對象分配空間,當對象滿了,基于coping算法的垃圾 收集就從根集中掃描活動對象,并將每個 活動對象復制到空閑面(使得活動對象所占的內存之間沒有空閑洞),這樣空閑面變成了對象面,原來的對象面變成了空閑面,程序會在新的對象面中分配內存。?
一種典型的基于coping算法的垃圾回收是stop-and-copy算法,它將堆分成對象面和空閑區域面,在對象面與空閑區域面的切換過程中,程序暫停執行。?
5、generation算法(Generational Collector)?
stop-and-copy垃圾收集器的一個缺陷是收集器必須復制所有的活動對象,這增加了程序等待時間,這是coping算法低效的原因。在程序設計中有這樣的規律:多數對象存在的時間比較短,少數的存在時間比較長。因此,generation算法將堆分成兩個或多個,每個子堆作為對象的一代(generation)。由于多數對象存在的時間比較短,隨著程序丟棄不使用的對象,垃圾收集器將從最年輕的子堆中收集這些對象。在分代式的垃圾收集器運行后,上次運行存活下來的對象移到下一最高代的子堆中,由于老一代的子堆不會經常被回收,因而節省了時間。?
6、adaptive算法(Adaptive Collector)?
在特定的情況下,一些垃圾收集算法會優于其它算法。基于Adaptive算法的垃圾收集器就是監控當前堆的使用情況,并將選擇適當算法的垃圾收集器。?
類加載機制
JVM的類加載是通過ClassLoader及其子類來完成的,類的層次關系和加載順序可以由下圖來描述:
1)Bootstrap ClassLoader
負責加載$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實現,不是ClassLoader子類
2)Extension ClassLoader
負責加載java平臺中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包
3)App ClassLoader
負責記載classpath中指定的jar包及目錄中class
4)Custom ClassLoader
? ? 屬于應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規范自行實現ClassLoader加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視為已加載此類,保證此類只所有ClassLoader加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。
6.如何判斷兩個鏈表相交?
采用快慢指針方法,遍歷兩個表分別知道兩個表的長度a, b。然后讓長表先走|a-b|步后,短表再開始走,直到相同。時間復雜度為o(m+n),具體的解決思路,可以看看這篇文章判斷兩個鏈表是否相交并找出交點
在寫作本篇博客的時候,借鑒了很多同行的博客,在此感謝zhangshixi,xuefeng0707,小小中,南蠻蟲等
我是kris,See you next time!!!
java筆記--關于線程同步(5種同步方式) 深入Java集合學習系列:HashMap的實現原理? (zhangshixi)
HashMap與ConcurrentHashMap的區別? ? (xuefeng0707)
HashTable 和 HashMap 的區別? ? (小小中)
JVM原理講解和調優
總結
以上是生活随笔為你收集整理的Android面试题详细整理系列(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android面试题详细整理系列(二)
- 下一篇: Android7.0新特性、新功能