1秤朗、哈希值
hashCode是通過一定規(guī)則將引用變?yōu)橐粋€int類型的數(shù)值,JDK內部寫好了這種規(guī)則硝皂,但我們可以通過重寫改變這種規(guī)則稽物,而hashCode一般和equals方法同時被重寫。為了方便說明 創(chuàng)建一個Animal類
public class Animal {
private String name;
private int age;
public Animal() {
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
現(xiàn)在我們在測試類中寫下如下語句:
public class AniTest {
public static void main(String[] args) {
Animal an1 = new Animal("狗", 10);
Animal an2 = new Animal("貓", 8);
boolean flag = an1.equals(an2);
System.out.println(flag);
System.out.println(an1.hashCode());
System.out.println(an2.hashCode());
}
}
顯然在沒有改寫equals和hashCode的情況下,先后輸出false和兩個不一樣的哈希值(暫時不討論碰撞)咪奖。如果此處只改寫equals方法:
@Override
public boolean equals(Object obj) {
boolean flag = false;
Animal an2 = (Animal) obj;
if (this.name.equals(an2.name)) {//只要姓名相同就視為同一個對象
flag = true;
}
return flag;
}
public class AniTest {
public static void main(String[] args) {
Animal an1 = new Animal("狗", 10);
Animal an2 = new Animal("貓", 8);
Animal an3 = new Animal("貓", 7);
boolean flag = an1.equals(an2);
System.out.println(flag);
System.out.println(an2.equals(an3));
System.out.println(an1.hashCode());
System.out.println(an2.hashCode());
System.out.println(an3.hashCode());
}
}
顯然an2.equals(an3)是一個true值趟佃,但是二者的哈希值仍不相同揖闸,這樣會做會使得利用hash的集合類料身,在使用該類對象做存儲的時候,會有意想不到的意外:
Animal an1 = new Animal("狗", 10);
Animal an2 = new Animal("貓", 8);
Animal an3 = new Animal("貓", 7);
Map<Animal,Integer> map = new HashMap<>();
map.put(an2, 1);
map.put(an3, 2);
System.out.println(map.get(new Animal("貓", 8)));
System.out.println(map.get(new Animal("貓", 7)));
理論上應該輸出1和2裸燎,但是最終輸出了兩個空值败潦,這是因為沒有改寫hashCode,所以兩個實例具有不同的散列碼令蛉,違反了hashCode的約定珠叔,因此弟劲,Put方法把對象an2(3)放在一個散列桶里兔乞,而get方法去另一個散列桶里查找他的對象凉唐。這時就只能找到null台囱。其實即使不重寫equals這里也會輸出null玄坦,不過改寫了equals之后绘沉,這兩個對象在邏輯上應該是等同的车伞,所以應該查找到相應的值另玖。如果我們改寫一下hashCode方法(改寫方法來自《Effective Java》)
@Override
public int hashCode() {
int result = 17;
result = 37 * result + this.age;
return result;
}
這樣再運行測試程序表伦,就能順利等得到1和2這兩個值 了蹦哼。
2、單例模式
單例模式就是保證類的外部只能同存在一個實例妆丘。想要做到這一點勺拣,首先就要私有化構造方法药有,這樣就無法通過new產生新的對象苹丸。以下用Phone類舉例:
public class Phone {
private Phone() {
}//私有化構造方法谈跛,禁止外部直接訪問
}
如果僅僅這樣,那么就根本無法產生對象蜡励,以下有兩種模式來實現(xiàn)單例:
1、餓漢單例
public class Phone {
private Phone() {
}
private static Phone phone=new Phone();//私有化一個靜態(tài)本類對象兼都,因為在本類內部稽寒,所以可以使用構造方法,而且因為是靜態(tài)的慎王,所以會只會加載一次赖淤。
public static Phone getInstance(){//通過一個靜態(tài)public方法返回這個對象
return phone;
}
}
通過getInstance實例化對象
public class Ph_Test {
public static void main(String[] args) {
Phone myPhone1 = Phone.getInstance();
Phone myPhone2 = Phone.getInstance();
System.out.println(myPhone1);
System.out.println(myPhone2);
}
}
執(zhí)行程序后谅河,兩個Phone的地址相同,即二者指向同一個對象吐限,
餓漢模式還有一種實現(xiàn)方式:
public class Phone {
private Phone() {
}
private static class InstanceKeeper {//利用靜態(tài)內部類實例化一個靜態(tài)本類對象诸典,同理是靜態(tài)的病袄,所以也都只會加載一次
private static Phone phone = new Phone();
}
public static Phone getInstance() {
return InstanceKeeper.phone;//通過上面的靜態(tài)內部類獲得本類對象
}
}
2、懶漢單例
public class Phone {
private Phone() {
}
private static Phone phone;//先聲明一個私有靜態(tài)本類對象脑奠,但是不實例化
public static Phone getInstance() {
if (phone == null) {//如果這個對象為空宋欺,那就實例化 這樣也可以確保只有一個實例
phone = new Phone();
}
return phone;
}
}
同理使用餓漢中的驗證可以得到兩個相同地址的對象胰伍。
3骂租、兩種單例的缺點
1渗饮、懶漢單例
懶漢單例在多線程中可能出現(xiàn)多個不同實例宿刮,例如:
public class Ph_Test {
public static void main(String[] args) {
Thread t0 = new Thread(() -> {//線程t0利用懶漢實例化
Phone myPhone1 = Phone.getInstance();
System.out.println(myPhone1);
});
Thread t1 = new Thread(() -> {//線程t1利用懶漢實例化
Phone myPhone2 = Phone.getInstance();
System.out.println(myPhone2);
});
t0.start();
t1.start();
}
}
執(zhí)行程序后有可能得到不用結果僵缺,因為在懶漢中有一個判定對象不為null磕潮,如果一個線程將已經實例化的地址返回自脯,另一個線程進來后也判斷為null,所以也會實例化一個對象冤今,這就出現(xiàn)了多個不同地址的對象。面對這種問題脚囊,最簡單的方法是在判斷條件里上鎖:
public static Phone getInstance() {
synchronized (Phone.class) {
if (phone == null) {
phone = new Phone();
}
}
return phone;
}
這樣悔耘,多線程就可以同步我擂,確保同時只存在一個不為空的對象實例。不過上鎖本身需要消耗資源看峻,如果對每個線程都上鎖互妓,那么會大大增加開銷,如果在上鎖之前先進行一次模糊的判斷冯勉,那樣可以減少很多次上鎖的過程:
private volatile static Phone phone;//volatile可以使phone能夠在線程運行過程中灼狰,值發(fā)生變化后能及時返回主內存浮禾,被其他線程讀取到
public static Phone getInstance() {//雙重檢驗
if (phone == null) {//如果phone不為null直接返回phone坛悉,不用加鎖
synchronized (Phone.class) {
if (phone == null) {
phone = new Phone();
}
}
}
return phone;
}
因為之前實驗證明裸影,多個線程異步時即使phone為null轩猩,也不能保證phone沒有被(或正在被)實例化荡澎,但是如果不為null,那肯定被實例化了彤委,加上外層判斷能過濾掉絕大部分已經實例化的線程或衡,剩下少部分再通過鎖來保證實例的單一性。
2斯辰、餓漢單例
餓漢單例是線程安全的彬呻,不需要加鎖柄瑰,因為靜態(tài)內容只會加載一次教沾,也正因為只加載一次详囤,那么如果在外部獲得單例之后將單例變?yōu)閚ull之后,該類的對象就只能為null隆箩。
3捌臊、線程安全
說到線程安全就不得不提與之相關的三大特性:原子性理澎、可見性和一致性。
1寇荧、原子性
所謂原子揩抡,在以前被認為是最小的粒子,不可再分割峦嗤。這里用原子來命名這種性質也有這個含義:當一個操作不能夠被中斷烁设,這個操作的結果要么是全部成功钓试,要么是全部失敗亚侠。
int a=0;//1
int b=a;//2
a++;//3
a+=2;//4
上面四個操作中硝烂,只有1是原子性的滞谢。在2中除抛,先從a中讀取數(shù)值到忽,然后賦值給b,是兩步操作护蝶。而3和4本質上是一樣的持灰,從a中獲取數(shù)值负饲,運算數(shù)值堤魁,將運算結果賦值給a喂链,是三步操作。在java內存中有8種操作被認為是原子性的妥泉,一般基本數(shù)據(jù)類型的訪問和讀寫都可以算原子性椭微,只有64位數(shù)據(jù)類型可以分為2次32位操作(最新的JDK中其實也可以實現(xiàn)原子性)。
2盲链、可見性
在了解可見性之前蝇率,要事先了解一些Java的內存模型。在Java中分為主內存和工作內存匈仗,所有數(shù)據(jù)都存儲在主內存中瓢剿,而每個線程也有自己的工作內存。當工作內存需要數(shù)據(jù)時悠轩,會從主內存獲得一份數(shù)據(jù)的副本间狂,然后再工作區(qū)內進行處理,之后會將處理好的數(shù)據(jù)副本更新到主內存中:
從這個模型可以看出,如果有的線程正在在執(zhí)行過程中,還沒有返回數(shù)據(jù)到主內存中時,另一個線程已經從主內存在讀取完(過時)數(shù)據(jù)了腊脱,這就是不可見鳄炉。而在java中泥技,可見性就是指一旦任意工作內存修改了數(shù)據(jù),能夠馬上被其它所有線程知曉這個修改。
3、有序性
下面請看:
int a = 10;//1
int b = 5;//2
int c= a + b;//3
正常情況下應該按照123的順序執(zhí)行代碼族壳,但是在java中為了進行優(yōu)化可能會對指令按照一定規(guī)則進行重排坏平。例如此處就可以按照213的順序執(zhí)行,這完全不會影響結果。但是這種重排只在本線程內有效,有人這樣總結:在本線程內操作都是有序的,但是觀察另一個線程所有操作就是無序的。例如:
public class ForFun {
private boolean flag = false;
private int a = 2;
public void change() {
a = 3;//1
flag = true;//2
}
public void res() {
if (flag) {
System.out.println(Thread.currentThread().getName() + ":" + a * a);
}
}
}
有線程1調用change方法,線程2調用res方法。對于線程1歹颓,它對在change經過重排领跛,按2->1的順序運行胧瓜。在運行完2之后,flag為true,此時線程2正好運行判斷語句妹沙,最后輸出的結果是4悍引,而不是9浓领。
4捎拯、volatile關鍵字
之前在單例模式中吗浩,除了利用鎖保證原子性外升略,還使用了一個關鍵字volatile。它用來修飾變量,有兩個作用:1、保證該變量的可見性浴骂。2、禁止發(fā)生重排尽棕,即保證了有序性询张。资厉。但是volatile并不能完全保證原子性,事實上如果一個被volatile修飾的變量進行了多步操作烹骨,那么就無法保證原子性:
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
}
public class ForFunTest {
public static void main(String[] args) {
final Test test = new Test();
for (int i = 0; i < 10; i++) {
Thread tt = new Thread(() -> {
for (int j = 0; j < 1000; j++)
test.increase();
});
tt.start();
}
while (Thread.activeCount() > 1) { //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.inc);
}
}
}
輸出的結果并不想象中的10000,一般都是小這個數(shù)埋凯。這是因為volatile關鍵字只能夠及時更新數(shù)據(jù)的變化沉颂,如果一個線程A獲得inc數(shù)據(jù)10還沒開始自加踏枣,因為數(shù)據(jù)沒有變化,顯然volatile并不會起作用笛谦。此時線程B也開始獲得inc數(shù)據(jù)笋颤,這是只能獲取10举农,而當A加完后將數(shù)據(jù)更新到主內存后,B也隨后加完更新至主內存中滚停,此時主內存中inc被同樣的數(shù)據(jù)11刷新了兩次,B線程的這次操作其實是無效(或者說有效但沒有意義)的嘀粱,這就導致的最后的結果小于10000.事實上甚至會低于6500锋叨。
5、如何保證線程安全
1逼庞、避免多線程璧南,盡量將操作放在單線程中。
2讼油、線程封閉
1杰赛、棧封閉:將變量定義在方法中
對于方法而言,內部的變量會隨著方法的產生而產生的矮台,也會隨著方法的結束而被回收乏屯。所以多線程在執(zhí)行同樣的方法時,其實是在各自的方法中修改變量嘿架,并不會影響到其他線程瓶珊。
2、ThreadLocale封閉
對于必須使用全局變量的數(shù)據(jù)耸彪,使用ThreadLocale伞芹,這樣也能保證對于變量操作的原子性。
public class ForFun {
private boolean flag = false;
private ThreadLocal<Integer> a = new ThreadLocal<>();
public void change() {
a.set(2);
flag = true;
try {
Thread.sleep(1000);//休眠1秒 其它線程能夠進來
} catch (InterruptedException e) {
e.printStackTrace();
}
a.set(3);
}
public void res() {
if (flag) {
System.out.println(Thread.currentThread().getName() + ":" + a.get());
}
}
}
public class ForFunTest {
public static void main(String[] args) {
ForFun ff = new ForFun();
Thread t0 = new Thread(() -> {
ff.change();
ff.res();
});
Thread t1 = new Thread(() -> {
ff.change();
ff.res();
});
t0.start();
t1.start();
}
}
無論運行多少次蝉娜,結果顯然都是3唱较。
3、利用鎖保證一塊代碼的原子性
這個例子在之前的《關于鎖》的筆記中的詳細舉例召川,此處就不再列舉南缓。
關于鎖 我對于鎖的初次接觸和簡單理解