本文是Android面試題整理中的一篇,結合右下角目錄食用更佳,包括:html
線程是操做系統可以進行調度的最小單位,它被包含在進程之中,是進程中的實際運做單位,可使用多線程對進行運算提速。java
- 一種是繼承Thread類;
- 另外一種是實現Runnable接口。兩種方式都要經過重寫run()方法來定義線程的行爲,推薦使用後者,由於Java中的繼承是單繼承,一個類有一個父類,若是繼承了Thread類就沒法再繼承其餘類了,顯然使用Runnable接口更爲靈活。
- 實現Callable接口,該接口中的call方法能夠在線程執行結束時產生一個返回值
FutureTask實現了Future接口和Runnable接口,能夠對任務進行取消和獲取返回值等操做。android
作不到,和gc同樣,只能通知系統,具體什麼時候啓動有系統控制git
啓動一個線程是調用start()方法,使線程所表明的虛擬處理機處於可運行狀態,這意味着它能夠由JVM 調度並執行,這並不意味着線程就會當即運行程序員
- wait( ):Object方法,必須在同步代碼塊或同步方法中使用,使當前線程處於等待狀態,釋放鎖
- notify ( ):Object方法,和wait方法聯合使用,通知一個線程,具體通知哪一個由jvm決定,使用不當可能發生死鎖
- notifyAll ( ):Object方法,和wait方法聯合使用,通知全部線程,具體哪一個線程得到運行權jvm決定
- sleep( ):使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要處理InterruptedException異常
- Synchronized修飾方法
- Synchronized修飾代碼塊
- Lock/ReadWriteLock
- ThreadLocal:每一個線程都有一個局部變量的副本,互不干擾。一種以空間換時間的方式
- java中有不少線程安全的容器和方法,能夠幫助咱們實現線程同步:如Collections.synchronizedList()方法將List轉爲線程同步;用ConurrentHashMap 實現hashmap的線程同步。BlockingQueue阻塞隊列也是線程同步的,很是適用於生產者消費者模式
- 擴展:volatile(volatile修飾的變量不會緩存在寄存器中,每次使用都會從主存中讀取):保證可見性,不保證原子性,所以不是線程安全。在一寫多讀/狀態標誌的場景中使用
所謂重入鎖,指的是以線程爲單位,當一個線程獲取對象鎖以後,這個線程能夠再次獲取本對象上的鎖,而其餘的線程是不能夠的github
- Java提供了很豐富的API但沒有爲中止線程提供API
- 能夠用volatile 布爾變量來退出run()方法的循環或者是取消任務來中斷線程
- 若是異常沒有被捕獲該線程將會中止執行
- 能夠用UncaughtExceptionHandler來捕獲這種異常
- 使用同一個runnable對象
- 使用不一樣的runnable對象,將同一共享數據實例傳給不一樣的runnable
- 使用不一樣的runnable對象,將這些Runnable對象做爲一個內部類,將共享數據做爲成員變量
- 給線程起個有意義的名字
- 避免使用鎖和縮小鎖的範圍
- 多用同步輔助類(CountDownLatch、CyclicBarrier、Semaphore)少用wait、notify
- 多用併發集合少用同步集合
- 供線程內的局部變量,線程獨有,不與其餘線程共享
- 適用場景:多線程狀況下某一變量不須要線程間共享,須要各個線程間相互獨立
- ThreadLocal經過得到Thread實例內部的ThreadLocalMap來存取數據
- ThreadLocal實例自己做爲key值
- 若是使用線程池,Threadlocal多是上一個線程的值,須要咱們顯示的控制
- ThreadLocal的key雖然採用弱引用,可是仍然可能形成內存泄漏(key爲null,value還有值)
擴展:Android中的ThreadLocal實現略有不一樣,使用Thread實例中的是數組存值,經過ThreadLocal實例計算一個惟一的hash肯定下標。
- 線程內的異常能夠捕獲,若是沒有捕獲,該線程會中止運行退出
- 不管是正常退出仍是異常退出,同步塊中的鎖都會釋放
兩個線程互相等待對方釋放資源才能繼續執行下去,這個時候就造成了死鎖,誰都沒法繼續執行(或者多個線程循環等待)面試
以一樣的順序加鎖和釋放鎖編程
處於等待狀態的線程可能會收到錯誤警報和僞喚醒,若是不在循環中檢查等待條件,程序就會在沒有知足結束條件的狀況下退出數組
- 同步集合與併發集合都爲多線程和併發提供了合適的線程安全的集合
- 併發集合性能更高
這是上題的擴展,活鎖和死鎖相似,不一樣之處在於處於活鎖的線程或進程的狀態是不斷改變的,活鎖能夠認爲是一種特殊的飢餓。一個現實的活鎖例子是兩個 人在狹小的走廊碰到,兩我的都試着避讓對方好讓彼此經過,可是由於避讓的方向都同樣致使最後誰都不能經過走廊。簡單的說就是,活鎖和死鎖的主要區別是前者進程的狀態能夠改變可是卻不能繼續執行緩存
java.lang.Thread中有一個方法叫holdsLock(),它返回true若是當且僅當當前線程擁有某個具體對象的鎖
ConcurrentHashMap把實際map劃分紅若干部分來實現它的可擴展性和線程安全。這種劃分是使用併發度得到的,它是 ConcurrentHashMap類構造函數的一個可選參數,默認值爲16,這樣在多線程狀況下就能避免爭用
阻塞式方法是指程序會一直等待該方法完成期間不作其餘事情,ServerSocket的accept()方法就是一直等待客戶端鏈接。這裏的阻塞是 指調用結果返回以前,當前線程會被掛起,直到獲得結果以後纔會返回。此外,還有異步和非阻塞式方法在任務完成前就返回。
忙循環就是程序員用循環讓一個線程等待,不像傳統方法wait(), sleep() 或 yield() 它們都放棄了CPU控制,而忙循環不會放棄CPU,它就是在運行一個空循環。這麼作的目的是爲了保留CPU緩存,在多核系統中,一個等待線程醒來的時候可 能會在另外一個內核運行,這樣會重建緩存。爲了不重建緩存和減小等待重建的時間就可使用它了。
可使用synchronized保證原子性,也可使用AtomicInteger類
擴展:volatile只能保證可見性,不能保證原子性,所以不行
Java中能夠對類、對象、方法或是代碼塊上鎖
- 同步代碼塊能夠指定更小的粒度
- 同步代碼塊能夠給指定實例加鎖
類鎖其實時一種特殊的對象鎖,它鎖的其實時類對應的class對象
- 兩個方法都是暫停線程,釋放cpu資源給其餘線程
- sleep是Thread的靜態方法,wait是Object的方法。
- sleep使線程進入阻塞狀態;wait使線程進入等待狀態,靠其餘線程notify或者notifyAll來改變狀態
- sleep能夠在任何地方使用,必須捕獲異常;而wait必須在同步方法或者同步塊中使用,不然會拋出運行時異常
- 最重要的:sleep繼續持用鎖,wait釋放鎖 擴展:yield中止當前線程,讓同優先級或者優先級高的線程先執行(但不會釋放鎖);join方法在某一個線程的執行過程當中調用另外一個線程執行,等到被調用的線程執行結束後,再繼續執行當前線程
- sleep方法使當前線程阻塞指定時間,隨後進入就緒狀態
- yield方法使當前線程進入就緒狀態,讓同優先級或者更高優先級的線程先執行
- sleep方法會拋出interruptedException
JAVA提供的鎖是對象級的而不是線程級的,每一個對象都有鎖,通 過線程得到。若是線程須要等待某些鎖那麼調用對象中的wait()方法就有意義了。若是wait()方法定義在Thread類中,線程正在等待的是哪一個鎖 就不明顯了
- java規定必須在同步塊中,不在同步塊中會拋出異常
- 若是不在同步塊中,有可能notify在執行的時候,wait沒有收到陷入死鎖
synchronized 用於線程同步
- 能夠修飾方法
- 能夠修飾代碼塊
- 當持有的鎖是類時,那麼全部實例對象調用該方法或者代碼塊都會被鎖
- synchronized修飾靜態方法時,鎖是類,全部的對象實例用同一把鎖
- 修飾普通方法時,鎖是類的實例
不能。其它線程只能訪問該對象的非同步方法。第一個線程持有了對象鎖,第二個線程的同步方法也須要該對象的鎖才能運行,只能在鎖池中等待了。
- volatile是一個修飾符,只能修飾成員變量
- volatile保證了變量的可見性(A線程的改變,B線程立刻能夠獲取到)
- volatile禁止進行指令重排序
private static volatile Singleton instance;
private Singleton(){}
public Singleton getInstance(
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return sinlgeton;
)
複製代碼
- 要加
- 兩個線程同時訪問雙檢鎖,有可能指令重排序,線程1初始化一半,切換到線程2;由於初始化不是一個原子操做,此時線程2讀到不爲null直接使用,可是由於尚未初始化完成引發崩潰
- Synchronized時java關鍵字,Lock/ReadWriteLock接口,它們都是可重入鎖
- Synchronized由虛擬機控制,不須要用戶去手動釋放鎖,執行完畢後自動釋放;而Lock是用戶顯示控制的,要用戶去手動釋放鎖,若是沒有主動釋放鎖,就有可能致使出現死鎖現象。
- Lock能夠用更多的方法,好比tryLock()拿到鎖返回true,不然false;tryLock(long time, TimeUnit unit)方法和tryLock()方法是相似的,只不過區別在於這個方法在拿不到鎖時會等待必定的時間;Lock有lockInterruptibly()方法,是可中斷鎖
- ReentrantLock能夠實現公平鎖(等得久的先執行)
- ReadWriteLock是一個接口,ReentrantReadWriteLock是它的一個實現,將對一個資源(好比文件)的訪問分紅了2個鎖,一個讀鎖和一個寫鎖,提升了讀寫效率。
LockSupport是JDK中比較底層的類,用來建立鎖和其餘同步工具類的基本線程阻塞原語
park 方法獲取許可。許可默認是被佔用的,調用park()時獲取不到許可,因此進入阻塞狀態 unpark 方法頒發許可
- 讀寫分離的鎖,能夠提高效率
- 讀讀能共存,讀寫、寫寫不能共存
- RetrantLock 是經過CAS和AQS實現的
- CAS(Compare And Swap):三個參數,一個當前內存值V、舊的預期值A、即將更新的值B,當且僅當預期值A和內存值V相同時,將內存值修改成B並返回true,不然什麼都不作,並返回false。原子性操做
- RetrantLock內部有一個AbstractQueuedSynchronizer實例,AbstractQueuedSynchronizer是一個抽象類,RetrantLock中有兩種對他的實現,一種是公平鎖,一種是非公平鎖
- 在lock時,調用一個CAS的方法compareAndSet來將state設置爲1,state是一個volitale的變量,並將當前線程和鎖綁定
- 當compareAndSet失敗時,嘗試獲取鎖:若是和鎖綁定的線程時當前線程,state+1
- 若是獲取鎖失敗,將其加入到隊列中等待,從而保證了併發執行的操做變成了串行
- 擴展:公平鎖和非公平鎖的區別:非公平鎖無視隊列,直接查看當前可不能夠拿到鎖;公平鎖會先查看隊列,隊列非空的話會加入隊列
synchronized 的實現原理以及鎖優化?:Monitor
volatile 的實現原理?:內存屏障
CAS?CAS 有什麼缺陷,如何解決?CompareAndSwap,經過cpu指令實現的
AQS :AbstractQueueSynchronizer,是ReentrantLock一個內部類
如何檢測死鎖?怎麼預防死鎖?:死鎖必須知足四個條件,破壞任意一個條件均可以解除死鎖
Fork/Join框架
- 頻繁的建立和銷燬對象很耗費資源,因此java引入了線程池。Java 5+中的Executor接口定義一個執行線程的工具。它的子類型即線程池接口是ExecutorService。
- Executors 是一個工具類,能夠幫咱們生成一些特性的線程池
newSingleThreadExecutor:建立一個單線程化的Executor,保證全部任務按照指定順序(FIFO, LIFO, 優先級)執行。
newFixedThreadPool:建立一個指定工做線程數量的線程池。每當提交一個任務就建立一個工做線程,若是工做線程數量達到線程池初始的最大數,則將提交的任務存入到池隊列中。
newCachedThreadPool:建立一個可緩存線程池,若是線程池長度超過處理須要,可靈活回收空閒線程,若無可回收,則新建線程。
newScheduleThreadPool:建立一個定長的線程池,並且支持定時的以及週期性的任務執行,支持定時及週期性任務執行。
複製代碼
- 咱們經常使用的ThreadPoolExecutor實現了ExecutorService接口,如下是原理和參數說明
原理:
step1.調用ThreadPoolExecutor的execute提交線程,首先檢查CorePool,若是CorePool內的線程小於CorePoolSize,新建立線程執行任務。
step2.若是當前CorePool內的線程大於等於CorePoolSize,那麼將線程加入到BlockingQueue。
step3.若是不能加入BlockingQueue,在小於MaxPoolSize的狀況下建立線程執行任務。
step4.若是線程數大於等於MaxPoolSize,那麼執行拒絕策略。
參數說明:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize 核心線程池大小
maximumPoolSize 線程池最大容量大小
keepAliveTime 線程池空閒時,線程存活的時間
TimeUnit 時間單位
ThreadFactory 線程工廠
BlockingQueue任務隊列
RejectedExecutionHandler 線程拒絕策略
擴展:ThreadPoolExecutor 的submit和excute方法都能執行任務,有什麼區別?
1. 入參不一樣:excute只能接受Runnable,submit能夠接受Runnable和Callable
2. submit有返回值
3. 在異常處理時,submit能夠經過Future.get捕獲拋出的異常
複製代碼
1.若是還沒達到最大線程數,則新建線程 2.若是已經達到最大線程數,交給RejectExecutionHandler處理。 3.若是沒有設置自定義RejectExecutionHandler,則拋出RejectExecutionExcuption
優點: 實現對線程的複用,避免了反覆建立及銷燬線程的開銷;使用線程池統一管理線程能夠減小併發線程的數目,而線程數過多每每會在線程上下文切換上以及線程同步上浪費過多時間。
用法: 咱們能夠調用ThreadPoolExecutor的某個構造方法來本身建立一個線程池。但一般狀況下咱們可使用Executors類提供給咱們的靜態工廠方法來更方便的建立一個線程池對象。建立了線程池對象後,咱們就能夠調用submit或者excute方法提交任務到線程池中去執行了;線程池使用完畢後咱們要記得調用shutdown方法來關閉它。
- CountDownLatch:利用它能夠實現相似計數器的功能。好比有一個任務A,它要等待其餘4個任務執行完畢以後才能執行,此時就能夠利用CountDownLatch來實現這種功能了
public class Test {
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(2);
new Thread(){
public void run() {
try {
System.out.println("子線程"+Thread.currentThread().getName()+"正在執行");
Thread.sleep(3000);
System.out.println("子線程"+Thread.currentThread().getName()+"執行完畢");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
new Thread(){
public void run() {
try {
System.out.println("子線程"+Thread.currentThread().getName()+"正在執行");
Thread.sleep(3000);
System.out.println("子線程"+Thread.currentThread().getName()+"執行完畢");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
try {
System.out.println("等待2個子線程執行完畢...");
latch.await();
System.out.println("2個子線程已經執行完畢");
System.out.println("繼續執行主線程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製代碼
- CyclicBarrier: 實現讓一組線程等待至某個狀態以後再所有同時執行
public class Test {
public static void main(String[] args) {
int N = 4;
CyclicBarrier barrier = new CyclicBarrier(N);
for(int i=0;i<N;i++)
new Writer(barrier).start();
}
static class Writer extends Thread{
private CyclicBarrier cyclicBarrier;
public Writer(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("線程"+Thread.currentThread().getName()+"正在寫入數據...");
try {
Thread.sleep(5000); //以睡眠來模擬寫入數據操做
System.out.println("線程"+Thread.currentThread().getName()+"寫入數據完畢,等待其餘線程寫入完畢");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println("全部線程寫入完畢,繼續處理其餘任務...");
}
}
}
擴展(CyclicBarrier和CountdownLatch的區別):1.CountdownLatch等待幾個任務執行完畢,CyclicBarrier等待達到某個狀態;2.CyclicBarrier能夠調用reset,循環使用;3.CyclicBarrier能夠有含Runnable的構造方法,當達到某一狀態時執行某一任務。
複製代碼
- Semaphore:Semaphore能夠控同時訪問的某個資源的線程個數
public class Test {
public static void main(String[] args) {
int N = 8; //工人數
Semaphore semaphore = new Semaphore(5); //機器數目
for(int i=0;i<N;i++)
new Worker(i,semaphore).start();
}
static class Worker extends Thread{
private int num;
private Semaphore semaphore;
public Worker(int num,Semaphore semaphore){
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println("工人"+this.num+"佔用一個機器在生產...");
Thread.sleep(2000);
System.out.println("工人"+this.num+"釋放出機器");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
複製代碼
- Semaphore能夠控制當前資源被訪問的線程個數,超過最大個數後線程處於阻塞等待狀態
- 當線程個數指定爲1時,能夠當鎖使用
全部線程須要阻塞等待,而且觀察到事件狀態改變知足條件時自動執行,能夠用如下方法實現
- 閉鎖CountDownLatch:閉鎖是典型的等待事件發生的同步工具類,將閉鎖的初始值設置1,全部線程調用await方法等待,當事件發生時調用countDown將閉鎖值減爲0,則全部await等待閉鎖的線程得以繼續執行。
- 阻塞隊列BlockingQueue:全部等待事件的線程嘗試從空的阻塞隊列獲取元素,將阻塞,當事件發生時,向阻塞隊列中同時放入N個元素(N的值與等待的線程數相同),則全部等待的線程從阻塞隊列中取出元素後得以繼續執行。
- 信號量Semaphore:設置信號量的初始值爲等待的線程數N,一開始將信號量申請完,讓剩餘的信號量爲0,待事件發生時,同時釋放N個佔用的信號量,則等待信號量的全部線程將獲取信號量得以繼續執行。
- 擴展:經過sychronized關鍵字實現
- 阻塞隊列的特徵是當取或放元素是,隊列不知足條件(好比隊列爲空時進行取操做)能夠阻塞等待,知道知足條件
public class BlockingQueueTest {
private int size = 20;
private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(size);
public static void main(String[] args) {
BlockingQueueTest test = new BlockingQueueTest();
Producer producer = test.new Producer();
Consumer consumer = test.new Consumer();
producer.start();
consumer.start();
}
class Consumer extends Thread{
@Override public void run() {
while(true){
try {
//從阻塞隊列中取出一個元素
queue.take();
System.out.println("隊列剩餘" + queue.size() + "個元素");
} catch (InterruptedException e) {
} } }
}
class Producer extends Thread{
@Override public void run() {
while (true) {
try {
//向阻塞隊列中插入一個元素
queue.put(1);
System.out.println("隊列剩餘空間:" + (size - queue.size()));
} catch (InterruptedException e) {} }}
}
}
複製代碼
- ArrayBlockingQueue:一個基於數組實現的阻塞隊列,它在構造時須要指定容量。當試圖向滿隊列中添加元素或者從空隊列中移除元素時,當前線程會被阻塞。
- CountDownLatch:同步計數器,是一個線程工具類,可讓一個或幾個線程等待其餘線程
Condition是一個接口,有await和signal方法,和Object的wait、notify相似 Condition 經過lock得到:Condition condition = lock.newCondition(); 相對於Object的wait、notify,Condition的控制更加靈活,能夠知足喚起某一線程的目的
- 就緒狀態:得到CPU調度時由 就緒狀態 轉換爲 運行狀態
- 運行狀態:CPU時間片用完了由 運行狀態 轉換爲 就緒狀態 運行狀態
- 阻塞狀態:因等待某個事件發生而進入 阻塞狀態,事件發生後由 阻塞狀態 轉換爲 就緒狀態
- 互斥:兩個進程因爲不能同時使用同一臨界資源,只能在一個進程使用完了,另外一進程才能使用,這種現象稱爲進程間的互斥。
- 對於互斥的資源,A進程到達了該點後,若此時B進程正在對此資源進行操做,則A停下來,等待這些操做的完成再繼續操做。這就是進程間的同步
- 互斥:一個資源一次只能被一個進程所使用,便是排它性使用
- 不剝奪條件:一個資源僅能被佔有它的進程所釋放,而不能被別的進程強佔
- 請求與保持條件:進程已經保持了至少一個資源,但又提出了新的資源要求,而該資源又已被其它進程佔有,此時請求進程阻塞,但又對已經得到的其它資源保持不放
- 環路等待條件:當每類資源只有一個時,在發生死鎖時,必然存在一個進程—資源的環形鏈
類加載器的做用是根據指定全限定名稱將class文件加載到JVM內存中,並轉爲Class對象。
- 啓動類加載器(根加載器 Bootstrap ClassLoader):由native代碼實現,負責將存放在<JAVA_HOME>\lib目錄或-Xbootclasspath參數指定的路徑中的類庫加載到內存中
- 擴展加載器(Extension ClassLoader):java語言實現,父加載器是Bootstrap,:負責加載<JAVA_HOME>\lib\ext目錄或java.ext.dirs系統變量指定的路徑中的全部類庫。
- 應用程序類加載器(Application ClassLoader):java實現,負責加載用戶類路徑(classpath)上的指定類庫,咱們能夠直接使用這個類加載器。通常狀況,若是咱們沒有自定義類加載器默認就是用這個加載器。
- 自定義類加載器:有時爲了安全會將類加密,或者從遠程(服務器)加載類 ,這個時候就須要自定義類加載器。自定義經過繼承ClassLoader類實現,loadClass方法已經實現了雙親委派模式,當父類沒有加載成功時,調用當前類的findclass方法,因此咱們通常重寫該方法。
- 類加載器採用雙親委派模型進行加載:每次經過先委託父類加載器加載,當父類加載器沒法加載時,再本身加載。
- 類的生命週期能夠分爲七個階段:加載 -> 鏈接(驗證 -> 準備*(爲靜態變量分配內存並設置默認的初始值)* -> 解析*(將符號引用替換爲直接引用)*)-> 初始化 -> 使用 -> 卸載
- 使用雙親委派模式,保證只加載一次該類
- 咱們可使用自定義的類加載器加載同名類,這樣就阻止了系統雙親委派模式的加載
- JVM 及 Dalvik 對類惟一的識別是 ClassLoader id + PackageName + ClassName
- 兩個相同的類可能由於兩個ClassLoader加載而不兼容
- 經過類的class對象類得到類的各類信息,建立對應的對象或者調用方法
- App的動態加載或者Android中調用其餘對象private方法,都須要反射
- String.class:不執行靜態塊和動態構造塊
- "hello".getClass();:執行靜態塊和動態構造塊
- Class.forName("java.lang.String");:執行靜態塊,不執行動態構造塊
- String.class.newInstance();
- String.class.getConstrutor(Stirng.class).newInstance("hello word");
- 經過類對象的getDeclaredField()方法得到(Field)對象
- 調用Field對象的setAccessible(true)方法將其設置爲可訪問
- 經過get/set方法來獲取/設置字段的值
- 經過類對象的getMethod方法得到Method對象
- 調用對象的invoke()方法
- 範型能夠用於類定義和方法定義
- 範型的實現是經過擦除實現的,也就是說編譯以後範型信息會被擦出
- 通配符有兩種用法:?extends A 和 ? super A
- ?extends A 表示?的上界是A,具體什麼類型並不清楚,適合於獲取,獲取到的必定是A類型
- ? super A 表示?的下界是A,具體什麼類型並不清楚,適合於插入,必定能夠插入A類型
註解分爲三種:源碼級別(source),類文件級別(class)或者運行時級別(runtime);butternife是類文件級別 參考:https://blog.csdn.net/javazejian/article/details/71860633 https://blog.csdn.net/u013045971/article/details/53509237
https://www.cnblogs.com/likeshu/p/5526187.html
http://www.jcodecraeer.com/a/chengxusheji/java/2015/0206/2421.html