哈希值-單例模式-線程安全

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、利用鎖保證一塊代碼的原子性

這個例子在之前的《關于鎖》的筆記中的詳細舉例召川,此處就不再列舉南缓。
關于鎖 我對于鎖的初次接觸和簡單理解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市荧呐,隨后出現(xiàn)的幾起案子汉形,更是在濱河造成了極大的恐慌,老刑警劉巖倍阐,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件概疆,死亡現(xiàn)場離奇詭異,居然都是意外死亡峰搪,警方通過查閱死者的電腦和手機岔冀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來概耻,“玉大人使套,你說我怎么就攤上這事罐呼。” “怎么了侦高?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵嫉柴,是天一觀的道長。 經常有香客問我奉呛,道長差凹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任侧馅,我火速辦了婚禮,結果婚禮上呐萌,老公的妹妹穿的比我還像新娘馁痴。我一直安慰自己,他們只是感情好肺孤,可當我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布罗晕。 她就那樣靜靜地躺著,像睡著了一般赠堵。 火紅的嫁衣襯著肌膚如雪小渊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天茫叭,我揣著相機與錄音酬屉,去河邊找鬼。 笑死揍愁,一個胖子當著我的面吹牛呐萨,可吹牛的內容都是我干的。 我是一名探鬼主播莽囤,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼谬擦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了朽缎?” 一聲冷哼從身側響起惨远,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎话肖,沒想到半個月后北秽,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡狼牺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年羡儿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片是钥。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡掠归,死狀恐怖缅叠,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情虏冻,我是刑警寧澤肤粱,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站厨相,受9級特大地震影響领曼,放射性物質發(fā)生泄漏。R本人自食惡果不足惜蛮穿,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一庶骄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧践磅,春花似錦单刁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至檐春,卻和暖如春逻淌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背疟暖。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工卡儒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人誓篱。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓朋贬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親窜骄。 傳聞我的和親對象是個殘疾皇子锦募,可洞房花燭夜當晚...
    茶點故事閱讀 43,527評論 2 349