摘要:當線程執行完后進入狀態,表示線程執行結束。其中和表示兩個線程。但要注意,讓出并不表示當前線程不執行了。關鍵字其作用是防止指令重排和使線程對一個對象的修改令其他線程可見。
JMM特性一覽
Java Memory Model的關鍵技術點都是圍繞著多線程的原子性、可見性和有序性來建立的。因此我們首先需要來了解這些概念。
原子性(Atomicity)原子性是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個人操作一旦開始,就不會被其他的線程干擾。
比如對一個靜態全局變量int i,兩個線程同時對它賦值,線程A給他賦值1,線程B給它賦值為-1.那么不管這么2個線程以合作方式、何種步調工作,i的值要么是1,要么是-1。線程A和B之間是沒有干擾的。這就是原子性的一個特點,不可被中斷。
可見性(Visibility)可見性是指當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道這個修改。顯然,對于串行程序來說,可見性問題是不存在的。因為你在任何一個操作步驟中修改了某個變量,那么在后續的步驟中,讀取這個變量的值,一定是修改后的新值。
有序性(Ordering)有序性問題是三個問題中最難理解的。對于一個線程的執行代碼而言,我們總是習慣地認為代碼的執行是從先往后,依次執行。這么理解也不是說完全錯誤,因為就一個線程內而言,確實會表現成這樣。但是,在并發時,程序的執行可能就會出現亂序。給人直觀的感覺就是:寫在前面的代碼,會在后面執行。然而有序性的問題的原因因為是程序在執行時,可能會進行指令重排,重排后的指令與原指令的順序未必一致。 那么在這里由于篇幅關系就不在展開介紹,有興趣的讀者可以自行搜索Java指令重排和CPU流水線等資料。
哪些指令不能重排——Happen-Before規則雖然Java虛擬機和執行系統會對指令進行一定的重排,但是指令重排是有規則的,并非所有的指令都可以隨便改變位置。原則基本包括以下:
程序順序原則:一個線程內保證語義的串行性
a=1; b=a+1; //第二條語句依賴于第一條執行結果。所以不允許指令重排。
volatile規則:volatile變量的寫,先發生與讀,這保證了volatile變量的可見性。一般用volatile修飾的都是經常修改的對象。
鎖規則:解鎖(unlock)必然發生在隨后的加鎖(lock)前
傳遞性:A先于B,B先于C,那么A必然先于C
線程的start()方法先于它的每一個動作
線程的所有操作先于線程的終結(Thread.join())
線程的中斷(interrupt())先于被中斷線程的代碼
對象的構造函數執行、結束先于finalize()方法
Java多線程線程所有的狀態都在Thread.State枚舉類中定義:
public enum State { /** * 表示剛剛創建的線程,這種線程還沒開始執行。 **/ NEW, /** * 調用start()方法后,線程開始執行,處于RUNNABLE狀態, * 表示線程所需要的一切資源以及準備好。 **/ RUNNABLE, /** * 當線程遇到synchronized同步塊,就進入了BLOCKED阻塞狀態。 * 這時線程會暫停執行,直到獲得請求的鎖。 **/ BLOCKED, /** * WAITING和TIMED_WAITING都表示等待狀態,他們是區別是WAITING表示進入一個無時間限制的等待 * TIMED_WAITING會進入一個有時間限制的等待。 * WAITING的狀態正是在等待特殊的事件,如notify()方法。而通過join()方法等待的線程,則是等待目標線程的終止。 * 一旦等到期望的時間,線程就會繼續執行,進入RUNNABLE狀態。 * 當線程執行完后進入TERMINATED狀態,表示線程執行結束。 **/ WAITING, TIMED_WAITING, TERMINATED; }線程的基本操作 新建線程
新建線程很簡單。只要使用new關鍵字創建一個線程對象,并且將其start()起來即可。start()方法額就會新建一個線程并讓這個線程執行run()方法。
常見就是有人直接對一個線程對象執行run()方法,那么只會在當前的線程中串行執行run()中的代碼 。
最后要說的是,默認的Thread.run()就是直接調用內部的Runnable接口。因此,使用Runnable接口告訴線程該做什么,更為合理。
終止線程Stop()方法是用不得的,會直接終止運行中的線程,并立刻釋放鎖。比如一個線程寫數據到一般被中止,則會寫壞。
那么最簡單的方法可以考慮給線程做一個死循環,然后對一個類似Flag的變量進行判斷,變量變化時退出循環。JDK所提供的線程中斷也是類似于此。
線程中斷線程中斷是重要的線程協作機制,中斷就是讓線程停止執行,但這個停止執行非stop()的暴力方式。JDK提供了更安全的支持,就是線程中斷。
線程中斷并不會使線程立即停止,而是給線程發送一個通知,告訴目標線程有人希望你退出。至于目標線程接到通知后什么時候停止,完全由目標線程自行決定。這點很重要,如果線程接到通知后立即退出,我們就又會遇到類似stop()方法的老問題。
與線程有關的三個方法,
中斷線程
void Thread.interrupt()
說明:Thread.interrupt() 是一個實例方法,他通知目標線程中斷,也就是設置中斷標志位。中斷標志位表示當前線程已經被中斷了。
判斷是否被中斷
boolean Thread.isInterrupted()
說明:Thread.isInterrupted() 也是實例方法,他判斷當前線程是否被中斷(通過檢查中斷標志位)
判斷是否被中斷,并清除當前中斷狀態
static boolean Thread.interrupted()
說明:Thread.interrupted() 是靜態方法,判斷當前線程的中斷狀態,但同時會清除當前線程的中斷標志位狀態。
Thread.sleep()方法會讓當前線程休眠若干時間,它會拋出一個interruptedException中斷異常。interruptedException是必須被捕獲的——當線程在sleep時,如果被中斷,這個異常就產生。
public class InterruptExample { public static void main(String [] a) throws InterruptedException{ Thread t1 = new Thread("線程小哥 - 1 "){ @Override public void run() { while (true){ /** * 必須得判斷是否接受到中斷通知,如果不寫退出方法,也無法將當前線程退出. */ if (Thread.currentThread().isInterrupted()){ System.out.println(Thread.currentThread().getName() + " Interrupted ... "); break; } try { /** * 處理業務邏輯花費10秒. * 而在這時,主線程發送了中斷通知,當線程在sleep的時候如果收到中斷 * 則會拋出InterruptedException,如果在異常中不處理,則線程不會中斷. * */ Thread.sleep(10000); } catch (InterruptedException e) { System.out.println("線程在睡眠中遭到中斷...."); /** * 在sleep過程中,收到中斷通知,拋出異常.可以直接退出線程. * 但如果還需要處理其他業務,則需要重新中斷自己.設置中斷標記位. * 這樣在下次循環的時候 線程發現中斷通知,才能正確的退出. */ Thread.currentThread().interrupt(); } Thread.yield(); } } }; t1.start(); try { /** * 處理業務500毫秒 * 然后發送中斷通知,此時t1線程還在sleep中. */ Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } /** * 給目標線程發送中斷通知 * 目標線程中必須有處理中斷通知的代碼 * 否則,就算發送了通知,目標線程也無法停止. */ t1.interrupt(); } }等待(wait)和通知(notify)
為了支持多線程之間的協作,JDK提供了兩個非常重要的等待方法wait()和nofity()方法。這兩個方法并不是Thread類中的,而是Object類,這意味著任何對象都可以調用這兩個方法。
這兩個方法的簽名如下:
public final void wait() throws InterruptedException public final native void notify()
如果一個線程調用了object.wait()方法,那么這個線程就會停止執行而轉為等待狀態,進入obj對象的等待隊列。這個等待隊列可能有多個線程,因為系統運行多個線程同時等待同一個對象。其他線程調用obj.notify()方法時,它就會從等待隊列中隨機選擇一個線程并將其喚醒。注意這個選擇是不公平的,是隨機的。
object.wait()方法并不是可以隨便調用。它必須包含在對應的synchronized語句中。無論是wait還是notify都必須首先獲得目標對象的一個監視器 。如下圖,顯示了wait()和nofity的工作流程細節。其中T1和T2表示兩個線程。T1在正確執行wait方法后,首先必須獲得object對象的監視器。而wait方法在執行后,會釋放這個監視器,這樣做的目的使得其他等待object對象上的線程不至于因為T1的休眠而全部無法正常執行。
線程T2在notify()調用前,也必須獲得object的監聽器。所幸,此時T1已經釋放了這個監視器。因此,T2可以順利獲得object的監視器。接著,T2執行了notify()方法嘗試喚醒一個等待線程,這里假設喚醒了T1。T1在被喚醒后,要做的第一件事并不是執行后續的代碼,而是要嘗試重新獲得object的監視器。而這個監視器也正是T1在wait()方法執行前所持有的那個。如果暫時無法獲得,T1還必須要等待這個監視器。當監視器順利獲得后,T1才可以真正意義上的繼續執行。
注意::Object.wait()和Thread.sleep()方法都可以讓線程等待若干的時間。除了wait()可以被喚醒外,另一個最主要的區別就是wait()方法會釋放目標對象的鎖,而Thread.sleep()方法不會釋放任何資源。
掛起(suspend)和繼續執行(resume)線程不推薦使用suspend()去掛起線程 的原因是因為suspend()在導致線程暫停的同時,并不會去釋放任何資源。此時,其他任何線程都想訪問它暫用的鎖時,都會被導致牽連,導致無法正常運行。直到對應的線程上進行了resume()操作,被掛起的線程才能繼續,從而其他所有阻塞在相關鎖上的線程也可以繼續執行。但是,如果resume()操作意外地在suspend()前就執行了,那么被掛起的線程可能就很難有機會被繼續執行。并且,更嚴重的是:它鎖占用的鎖不會被釋放,因此可能會導致整個操作系統工作不正常。而且,對于被掛起的線程,從它的線程上看狀態,居然會是Runnable,這是最氣的。
等待線程結束(join)和謙讓(yield)join的方法簽名:
public final void join () throws InterruptedException //一直阻塞當前線程,直到目標線程執行完畢 public final synchronized void join (long millis) throws InterruptedException//和之前一樣,不過增加了最大等待時間
public static native void yield();
這是一個靜態方法,一旦執行,它會使當前線程讓出CPU。但要注意,讓出CPU并不表示當前線程不執行了。當前線程在讓出CPU以后,還會進行CPU資源爭奪,但是是否能夠再次分配到就要看人品了。
如果你覺得一個線程不那么重要,或者優先級非常低,而且又害怕它會占用太多的CPU資源,那么可以在適當的時候調用Thread.yield(),給予其他重要線程更多的工作機會。
關鍵字volatile其作用是防止CPU指令重排和使線程對一個對象的修改令其他線程可見。
對于Java的內存模型來說,每個volatile會在線程的工作內存從保留一個拷貝,只不過java內存模型通過對volatile變量的添加了特殊機制保證了變量的可見性。線程在修改volatile類型變量以后必須立即保存到主內存,在使用變量前必須從主內存加載數據,同時還做了一些禁止指令重排序的操作。對于各個線程的工作內存(私有內存)來說,存在volatile變量不一致的時刻,但是對于執行引擎來說,通過了上面的幾條規則保證了變量是一致的。
可參考: Java并發編程之volatile關鍵字解析
線程安全的概念與synchronized并行程序開發的一大關注重點就是線程安全。一般來說,程序并行化就是為了獲得更高的執行效率,但前提是,不能以犧牲正確性為代價。如果程序并行化以后,連基本的執行結果都無法保證,那么并行程序本身也就沒有任何意義了。
volatile并不能真正的保障線程安全。它只能確保一個線程修改了數據后,其他線程能夠看到這個改動。但當兩個線程同時修改某一個數據時,卻依然會產生沖突。
關鍵字synchronized的作用是實現線程間的同步。它的工作是對同步的代碼加鎖,使得每一次,只能有一個線程進入同步塊,從而保證線程間的安全。
關鍵字synchronized可以有多種用法:
指定加鎖對象:對給定對象加鎖,進入同步代碼前要獲得給定對象的鎖。
直接作用于實例方法:相當于對當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。
直接作用于靜態方法:相當于對當前類加鎖,進入同步代碼前要獲得當前類的鎖。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/65184.html
摘要:前提深入理解內存模型程曉明著,該書在以前看過一遍,現在學的東西越多,感覺那塊越重要,于是又再細看一遍,于是便有了下面的讀書筆記總結。同步同步是指程序用于控制不同線程之間操作發生相對順序的機制。線程之間的通信由內存模型控制。 showImg(https://mmbiz.qpic.cn/mmbiz_jpg/1flHOHZw6RtPu3BNx3zps1JhSmPICRw7QgeOmxOfTb...
摘要:前提深入理解內存模型程曉明著,該書在以前看過一遍,現在學的東西越多,感覺那塊越重要,于是又再細看一遍,于是便有了下面的讀書筆記總結。同步同步是指程序用于控制不同線程之間操作發生相對順序的機制。線程之間的通信由內存模型控制。 showImg(https://segmentfault.com/img/remote/1460000013474312?w=1920&h=1271); 前提 《深...
摘要:線程之間的通信由內存模型本文簡稱為控制,決定一個線程對共享變量的寫入何時對另一個線程可見。為了保證內存可見性,編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。 并發編程模型的分類 在并發編程中,我們需要處理兩個關鍵問題:線程之間如何通信及線程之間如何同步(這里的線程是指并發執行的活動實體)。通信是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的...
摘要:編譯器,和處理器會共同確保單線程程序的執行結果與該程序在順序一致性模型中的執行結果相同。正確同步的多線程程序的執行將具有順序一致性程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。 前情提要 深入理解Java內存模型(六)——final 處理器內存模型 順序一致性內存模型是一個理論參考模型,JMM和處理器內存模型在設計時通常會把順序一致性內存模型作為參照。JMM和處理器內...
摘要:基礎問題的的性能及原理之區別詳解備忘筆記深入理解流水線抽象關鍵字修飾符知識點總結必看篇中的關鍵字解析回調機制解讀抽象類與三大特征時間和時間戳的相互轉換為什么要使用內部類對象鎖和類鎖的區別,,優缺點及比較提高篇八詳解內部類單例模式和 Java基礎問題 String的+的性能及原理 java之yield(),sleep(),wait()區別詳解-備忘筆記 深入理解Java Stream流水...
閱讀 1129·2021-10-27 14:13
閱讀 2636·2021-10-09 09:54
閱讀 897·2021-09-30 09:46
閱讀 2424·2021-07-30 15:30
閱讀 2166·2019-08-30 15:55
閱讀 3409·2019-08-30 15:54
閱讀 2847·2019-08-29 14:14
閱讀 2771·2019-08-29 13:12