《實戰(zhàn)高并發(fā)程序設計》讀書筆記-線程基本知識補充

volatile

? Java內(nèi)存模型都是圍繞著原子性预厌、有序性和可見性展開的带饱,為了在適當?shù)膱龊狭诘欤_保線程間的有序性、可見性和原子性辜妓。Java使用了一些特殊的操作或者關鍵字來申明酥泛、告訴虛擬機,在這個地方嫌拣,要尤其注意,不能隨意變動優(yōu)化目標指令呆躲。關鍵字volatile就是其中之一异逐,用于保持線程之間的\color{red}{可見性}

? 但是同時應該注意的是插掂,volatile并不能當鎖用灰瞻,他無法保證共享變量操作的原子性腥例。比如下面的例子,通過volatile是無法保證i++的原子性操作的:

01 static volatile int i=0;
02 public static class PlusTask implements Runnable{
03     @Override
04     public void run() {
05         for(int k=0;k<10000;k++)
06             i++;
07     }
08 }
09
10 public static void main(String[] args) throws InterruptedException {
11     Thread[] threads=new Thread[10];
12     for(int i=0;i<10;i++){
13         threads[i]=new Thread(new PlusTask());
14         threads[i].start();
15     }
16     for(int i=0;i<10;i++){
17         threads[i].join();
18     }
19
20     System.out.println(i);
21 }

? 執(zhí)行上述代碼酝润,如果第6行i++是原子性的燎竖,那么最終的值應該是100000(10個線程各累加10000次)。但實際上要销,上述代碼的輸出總是會小于100000构回。

重點

\color{red}{只能保證線程之間的可見性,和程序的有序性疏咐,不能保證共享變量操作的原子性}纤掸。

線程組

可以通過線程組對不同的線程分批管理

比如:

01 public class ThreadGroupName implements Runnable {
02     public static void main(String[] args) {
03         ThreadGroup tg = new ThreadGroup("PrintGroup");
04         Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
05         Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
06         t1.start();
07         t2.start();
08         System.out.println(tg.activeCount());
09         tg.list();
10     }
11
12     @Override
13     public void run() {
14         String groupAndName=Thread.currentThread().getThreadGroup().getName()
15                 + "-" + Thread.currentThread().getName();
16         while (true) {
17             System.out.println("I am " + groupAndName);
18             try {
19                 Thread.sleep(3000);
20             } catch (InterruptedException e) {
21                 e.printStackTrace();
22             }
23         }
24     }
25 }

通過\color{red}{ThreadGroup}創(chuàng)建一個名為“PrintGroup”的線程組,并將T1和T2兩個線程加入這個組中浑塞。第8借跪、9兩行,展示了線程組的兩個重要的功能酌壕,activeCount()可以獲得活動線程的總數(shù)掏愁,但由于線程是動態(tài)的,因此這個值只是一個估計值卵牍,無法確定精確果港,list()方法可以打印這個線程組中所有的線程信息,對調試有一定幫助辽慕。代碼中第4京腥、5兩行創(chuàng)建了兩個線程,使用Thread的構造函數(shù)溅蛉,指定線程所屬的線程組公浪,將線程和線程組關聯(lián)起來。
線程組還有一個值得注意的方法stop()船侧,它會停止線程組中所有的線程欠气。這看起來是一個很方便的功能,但是它會遇到和Thread.stop()相同的問題(終止業(yè)務邏輯沒有處理完的線程)镜撩,因此使用時也需要格外謹慎预柒。

重點

通過ThreadGroup為線程分組,并且為之按照業(yè)務需要命名袁梗,讓線程的名字更有意義宜鸯,慎用線程組的stop(),這會導致破壞線程業(yè)務邏輯的原子性, 線程組的activeCount()可以獲得活動線程的總數(shù)遮怜,并且可以通過list()方法可以打印這個線程組中所有的線程信息淋袖,對調試有一定幫助

守護線程(Daemon)

? 守護線程是一種特殊的線程,就和它的名字一樣锯梁,它是系統(tǒng)的守護者即碗,在后臺默默地完成一些系統(tǒng)性的服務焰情,比如垃圾回收線程、JIT線程就可以理解為守護線程剥懒。\color{red}{與之相對應的是用戶線程内舟,用戶線程可以認為是系統(tǒng)的工作線程},它會完成這個程序應該要完成的業(yè)務操作初橘。\color{red}{如果用戶線程全部結束验游,這也意味著這個程序實際上無事可做了}。守護線程要守護的對象已經(jīng)不存在了壁却,那么整個應用程序就自然應該結束批狱。因此,\color{red}{當一個Java應用內(nèi)展东,只有守護線程時赔硫,Java虛擬機就會自然退出}

01 public class DaemonDemo {
02     public static class DaemonT extends Thread{
03         public void run(){
04             while(true){
05                 System.out.println("I am alive");
06                 try {
07                     Thread.sleep(1000);
08                 } catch (InterruptedException e) {
09                     e.printStackTrace();
10                 }
11             }
12         }
13     }
14     public static void main(String[] args) throws InterruptedException {
15         Thread t=new DaemonT();
16         t.setDaemon(true);
17         t.start();
18
19         Thread.sleep(2000);
20     }
21 }

? 上述代碼第16行盐肃,將線程t設置為守護線程爪膊。這里注意,\color{red}{設置守護線程必須在線程start()之前設置}砸王,否則你會得到一個類似以下的異常推盛,告訴你守護線程設置失敗。但是你的程序和線程依然可以正常執(zhí)行谦铃。只是被當做用戶線程而已耘成。因此,如果不小心忽略了下面的異常信息,你就很可能察覺不到這個錯誤。

Exception in thread "main" java.lang.IllegalThreadStateException
    at java.lang.Thread.setDaemon(Thread.java:1367)
    at geym.conc.ch2.daemon.DaemonDemo.main(DaemonDemo.java:20)

? 在這個例子中行瑞,由于t被設置為守護線程,系統(tǒng)中只有主線程main為用戶線程师妙,因此在main線程休眠2秒后退出時,整個程序也隨之結束屹培。但如果不把線程t設置為守護線程默穴,main線程結束后,t線程還會不停地打印褪秀,永遠不會結束蓄诽。

重點

設置守護線程必須在線程start()之前設置,否則會得到以下異常,導致守護線程失效媒吗,但是不影響主線程執(zhí)行仑氛。

java.lang.IllegalThreadStateException

線程優(yōu)先級

在Java中,使用1到10表示線程優(yōu)先級蝴猪。一般可以使用內(nèi)置的三個靜態(tài)標量表示:

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 1

數(shù)字越大則優(yōu)先級越高调衰,但有效范圍在1到10之間。下面的代碼展示了優(yōu)先級的作用自阱。高優(yōu)先級的線程傾向于更快地完成嚎莉。

01 public class PriorityDemo {
02     public static class HightPriority extends Thread{
03         static int count=0;
04         public void run(){
05             while(true){
06                 synchronized(PriorityDemo.class){
07                     count++;
08                     if(count>10000000){
09                         System.out.println("HightPriority is complete");
10                         break;
11                     }
12                 }
13             }
14         }
15     }
16     public static class LowPriority extends Thread{
17         static int count=0;
18         public void run(){
19             while(true){
20                 synchronized(PriorityDemo.class){
21                     count++;
22                     if(count>10000000){
23                         System.out.println("LowPriority is complete");
24                         break;
25                     }
26                 }
27             }
28         }
29     }
30
31     public static void main(String[] args) throws InterruptedException {
32         Thread high=new HightPriority();
33         LowPriority low=new LowPriority();
34         high.setPriority(Thread.MAX_PRIORITY);
35         low.setPriority(Thread.MIN_PRIORITY);
36         low.start();
37         high.start();
38     }
39 }

? 上述代碼定義兩個線程,分別為HightPriority設置為高優(yōu)先級沛豌,LowPriority為低優(yōu)先級趋箩。讓它們完成相同的工作,也就是把count從0加到10000000加派。完成后叫确,打印信息給一個提示,這樣我們就知道誰先完成工作了芍锦。這里要注意竹勉,在對count累加前,我們使用synchronized產(chǎn)生了一次資源競爭娄琉。目的是使得優(yōu)先級的差異表現(xiàn)得更為明顯次乓。
? 大家可以嘗試執(zhí)行上述代碼,可以看到孽水,高優(yōu)先級的線程在大部分情況下票腰,都會首先完成任務(就這段代碼而言,試運行多次女气,HightPriority總是比LowPriority快杏慰,但這不能保證在所有情況下,一定都是這樣)炼鞠。

重點

可以通過通過setPriority()設置線程的優(yōu)先級缘滥,從0到10,優(yōu)先級越高越容易搶占資源,但這不是絕對的簇搅,只是說理論上搶占共享資源的幾率更高完域。

synchronized

? Java 內(nèi)置的管程方案(synchronized)使用簡單,synchronized 關鍵字修飾的代碼塊瘩将,在編譯期會自動生成相關加鎖和解鎖的代碼吟税,但是僅支持一個條件變量;而 Java SDK 并發(fā)包實現(xiàn)的管程支持多個條件變量姿现,不過并發(fā)包里的鎖肠仪,需要開發(fā)人員自己進行加鎖和解鎖操作。

? 并發(fā)編程里兩大核心問題——互斥和同步备典,都可以由管程來幫你解決异旧。學好管程,理論上所有的并發(fā)問題你都可以解決提佣,并且很多并發(fā)工具類底層都是管程實現(xiàn)的吮蛹,所以學好管程荤崇,就是相當于掌握了一把并發(fā)編程的萬能鑰匙。

? 關鍵字synchronized的作用是實現(xiàn)線程間的同步潮针。它的工作是對同步的代碼加鎖术荤,使得每一次,只能有一個線程進入同步塊每篷,從而保證線程間的安全性

synchronized的多種用法

  • 指定加鎖對象:
    • 對給定\color{red}{對象加鎖}瓣戚,進入同步代碼前要獲得給定對象的鎖。
  • 直接作用于實例方法:
    • 相當于對當前\color{red}{實例加鎖}焦读,進入同步代碼前要獲得當前實例的鎖子库。
  • 直接作用于靜態(tài)方法:
    • 相當于對當前\color{red}{類加鎖},進入同步代碼前要獲得當前類的鎖矗晃。

以上是原書給的分類仑嗅,但是我感覺可以按照鎖對象這個維度來看synchronized的用法:

鎖對象

鎖實例對象
1.修飾普通方法,鎖的是this
image.png

此時鎖對象是this喧兄,該類的實例對象无畔。

如果實例對象不是一個,那么保證不了共享資源的安全吠冤。

2.synchronized(this)代碼塊
image.png
類鎖浑彰,鎖的是內(nèi)存中唯一存在的class對象
1.修飾靜態(tài)方法
image.png
2.synchronized(*.class)代碼塊
image.png

synchronized的配套使用

image.png
image.png

synchronized的使用例子

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        for(int j=0;j<10000000;j++){
            //類鎖
            synchronized(instance){
                i++;
            }
        }
    }

當然,上述代碼也可以寫成如下形式拯辙,兩者是等價的:

01 public class AccountingSync2 implements Runnable{
02     static AccountingSync2 instance=new AccountingSync2();
03     static int i=0;
04     public synchronized void increase(){
05         i++;
06     }
07     @Override
08     public void run() {
09         for(int j=0;j<10000000;j++){
10             increase();
11         }
12     }
13     public static void main(String[] args) throws InterruptedException {
14         Thread t1=new Thread(instance);//鎖的是當前實例郭变,但是兩個線程是同一個鎖對象
15         Thread t2=new Thread(instance);
16         t1.start();t2.start();
17         t1.join();t2.join();
18         System.out.println(i);
19     }
20 }

上述代碼中,synchronized關鍵字作用于一個實例方法涯保。這就是說在進入increase()方法前诉濒,線程必須獲得當前對象實例的鎖。在本例中就是instance對象夕春。在這里未荒,我不厭其煩地再次給出main函數(shù)的實現(xiàn),是希望強調第14及志、15行代碼片排,也就是Thread的創(chuàng)建方式。這里使用Runnable接口創(chuàng)建兩個線程速侈,并且這兩個線程都指向同一個Runnable接口實例(instance對象)率寡,這樣才能保證兩個線程在工作時,能夠關注到同一個對象鎖上去倚搬,從而保證線程安全冶共。
一種錯誤的同步方式如下

01 public class AccountingSyncBad implements Runnable{
02     static int i=0;
03     public synchronized void increase(){
04         i++;
05     }
06     @Override
07     public void run() {
08         for(int j=0;j<10000000;j++){
09             increase();
10         }
11     }
12     public static void main(String[] args) throws InterruptedException {
13         Thread t1=new Thread(new AccountingSyncBad());
14         Thread t2=new Thread(new AccountingSyncBad());
15         t1.start();t2.start();
16         t1.join();t2.join();
17         System.out.println(i);
18     }
19 }

? 上述代碼就犯了一個嚴重的錯誤。雖然在第3行的increase()方法中,申明這是一個同步方法捅僵。但是當鎖對象是當前實例家卖,兩個線程分別分配了不同實例,導致鎖對象不同庙楚,不是一把鎖篡九,線程安全無法保證。
但我們只要簡單地修改上述代碼醋奠,就能使其正確執(zhí)行。那就是使用synchronized的第三種用法伊佃,將其作用于靜態(tài)方法窜司。將increase()方法修改如下:

public static synchronized void increase(){
    i++;

? 除了用于線程同步、確保線程安全外航揉,synchronized還可以保證線程間的可見性和有序性塞祈。從可見性的角度上講,synchronized可以完全替代volatile的功能帅涂,只是使用上沒有那么方便议薪。就有序性而言,由于synchronized限制每次只有一個線程可以訪問同步塊媳友,因此斯议,無論同步塊內(nèi)的代碼如何被亂序執(zhí)行,只要保證串行語義一致醇锚,那么執(zhí)行結果總是一樣的哼御。而其他訪問線程,又必須在獲得鎖后方能進入代碼塊讀取數(shù)據(jù)焊唬,因此恋昼,它們看到的最終結果并不取決于代碼的執(zhí)行過程,從而有序性問題自然得到了解決(換言之赶促,被synchronized限制的多個線程是串行執(zhí)行的)液肌。

notice:

什么是管程

所謂管程,指的是管理共享變量以及對共享變量的操作過程鸥滨,讓他們支持并發(fā)嗦哆。翻譯為 Java 領域的語言,就是管理類的成員變量和成員方法爵赵,讓這個類是線程安全的吝秕。

Java 采用的是管程技術,synchronized 關鍵字及 wait()空幻、notify()烁峭、notifyAll() 這三個方法都是管程的組成部分。而管程和信號量是等價的,所謂等價指的是用管程能夠實現(xiàn)信號量约郁,也能用信號量實現(xiàn)管程缩挑。但是管程更容易使用,所以 Java 選擇了管程鬓梅。

管程供置,對應的英文是 Monitor,而不是直譯為“監(jiān)視器”绽快。

如果對管程這種技術思想有興趣芥丧,可以繼續(xù)看我的關于這塊的筆記。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末坊罢,一起剝皮案震驚了整個濱河市续担,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌活孩,老刑警劉巖物遇,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異憾儒,居然都是意外死亡询兴,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門起趾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诗舰,“玉大人,你說我怎么就攤上這事训裆∈夹疲” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵缭保,是天一觀的道長汛闸。 經(jīng)常有香客問我,道長艺骂,這世上最難降的妖魔是什么诸老? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮钳恕,結果婚禮上别伏,老公的妹妹穿的比我還像新娘。我一直安慰自己忧额,他們只是感情好厘肮,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著睦番,像睡著了一般类茂。 火紅的嫁衣襯著肌膚如雪耍属。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天巩检,我揣著相機與錄音厚骗,去河邊找鬼。 笑死兢哭,一個胖子當著我的面吹牛领舰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播迟螺,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼冲秽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了矩父?” 一聲冷哼從身側響起劳跃,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎浙垫,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體郑诺,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡夹姥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了辙诞。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辙售。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖飞涂,靈堂內(nèi)的尸體忽然破棺而出旦部,到底是詐尸還是另有隱情,我是刑警寧澤较店,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布士八,位于F島的核電站,受9級特大地震影響梁呈,放射性物質發(fā)生泄漏婚度。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一官卡、第九天 我趴在偏房一處隱蔽的房頂上張望蝗茁。 院中可真熱鬧,春花似錦寻咒、人聲如沸哮翘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽饭寺。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間佩研,已是汗流浹背柑肴。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留旬薯,地道東北人晰骑。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像绊序,于是被迫代替她去往敵國和親硕舆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

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