九種基本類型及封裝類
基本類型 | 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á)式只能是short
、char
、int
、long
整數(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)存模型的抽象示意圖如下:
從上圖來看,線程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
來理解JVM
每new
一個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ù)他們的
hashcode
去hash表
中找他的對象,當(dāng)兩個對象的hashcode
相同,那么就是說他們這兩個對象放在Hash
表中的同一個key
上,那么他們一定在這個key
上的鏈表
上。那么此時就只能根據(jù)Object
的equal
方法來比較這個對象是否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)行完之后,object
和objArr
都已經(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都是先把SoftReference
和WeakReference
中的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()
,ConditionObject
有await()
、signal()
、signalAll()
,類似于Ojbect
類的wait()
、notify()
、notifyAll()
。這些方法都是用來實現(xiàn)線程間通信。lock
將synchronized
的互斥性和線程間通信分離開來,一個lock
可以有多個condition
。另外lock
的signal
可以實現(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)
JVM
用synchronized
管理鎖定請求和釋放時,JVM
在生成線程轉(zhuǎn)儲時能夠包括鎖定信息。這些對調(diào)試非常有價值,因為它們能標(biāo)識死鎖或者其他異常行為的來源。Lock
類只是普通的類,JVM
不知道具體哪個線程擁有Lock
對象。
ArrayList,LinkedList和Vector
-
ArrayList
和Vector
都是基于數(shù)組
實現(xiàn)的,所以查詢效率高,插入效率低
。 -
LinkedList
基于雙向鏈表實現(xiàn)的,所以插入效率高,查詢效率低
。 -
Vector
使用了synchronized
方法,所以線程安全,性能比ArrayList
低。 -
LinkedList
實現(xiàn)了List接口
,還提供了額外的get
,remove
,insert
方法在LinkedList
的首部或尾部,這些操作使LinkedList
可被用作棧(Stack
),隊列(Queue
)或雙向隊列(deque
)。 -
ArrayList
和LinkedList
允許null元素,重復(fù)元素
。
HashMap和HashTable
都實現(xiàn)了Map接口
-
HashMap允許key為null,value為null而HashTable不允許
,如果新加入的key
和之前重復(fù)了,會覆蓋
之前的value
。 -
HashTable線程安全,而HashMap不是線程安全
。因此單線程下HashTable
比HashMap
慢。 -
HashMap不能保證隨著時間推移Map中的元素次序是不變的
。 -
Hashtable中hash數(shù)組默認(rèn)大小是11
,增加的方式是old*2+1
。HashMap中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>
,把HashMap
的key
作為HashSet
的值,HashMap
的value
是一個固定的Object
對象,因為HashMap
的key
是不允許重復(fù)
的,所以HashSet
里的元素也是不能重復(fù)
的,也可以看出HashSet的查詢效率很高
。
String,StringBuilder和StringBuffer
-
CharSequence
接口:一個字符序列
。String
,StringBuilder
和StringBuffer
都實現(xiàn)了它。 -
String
類:是常量,不可變. -
StringBuilder
類:只可以在單線程的情況下進(jìn)行修改(線程不安全
),字符串拼接用,除了StringBuffer
可用場景外。 -
StringBuffer
類:可以在多線程的情況下進(jìn)行改變(線程安全
),比如:在http
請求中拼接。 -
StringBuilder
比StringBuffer
效率高,應(yīng)該盡量使用StringBuilder
。
Excption與Error包結(jié)構(gòu)
結(jié)構(gòu)圖:
Throwable
-
Throwable
是Java
語言中所有錯誤或異常的超類。 -
Throwable
包含兩個子類:Error
和Exception
。它們通常用于指示發(fā)生了異常情況。 -
Throwable
包含了其線程創(chuàng)建時線程執(zhí)行堆棧的快照,它提供了printStackTrace()
等接口用于獲取堆棧跟蹤數(shù)據(jù)等信息。
Exception
-
Exception
及其子類是Throwable
的一種形式,它指出了合理的應(yīng)用程序想要捕獲的條件。
RuntimeException
-
RuntimeException
是那些可能在Java
虛擬機(jī)正常運(yùn)行期間拋出的異常的超類。 - 編譯器不會檢查
RuntimeException
異常。 例如,除數(shù)為零時,拋出ArithmeticException
異常。RuntimeException
是ArithmeticException
的超類。當(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)部類都可以用
public
,protected
,private
修飾。- 方法中都可以調(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ù)組的方法。