Java 經(jīng)典問題

九種基本類型及封裝類

基本類型 boolean byte char short int long double float
二進(jìn)制位數(shù) 1 8(一字節(jié)) 16(2字節(jié)) 16(2字節(jié)) 32(4字節(jié)) 64(8字節(jié)) 64(8字節(jié)) 32(4字節(jié))
封裝器類 Boolean Byte Character Short Integer Long Double Void

switch語句后的控制表達(dá)式只能是shortcharintlong整數(shù)類型和枚舉類型,不能是float,double和boolean類型。String類型是java7開始支持。

位運(yùn)算符

  • 左移(<<)
  • 右移(>>):int是32位,最高位是符號位,0代表正數(shù),1代表負(fù)數(shù),負(fù)數(shù)以補(bǔ)碼的形式存儲在計算機(jī)中。右移規(guī)則:最高位是什么(0或者1),右移的時候左邊就補(bǔ)什么。即正數(shù)右移用0補(bǔ)位左邊負(fù)數(shù)右移用1補(bǔ)位左邊
  • 無符號右移(>>>):不管是負(fù)數(shù)還是正數(shù),右移總是左邊補(bǔ)0
  • 運(yùn)算(&)
  • 運(yùn)算(|)
  • 運(yùn)算(~)
  • 異或運(yùn)算(^):位相同為0相異為1
-5右移3位后結(jié)果為-1,-1的二進(jìn)制為: 
1111 1111 1111 1111 1111 1111 1111 1111   // (用1進(jìn)行補(bǔ)位)
-5無符號右移3位后的結(jié)果 536870911 換算成二進(jìn)制: 
0001 1111 1111 1111 1111 1111 1111 1111   // (用0進(jìn)行補(bǔ)位)

應(yīng)用:不用臨時變量交換兩個數(shù)

void swap(int argc, char *argv[])
{
    a = a ^ b;
    b = b ^ a;
    a = a ^ b;
}

for循環(huán),F(xiàn)orEach,迭代器效率

直接for循環(huán)效率最高,其次是迭代器和 ForEach操作。
其實 ForEach 編譯成字節(jié)碼之后,使用的是迭代器實現(xiàn)的。

synchronized和volatile

volatile僅能使用在變量級別;synchronized則可以使用在變量方法、和級別的。
volatile保證了變量的可見性synchronized保證了原子性可見性

volatile

原理:首先我們要先意識到有這樣的現(xiàn)象,編譯器為了加快程序運(yùn)行的速度,對一些變量的寫操作會先在寄存器或者是CPU緩存上進(jìn)行,最后才寫入內(nèi)存。而在這個過程,變量的新值對其他線程是不可見的,而volatile的作用就是使它修飾的變量的讀寫操作都必須在內(nèi)存中進(jìn)行volatile告訴JVM, 它所修飾的變量不保留拷貝,直接訪問主內(nèi)存中的

volatile與synchronized

  • volatile本質(zhì)是在告訴JVM當(dāng)前變量在寄存器中的值是不確定的,需要從主存中讀取,synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住
  • volatile僅能使用在變量級別,synchronized則可以使用在變量方法.
  • volatile僅能實現(xiàn)變量的修改可見性,但不具備原子特性,而synchronized則可以保證變量的修改可見性原子性
  • volatile不會造成線程的阻塞,而synchronized可能會造成線程的阻塞。
  • volatile標(biāo)記的變量不會被編譯器優(yōu)化,而synchronized標(biāo)記的變量可以被編譯器優(yōu)化。

volatile不能保證原子性原因:線程A修改了變量還沒結(jié)束時,另外的線程B可以看到已修改的值,而且可以修改這個變量,而不用等待A釋放鎖,因為Volatile 變量沒上鎖。

注意
聲明為volatile的簡單變量如果當(dāng)前值由該變量以前的值相關(guān),那么volatile關(guān)鍵字不起作用

也就是說如下的表達(dá)式都不是原子操作: 
n  =  n  +   1 ; 
n ++ ; 

只有當(dāng)變量的值和自身上一個值無關(guān)時對該變量的操作才是原子級別的,如n = m + 1。

Java內(nèi)存模型的抽象(volatile)

java中,所有實例域、靜態(tài)域和數(shù)組元素存儲在堆內(nèi)存中,堆內(nèi)存在線程之間共享(本文使用“共享變量”這個術(shù)語代指實例域,靜態(tài)域和數(shù)組元素)。局部變量方法定義參數(shù)異常處理器參數(shù)不會在線程之間共享,在棧內(nèi)存中,不需要同步處理,因為棧內(nèi)存是線程獨(dú)享的,它們不會有內(nèi)存可見性問題,也不受內(nèi)存模型的影響。

Java線程之間的通信由Java內(nèi)存模型(本文簡稱為JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個線程都有一個私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本(寄存器或CPU緩存)本地內(nèi)存是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。Java內(nèi)存模型的抽象示意圖如下:

java內(nèi)存模型

從上圖來看,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個步驟:

  • 首先,線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去
  • 然后,線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量

下面通過示意圖來說明這兩個步驟:


如上圖所示,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本。假設(shè)初始時,這三個內(nèi)存中的x值都為0。線程A在執(zhí)行時,把更新后的x值(假設(shè)值為1)臨時存放在自己的本地內(nèi)存A中。當(dāng)線程A和線程B需要通信時,線程A首先會把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中,此時主內(nèi)存中的x值變?yōu)榱?。隨后,線程B到主內(nèi)存中去讀取線程A更新后的x值,此時線程B的本地內(nèi)存的x值也變?yōu)榱恕?/p>

從整體來看,這兩個步驟實質(zhì)上是線程A在向線程B發(fā)送消息,而且這個通信過程必須要經(jīng)過主內(nèi)存。JMM通過控制主內(nèi)存與每個線程的本地內(nèi)存之間的交互,來為java程序員提供內(nèi)存可見性保證。

equals與==的區(qū)別

==常用于比較原生類型,而equals()方法用于檢查對象的相等性。另一個不同的點(diǎn)是:如果==equals()用于比較對象,當(dāng)兩個引用地址相同,== 返回true。而equals()可以返回true或者false主要取決于重寫實現(xiàn)。最常見的一個例子,字符串的比較,不同情況 ==equals()返回不同的結(jié)果。

看Object源碼

 public boolean equals(Object obj) {
        return (this == obj);
    }

==表示的是比較兩個對象實例的內(nèi)存地址是否相同。如果不重寫equal(),就和==等效,

  • 相等(相同)的對象必須具有相等的哈希碼(或者散列碼)。
  • 如果兩個對象的hashCode相同,它們并不一定相同。

術(shù)語來講的區(qū)別

  • ==是判斷兩個變量或?qū)嵗遣皇侵赶蛲粋€內(nèi)存空間
    equals是判斷兩個變量或?qū)嵗赶虻?code>內(nèi)存空間的值是不是相同。
  • ==引用是否相同
    equals()指的是是否相同

hashCode作用

java.lang.Object來理解JVMnew一個Object,它都會將這個Object丟到一個Hash哈希表中去,這樣的話,下次做Object的比較或者取這個對象的時候,它會根據(jù)對象的hashcode再從Hash表中取這個對象。這樣做的目的是提高取對象的效率

具體過程是這樣:

  • new Object()JVM根據(jù)這個對象的Hashcode值放入到對應(yīng)的Hash表對應(yīng)的Key上,如果不同的對象卻產(chǎn)生了相同的hash值,也就是發(fā)生了Hash key相同導(dǎo)致沖突的情況,那么就在這個Hash key的地方產(chǎn)生一個鏈表,將所有產(chǎn)生相同hashcode的對象放到這個單鏈表上串在一起。
  • 比較兩個對象的時候,首先根據(jù)他們的hashcodehash表中找他的對象,當(dāng)兩個對象的hashcode相同,那么就是說他們這兩個對象放在Hash表中的同一個key上,那么他們一定在這個key上的鏈表上。那么此時就只能根據(jù)Objectequal方法來比較這個對象是否equal。當(dāng)兩個對象的hashcode不同的話,肯定他們不能equal。

java.lang.Object中對hashCode的約定:

  • 在一個應(yīng)用程序執(zhí)行期間,如果一個對象的equals方法做比較所用到的信息沒有被修改的話,則對該對象調(diào)用hashCode方法多次,它必須始終如一地返回同一個整數(shù)。
  • 如果兩個對象根據(jù)equals(Object o)方法是相等的,則調(diào)用這兩個對象中任一對象的hashCode方法必須產(chǎn)生相同的整數(shù)結(jié)果。
  • 如果兩個對象根據(jù)equals(Object o)方法是不相等的,則調(diào)用這兩個對象中任一個對象的hashCode方法,不要求產(chǎn)生不同的整數(shù)結(jié)果。但如果能不同,則可能提高散列表的性能。

Object的公用方法

  • clone 保護(hù)方法,只有實現(xiàn)了Cloneable接口才可以調(diào)用,否則拋異常
  • getClass final方法,獲得運(yùn)行時類型
  • toString
  • equals
  • hashCode
  • wait 就是使當(dāng)前線程等待該對象的鎖,當(dāng)前線程必須是該對象的擁有者,也就是具有該對象的鎖。wait方法一直等待,直到獲得鎖或者被中斷。wait設(shè)定一個超時間隔,如果在規(guī)定時間內(nèi)沒有獲得鎖就返回。
    調(diào)用該方法后當(dāng)前線程進(jìn)入睡眠狀態(tài),直到以下事件發(fā)生。
  • 其他線程調(diào)用了該對象的notify方法。
  • 其他線程調(diào)用了該對象的notifyAll方法。
  • 其他線程調(diào)用了interrupt中斷該線程。
  • 時間間隔到了。
    此時該線程就可以被調(diào)度了,如果是被中斷的話就拋出一個InterruptedException異常。
  • notify
  • notifyAll

Java四種引用 --- 這里指的是“引用“,不是對象

強(qiáng)引用

平常我們使用對象的方式Object object = new Object();如果一個對象具有強(qiáng)引用,它就不會被垃圾回收器回收。即使當(dāng)前內(nèi)存空間不足,JVM也不會回收它,而是拋出 OutOfMemoryError 錯誤,使程序異常終止。例如下面的代碼:

public class Main {
    public static void main(String[] args) {
        new Main().fun1();
    }

    public void fun1() {
        Object object = new Object();
        Object[] objArr = new Object[1000];
    }
}

當(dāng)運(yùn)行至Object[] objArr = new Object[1000];這句時,如果內(nèi)存不足,JVM會拋出OOM錯誤也不會回收object指向的對象。不過要注意的是,當(dāng)fun1運(yùn)行完之后,objectobjArr都已經(jīng)不存在了,所以它們指向的對象都會被JVM回收。
但如果想中斷強(qiáng)引用和某個對象之間的關(guān)聯(lián),可以顯式地將引用賦值為null,這樣一來的話,JVM在合適的時間就會回收該對象。

軟引用

軟引用通過SoftReference創(chuàng)建,在使用軟引用時,如果內(nèi)存的空間足夠,軟引用就能繼續(xù)被使用,而不會被垃圾回收器回收,只有在內(nèi)存不足時,軟引用才會被垃圾回收器回收

軟引用的這種特性使得它很適合用來解決 OOM 問題,實現(xiàn)緩存機(jī)制,例如:圖片緩存、網(wǎng)頁緩存等等……
軟引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對象被JVM回收,這個軟引用就會被加入到與之關(guān)聯(lián)的引用隊列中。

弱引用

事實上軟引用和弱引用非常類似,兩者的區(qū)別在于:只具有弱引用的對象擁有的生命周期更短暫。因為當(dāng) JVM 進(jìn)行垃圾回收,一旦發(fā)現(xiàn)弱引用對象,無論當(dāng)前內(nèi)存空間是否充足,都會將弱引用回收。不過由于垃圾回收器是一個優(yōu)先級較低的線程,所以并不一定能迅速發(fā)現(xiàn)弱引用對象。

弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果弱引用所引用的對象被JVM回收,這個弱引用就會被加入到與之關(guān)聯(lián)的引用隊列中。

虛引用

虛引用”顧名思義,就是形同虛設(shè),與其他幾種引用都不同,虛引用并不會影響對象的生命周期。如果一個對象僅持有虛引用,那么它相當(dāng)于沒有引用,在任何時候都可能被垃圾回收器回收

  • 強(qiáng)引用:不管什么時候都不會被回收。
  • 軟引用:當(dāng)內(nèi)存不足的時候,JVM垃圾回收會回收。
  • 弱引用:不管內(nèi)存足不足,只要發(fā)生JVM垃圾回收就會回收。
  • 虛引用:隨時都可能會被回收。

小結(jié)

引用和引用隊列提供了一種通知機(jī)制,允許我們知道對象已經(jīng)被銷毀或者即將被銷毀。GC要回收一個對象的時候,如果發(fā)現(xiàn)該對象有軟、弱、虛引用的時候,會將這些引用加入到注冊的引用隊列中。軟引用和弱引用差別不大,JVM都是先把SoftReferenceWeakReference中的referent字段值設(shè)置成null,之后加入到引用隊列;而虛引用則不同,如果某個堆中的對象,只有虛引用,那么JVM會將PhantomReference加入到引用隊列中,JVM不會自動將referent字段值設(shè)置成null

實際應(yīng)用:利用軟引用和弱引用緩存解決OOM問題。

如:Bitmap的緩存

設(shè)計思路是:用一個HashMap來保存圖片的路徑和相應(yīng)圖片對象(Bitmap)的軟引用之間的映射關(guān)系,在內(nèi)存不足時,JVM會自動回收這些緩存圖片對象所占用的空間,從而有效地避免了OOM的問題。在Android開發(fā)中對于大量圖片下載會經(jīng)常用到。

wait()、notify()和sleep()

wait()和notify()

  • wait()notify()是直接隸屬于Object類,在JAVA中的Object類型中,都是帶有一個內(nèi)存鎖的,在有線程獲取該內(nèi)存鎖后,其它線程無法訪問該內(nèi)存,從而實現(xiàn)JAVA中簡單的同步、互斥操作。明白這個原理,就能理解為什么synchronized(this)synchronized(static XXX)的區(qū)別了,synchronized就是針對內(nèi)存區(qū)塊申請內(nèi)存鎖,this關(guān)鍵字代表類的一個對象,所以其內(nèi)存鎖是針對相同對象的互斥操作,而static成員屬于類專有,其內(nèi)存空間為該類所有成員共有,這就導(dǎo)致synchronized()static成員加鎖,相當(dāng)于對類加鎖,也就是在該類的所有成員間實現(xiàn)互斥,在同一時間只有一個線程可訪問該類的實例wait只能由持有對像鎖的線程來調(diào)用
  • Obj.wait()Obj.notify()必須要與synchronized(Obj)一起使用,從功能上來說wait就是說線程在獲取對象鎖后,主動釋放對象鎖同時本線程休眠。直到有其它線程調(diào)用對象的notify()喚醒該線程,才能繼續(xù)獲取對象鎖,并繼續(xù)執(zhí)行。相應(yīng)的notify()就是對對象鎖的喚醒操作。但有一點(diǎn)需要注意的是notify()調(diào)用后,并不是馬上就釋放對象鎖的,而是在相應(yīng)的synchronized(){}語句塊執(zhí)行結(jié)束,自動釋放鎖后,JVM會在wait()對象鎖的線程中隨機(jī)選取一線程,賦予其對象鎖,喚醒線程,繼續(xù)執(zhí)行。這樣就提供了在線程間同步、喚醒的操作。Thread.sleep()Object.wait()二者都可以暫停當(dāng)前線程,釋放CPU控制權(quán),主要的區(qū)別在于Object.wait()在釋放CPU同時,釋放了對象鎖的控制
  • wait():促使當(dāng)前線程等待直到另外一個線程調(diào)用這個對象的notify()方法喚醒。和synchronized塊使用的時候,synchronized獲取對象的鎖以后,可以通過wait()方法釋放,同時阻塞當(dāng)前線程,停止執(zhí)行(這也是和sleep的區(qū)別)。
public static void firstMethod(){
        synchronized (a){
            System.out.println(Thread.currentThread().getName() + "  firstMethod--死鎖");
            try {
//                Thread.sleep(10);
                a.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (b){
                System.out.println(Thread.currentThread().getName() + "  firstMethod--解鎖");
            }
        }
    }

    public static void seconedMethod(){
        synchronized (b){
            System.out.println(Thread.currentThread().getName() + "  seconedMethod--死鎖");
            synchronized (a){
                System.out.println(Thread.currentThread().getName() + "  seconedMethod--解鎖");
                a.notify();
            }
        }
    }

如果用兩個線程分別執(zhí)行這兩個方法

public static void main(String[] args) {

        Runnable runnable1 = new Runnable() {
            @Override
            public void run() {
                firstMethod();
            }
        };
        Runnable runnable2 = new Runnable() {
            @Override
            public void run() {
                seconedMethod();
            }
        };
        Thread thread1 = new Thread(runnable1);
        Thread thread2 = new Thread(runnable2);
        thread1.start();
        thread2.start();

    }

如果是用sleep方法替換掉wait方法,就是一個死鎖,線程thread1先執(zhí)行拿到a對象的鎖,然后阻塞10ms(并沒有釋放鎖),thread2然后拿到對象b的鎖,這時候seconedMethod需要a對象的鎖,但是firstMethod并沒有釋放,然后10ms過后,firstMethod需要b的鎖,然后b的鎖也沒有在seconedMethod方法中釋放,兩個線程相互等待對方釋放鎖,就形成了死鎖。

運(yùn)行結(jié)果:

Thread-0 firstMethod--死鎖
Thread-1 seconedMethod--死鎖

如果不使用sleep而是使用wait方法,就不會發(fā)生死鎖。因為wait釋放了firstMethod中的a對象的鎖,當(dāng)seconedMethod需要a對象鎖的時候就可以用了。

運(yùn)行結(jié)果:

Thread-0 firstMethod--死鎖
Thread-1 seconedMethod--死鎖
Thread-1 seconedMethod--解鎖
Thread-0 firstMethod--解鎖

notify():喚醒在此對象監(jiān)視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程(隨機(jī))直到當(dāng)前的線程放棄此對象上的鎖,才能繼續(xù)執(zhí)行被喚醒的線程

sleep()

通過Thread.sleep()使當(dāng)前線程暫停執(zhí)行一段時間,讓其他線程有機(jī)會繼續(xù)執(zhí)行,但它并不釋放對象鎖。也就是說如果有synchronized同步塊,其他線程仍然不能訪問共享數(shù)據(jù)。注意該方法要捕捉異常。

public class ThreadLock {

    Object lock = new Object();
    int num = 0;

    public static void main(String[] args) {

        ThreadLock test = new ThreadLock();

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        };

        Thread thread1 = new Thread(runnable);
        thread1.start();
        test.method1();
    }

    public void method1(){
        synchronized (lock){
            try {
                Thread.sleep(1000);
//                lock.wait(1000);
                num += 100;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
        }
    }

    public void method2(){
        synchronized (lock){
            num += 9;
            System.out.println(num);
        }
    }

}

因為在main線程調(diào)用方法,因此先執(zhí)行主線程的method1,對象鎖被主線程拿走了,那么子線程執(zhí)行method2的時候就需要等待1秒后把鎖還回來。

1秒后輸出結(jié)果:
109

如果替換成lock.wait(1000);

lock.wait(1000)會讓當(dāng)前線程(main線程)睡眠1秒,同時釋放synchronized的對象鎖,因此小于1秒輸出9

synchronized和lock

幾個概念

  • 共享變量(shared variable):多個線程都能訪問的變量。
  • 變量可見性(variable visibility):一個線程更新了共享變量,對其他線程立刻可見
  • 互斥(mutual exclusion ):幾個線程中的任何一個不能與其他一個或多個同時操作一個變量
  • 臨界區(qū)(critical section):訪問共享資源的一段代碼塊。

synchronized

  • 保證共享變量的可見性:變量緩存與編譯器指令優(yōu)化會導(dǎo)致變量修改的不可見性
  • 保證共享變量的互斥性:同一時刻只能有一個線程對共享變量的修改(注意修改一次,是先讀再寫,是兩個操作)

特點(diǎn)

  • 當(dāng)程序運(yùn)行到非靜態(tài)的synchronized同步方法上時,自動獲得與正在執(zhí)行代碼類的當(dāng)前實例(this實例)有關(guān)的鎖。
  • 如果兩個線程要執(zhí)行一個類中的synchronized方法,并且兩個線程使用相同的實例來調(diào)用方法,那么一次只能有一個線程能夠執(zhí)行方法,另一個需要等待,直到鎖被釋放。也就是說:如果一個線程在對象上獲得一個鎖,就沒有任何其他線程可以進(jìn)入(該對象的)類中的任何一個同步方法。
  • 線程可以獲得多個鎖。比如,在一個對象的同步方法里面調(diào)用另外一個對象的同步方法,則獲取了兩個對象的同步鎖。
  • 線程同步方法是通過鎖來實現(xiàn),每個對象都有且僅有一個鎖,這個鎖與一個特定的對象關(guān)聯(lián),線程一旦獲取了對象鎖,其他訪問該對象的線程就無法再訪問該對象的其他同步方法。
  • 對于同步,要時刻清醒在哪個對象上同步,這是關(guān)鍵。
  • 死鎖是線程間相互等待鎖造成的

lock

lock提供了如下的方法

  • void lock()獲取一個鎖,如果鎖當(dāng)前被其他線程獲得,當(dāng)前的線程將被休眠
  • boolean tryLock(),嘗試獲取一個鎖,如果當(dāng)前鎖被其他線程持有,則返回false不會使當(dāng)前線程休眠
  • boolean tryLock(long timeout,TimeUnit unit),如果獲取了鎖定立即返回true,如果別的線程正持有鎖,會等待參數(shù)給定的時間,在等待的過程中,如果獲取了鎖定,就返回true,如果等待超時,返回false。
  • void lockInterruptibly(),如果獲取了鎖定立即返回,如果沒有獲取鎖定,當(dāng)前線程處于休眠狀態(tài),直到或者鎖定,或者當(dāng)前線程被別的線程中斷。

synchronized和lock區(qū)別

  • synchronized是在JVM層面上實現(xiàn)的,如果代碼執(zhí)行時出現(xiàn)異常,JVM會自動釋放monitor鎖。而lock代碼是用戶寫的,需要用戶來保證最終釋放掉鎖。
  • lock提供了一個重要的方法newConditon()ConditionObjectawait()signal()signalAll(),類似于Ojbect類的wait()notify()notifyAll()。這些方法都是用來實現(xiàn)線程間通信。locksynchronized的互斥性和線程間通信分離開來,一個lock可以有多個condition。另外locksignal可以實現(xiàn)公平的通知,而notify是隨機(jī)從鎖等待隊列中喚醒某個進(jìn)程。
  • 性能上來說,在多線程競爭比較激烈地情況下,lock比synchronize高效得多
public class ThreadLock {

    public static void main(String[] args) {
        Test test = new Test();
        Lock lock = new ReentrantLock();

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                    lock.lock();
                    for (int i = 0; i < 50 ; i++) {
                        test.setX(1);
                        System.out.println(Thread.currentThread().getName() + " : " +test.getX());
                    }
                    lock.unlock();
                }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }

    static class Test{
        private int x = 100;

        public int getX(){
            return x;
        }

        public void setX(int y){
            x = x - y;
        }

    }

}

ReentrantLock與synchronized的比較

ReentrantLocak(可重入鎖)

簡單來說,它有一個與鎖相關(guān)的獲取計數(shù)器,如果擁有鎖的某個線程再次得到鎖,那么獲取計數(shù)器就加1,然后鎖需要被釋放兩次才能獲得真正釋放。
ReentrantLock提供了synchronized類似的功能和內(nèi)存語義。

不同

  • ReentrantLock功能性方面更全面,比如時間鎖等候,可中斷鎖等候,鎖投票等,因此更有擴(kuò)展性。在多個條件變量和高度競爭鎖的地方,用ReentrantLock更合適,ReentrantLock還提供了Condition,對線程的等待和喚醒等操作更加靈活,一個ReentrantLock可以有多個Condition實例,所以更有擴(kuò)展性。
  • ReentrantLock的性能比synchronized會好點(diǎn)。
  • ReentrantLock提供了可輪詢的鎖請求,他可以嘗試的去取得鎖,如果取得成功則繼續(xù)處理,取得不成功,可以等下次運(yùn)行的時候處理,所以不容易產(chǎn)生死鎖,而synchronized則一旦進(jìn)入鎖請求要么成功,要么一直阻塞,所以更容易產(chǎn)生死鎖。

缺點(diǎn)

  • lock 必須在 finally 塊中釋放。否則,如果受保護(hù)的代碼將拋出異常,鎖就有可能永遠(yuǎn)得不到釋放!這一點(diǎn)區(qū)別看起來可能沒什么,但是實際上,它極為重要。忘記在 finally 塊中釋放鎖,可能會在程序中留下一個定時炸彈,當(dāng)有一天炸彈爆炸時,您要花費(fèi)很大力氣才能找到源頭在哪。而使用同步,JVM 將確保鎖會獲得自動釋放
  • 當(dāng) JVMsynchronized 管理鎖定請求和釋放時,JVM 在生成線程轉(zhuǎn)儲時能夠包括鎖定信息。這些對調(diào)試非常有價值,因為它們能標(biāo)識死鎖或者其他異常行為的來源。 Lock 類只是普通的類,JVM 不知道具體哪個線程擁有 Lock 對象。

ArrayList,LinkedList和Vector

  • ArrayListVector都是基于數(shù)組實現(xiàn)的,所以查詢效率高,插入效率低
  • LinkedList基于雙向鏈表實現(xiàn)的,所以插入效率高,查詢效率低
  • Vector使用了synchronized方法,所以線程安全,性能比ArrayList低。
  • LinkedList實現(xiàn)了List接口,還提供了額外的getremoveinsert方法在LinkedList的首部或尾部,這些操作使LinkedList可被用作棧(Stack),隊列(Queue)或雙向隊列(deque)。
  • ArrayListLinkedList允許null元素,重復(fù)元素

HashMap和HashTable

  • 都實現(xiàn)了Map接口
  • HashMap允許key為null,value為null而HashTable不允許,如果新加入的key和之前重復(fù)了,會覆蓋之前的value
  • HashTable線程安全,而HashMap不是線程安全。因此單線程下HashTableHashMap慢。
  • HashMap不能保證隨著時間推移Map中的元素次序是不變的
  • Hashtable中hash數(shù)組默認(rèn)大小是11,增加的方式是 old*2+1HashMap中hash數(shù)組的默認(rèn)大小是16而且一定是2的指數(shù)

ConcurrentHashMap

鎖分段技術(shù)

HashTable容器在競爭激烈的并發(fā)環(huán)境下表現(xiàn)出效率低下的原因是所有訪問HashTable的線程都必須競爭同一把鎖,那假如容器里有多把鎖,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù),那么當(dāng)多線程訪問容器里不同數(shù)據(jù)段的數(shù)據(jù)時,線程間就不會存在鎖競爭,從而可以有效的提高并發(fā)訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術(shù),首先將數(shù)據(jù)分成一段一段的存儲,然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個線程占用鎖訪問其中一個段數(shù)據(jù)的時候,其他段的數(shù)據(jù)也能被其他線程訪問

HashSet

實現(xiàn)了Set接口HashSet< T >本質(zhì)就是一個HashMap<T , Object>,把HashMapkey作為HashSet的值,HashMapvalue是一個固定的Object對象,因為HashMapkey不允許重復(fù)的,所以HashSet里的元素也是不能重復(fù)的,也可以看出HashSet的查詢效率很高

String,StringBuilder和StringBuffer

  • CharSequence接口:一個字符序列StringStringBuilderStringBuffer都實現(xiàn)了它。
  • String類:是常量,不可變.
  • StringBuilder類:只可以在單線程的情況下進(jìn)行修改(線程不安全),字符串拼接用,除了StringBuffer可用場景外。
  • StringBuffer類:可以在多線程的情況下進(jìn)行改變(線程安全),比如:在http請求中拼接。
  • StringBuilderStringBuffer效率高,應(yīng)該盡量使用StringBuilder

Excption與Error包結(jié)構(gòu)

結(jié)構(gòu)圖:


結(jié)構(gòu)圖

Throwable

  • ThrowableJava 語言中所有錯誤或異常的超類。
  • Throwable包含兩個子類: ErrorException 。它們通常用于指示發(fā)生了異常情況。
  • Throwable包含了其線程創(chuàng)建時線程執(zhí)行堆棧的快照,它提供了printStackTrace()等接口用于獲取堆棧跟蹤數(shù)據(jù)等信息。

Exception

  • Exception及其子類是 Throwable 的一種形式,它指出了合理的應(yīng)用程序想要捕獲的條件。

RuntimeException

  • RuntimeException是那些可能在 Java 虛擬機(jī)正常運(yùn)行期間拋出的異常的超類。
  • 編譯器不會檢查RuntimeException異常。 例如,除數(shù)為零時,拋出ArithmeticException異常。RuntimeExceptionArithmeticException的超類。當(dāng)代碼發(fā)生除數(shù)為零的情況時,倘若既"沒有通過throws聲明拋出ArithmeticException異常",也"沒有通過try...catch...處理該異常",也能通過編譯。這就是我們所說的"編譯器不會檢查RuntimeException異常"!
  • 如果代碼會產(chǎn)生RuntimeException異常,則需要通過修改代碼進(jìn)行避免。 例如,若會發(fā)生除數(shù)為零的情況,則需要通過代碼避免該情況的發(fā)生!

Error

  • Exception一樣, Error也是Throwable的子類。 它用于指示合理的應(yīng)用程序不應(yīng)該試圖捕獲的嚴(yán)重問題,大多數(shù)這樣的錯誤都是異常條件。
  • 和RuntimeException一樣, 編譯器也不會檢查Error

Interface與abstract類的區(qū)別

參數(shù) 抽象類 接口
默認(rèn)的方法實現(xiàn) 它可以有默認(rèn)的方法實現(xiàn) 接口完全是抽象的。它根本不存在方法的實現(xiàn)
實現(xiàn) 子類使用extends關(guān)鍵字來繼承抽象類。如果子類不是抽象類的話,它需要提供抽象類中所有聲明的方法的實現(xiàn)。 子類使用關(guān)鍵字implements來實現(xiàn)接口。它需要提供接口中所有聲明的方法的實現(xiàn)
構(gòu)造器 抽象類可以有構(gòu)造器 接口不能有構(gòu)造器
與正常Java類的區(qū)別 除了你不能實例化抽象類之外,它和普通Java類沒有任何區(qū)別 接口是完全不同的類型
訪問修飾符 抽象方法可以有public、protected和default這些修飾符 接口方法默認(rèn)修飾符是public。你不可以使用其它修飾符。
main方法 抽象方法可以有main方法并且我們可以運(yùn)行它 接口沒有main方法,因此我們不能運(yùn)行它。
多繼承 抽象類可以繼承一個類和實現(xiàn)多個接口 接口只可以繼承一個或多個其它接口
速度 它比接口速度要快 接口是稍微有點(diǎn)慢的,因為它需要時間去尋找在類中實現(xiàn)的方法。
添加新方法 如果你往抽象類中添加新的方法,你可以給它提供默認(rèn)的實現(xiàn)。因此你不需要改變你現(xiàn)在的代碼。 如果你往接口中添加方法,那么你必須改變實現(xiàn)該接口的類。

靜態(tài)內(nèi)部類和非靜態(tài)內(nèi)部類

相同點(diǎn)

  • 內(nèi)部類都可以用publicprotectedprivate修飾。
  • 方法中都可以調(diào)用外部類的靜態(tài)成員變量

不同點(diǎn)

  • 靜態(tài)內(nèi)部類可以聲明靜態(tài)和非靜態(tài)成員變量,非靜態(tài)內(nèi)部類只能聲明非靜態(tài)成員變量
  • 靜態(tài)內(nèi)部類可以聲明靜態(tài)和非靜態(tài)方法,非靜態(tài)內(nèi)部類只能聲明非靜態(tài)方法
  • 靜態(tài)內(nèi)部類不能調(diào)用外部類的非靜態(tài)成員變量(靜態(tài)方法和非靜態(tài)方法都一樣),非靜態(tài)內(nèi)部類都可以調(diào)用。

泛型擦除

一篇好博客

泛型在JDK5以后才有的,擦除是為了兼容之前沒有的使用泛型的類庫和代碼。如:ArrayList< String >ArrayList< Integer > 在編譯器編譯的時候都變成了ArrayList

List<Integer> list = new ArrayList<Integer>();
Map<Integer, String> map = new HashMap<Integer, String>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));

/* Output
[E]
[K, V]
*/

我們期待的是得到泛型參數(shù)的類型,但是實際上我們只得到了一堆占位符

public class Main<T> {

    public T[] makeArray() {
        // error: Type parameter 'T' cannot be instantiated directly
        return new T[5];
    }
}

我們無法在泛型內(nèi)部創(chuàng)建一個T類型的數(shù)組,原因也和之前一樣,T僅僅是個占位符,并沒有真實的類型信息,實際上,除了new表達(dá)式之外,instanceof操作和轉(zhuǎn)型(會收到警告)在泛型內(nèi)部都是無法使用的,而造成這個的原因就是之前講過的編譯器對類型信息進(jìn)行了擦除。

public class Main<T> {

    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public static void main(String[] args) {
        Main<String> m = new Main<String>();
        m.set("findingsea");
        String s = m.get();
        System.out.println(s);
    }
}

/* Output
findingsea
*/

雖然有類型擦除的存在,使得編譯器在泛型內(nèi)部其實完全無法知道有關(guān)T的任何信息,但是編譯器可以保證重要的一點(diǎn):內(nèi)部一致性,也是我們放進(jìn)去的是什么類型的對象,取出來還是相同類型的對象,這一點(diǎn)讓Java的泛型起碼還是有用武之地的。

代碼片段展現(xiàn)就是編譯器確保了我們放在T上的類型的確是T(即便它并不知道有關(guān)T的任何類型信息)。這種確保其實做了兩步工作:

  • set()處的類型檢驗
  • get()處的類型轉(zhuǎn)換

這兩步工作也成為邊界動作。

public class Main<T> {

    public List<T> fillList(T t, int size) {
        List<T> list = new ArrayList<T>();
        for (int i = 0; i < size; i++) {
            list.add(t);
        }
        return list;
    }

    public static void main(String[] args) {
        Main<String> m = new Main<String>();
        List<String> list = m.fillList("findingsea", 5);
        System.out.println(list.toString());
    }
}

/* Output
[findingsea, findingsea, findingsea, findingsea, findingsea]
*/

代碼片段同樣展示的是泛型的內(nèi)部一致性。

擦除的補(bǔ)償
如上看到的,但凡是涉及到確切類型信息的操作,在泛型內(nèi)部都是無法共工作的。那是否有辦法繞過這個問題來編程,答案就是顯示地傳遞類型標(biāo)簽。

public class Main<T> {

    public T create(Class<T> type) {
        try {
            return type.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        Main<String> m = new Main<String>();
        String s = m.create(String.class);
    }
}
代碼片段展示了一種用類型標(biāo)簽生成新對象的方法,但是這個辦法很脆弱,因為這種辦法要求對應(yīng)的類型必須有默認(rèn)構(gòu)造函數(shù),遇到Integer類型的時候就失敗了,而且這個錯誤還不能在編譯器捕獲。
public class Main<T> {

    public T[] create(Class<T> type) {
        return (T[]) Array.newInstance(type, 10);
    }

    public static void main(String[] args) {
        Main<String> m = new Main<String>();
        String[] strings = m.create(String.class);
    }
}
代碼片段七展示了對泛型數(shù)組的擦除補(bǔ)償,本質(zhì)方法還是通過顯示地傳遞類型標(biāo)簽,通過Array.newInstance(type, size)來生成數(shù)組,同時也是最為推薦的在泛型內(nèi)部生成數(shù)組的方法。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 從三月份找實習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍(lán)閱讀 42,340評論 11 349
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,739評論 18 399
  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,726評論 0 11
  • (一)Java部分 1、列舉出JAVA中6個比較常用的包【天威誠信面試題】 【參考答案】 java.lang;ja...
    獨(dú)云閱讀 7,136評論 0 62
  • 一切都在等你,那個恰到好處的自己。 寫日記時,突然明白了一件事,那就是我們的生活是和誰一起? 年齡的增長讓我們懂得...
    七月半的美麗人生閱讀 362評論 0 0