這篇文章主要講解了“如何理解Java多線程”,文中的講解內(nèi)容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何理解Java多線程”吧!
成都做網(wǎng)站、網(wǎng)站設計,成都做網(wǎng)站公司-創(chuàng)新互聯(lián)已向1000多家企業(yè)提供了,網(wǎng)站設計,網(wǎng)站制作,網(wǎng)絡營銷等服務!設計與技術結合,多年網(wǎng)站推廣經(jīng)驗,合理的價格為您打造企業(yè)品質網(wǎng)站。
進程是程序的一次執(zhí)行過程,是系統(tǒng)運行程序的基本單位,因此進程是動態(tài)的。系統(tǒng)運行一個程序即是從一個進程從創(chuàng)建、運行到消亡的過程。在Java中,當我們啟動main函數(shù)時其實就是啟動了一個JVM的進程,而mian函數(shù)所在的線程就是這個進程中的一個線程,稱為主線程。
線程是比進程更小的執(zhí)行單位。一個進程在其執(zhí)行的過程中可以產(chǎn)生多個線程。與進程不同的是同類的多個線程共享進程的堆和方法區(qū)資源,但每個線程都有自己的程序計數(shù)器、虛擬機和本地方法棧,所以系統(tǒng)在產(chǎn)生一個線程,或在各個線程之間切換工作是,負擔要比進程小很多,所以線程也稱輕量級進程。
并發(fā):同一時間段內(nèi),多個任務都在執(zhí)行(單位時間內(nèi)不一定同時執(zhí)行)
并行:單位時間內(nèi),多個任務同時執(zhí)行。
多線程編程中一般線程的個數(shù)都大于CPU核心的個數(shù),而一個CPU核心在任意時刻內(nèi)只能被一個線程使用,為了讓這些線程都能得到有效執(zhí)行,CPU采取的策略時為每個線程分配時間片并輪轉的形式。當一個線程的時間片用完的時候就會重新處于就緒狀態(tài)讓給其他線程使用,這個過程屬于一次上下文切換。
換句話說,當前任務在執(zhí)行完CPU時間片切換到另一個任務之前會先保存自己的狀態(tài),以便下次再切換會這個任務時,可以再加載這個任務的狀態(tài)。任務從保存到再加載的過程就是一次上下文切換。
最主要的區(qū)別是sleep()
方法沒有釋放鎖,而wait()
方法釋放了鎖。
兩者都可以暫停線程的執(zhí)行。
wait()
通常用于線程間交互/通信,sleep()
通常用于暫停執(zhí)行。
wait()
方法被調用后,線程不會自動蘇醒(除非超時),需要別的線程調用同一個對象上的notify()
或notifyAll()
方法。而sleep()
方法執(zhí)行完后,線程會自動蘇醒。
為什么調用start()
方法時會執(zhí)行run()
方法,為什么不能直接調用run()
方法?
當我們new一個Thread
時,線程進入了新建狀態(tài),調用start()
方法,會啟動一個線程并使線程進入就緒狀態(tài),等分到時間片后就可以開始運行了。 start()
會執(zhí)行線程的相應準備工作,然后自動執(zhí)行run()
方法的內(nèi)容,這是真正的多線程工作。
而直接執(zhí)行run()
方法會把run
方法當作一個main線程下的普通方法去執(zhí)行,并不是在某個線程中執(zhí)行它,所以這不是多線程工作。
synchronized
關鍵字是解決多個線程之間訪問資源的同步性,可以保證被它修飾的方法或代碼塊在任意時刻只能有一個線程執(zhí)行。
synchronized
主要的三種使用方式:
1.修飾實例方法
作用于當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖。
2.修飾靜態(tài)方法
給當前類加鎖,會作用于類的所有對象實例,因為靜態(tài)成員是類成員,不屬于任何一個實例對象,所以線程A調用一個實例對象的非靜態(tài)synchronized
方法,而線程B調用該實例對象所屬類的靜態(tài)synchronized
方法時是允許的,不會沖突互斥。因為訪問靜態(tài)synchronized
方法占用的是當前類的鎖,而訪問非靜態(tài)synchronized
方法占用的是當前實例對象鎖。
3.修飾代碼塊
指定加鎖對象,進入同步代碼庫前要獲得給定對象的鎖。
synchronized和ReentrantLock:
1.兩者都是可重入鎖 即自己可以再次獲取自己的內(nèi)部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖時還是可以獲取的。
2.前者依賴JVM而后者依賴API synchronized
是依賴于JVM實現(xiàn)的,ReentrantLock
是依賴于JDK層面實現(xiàn)的。 3.ReentrantLock比synchronized功能多 ReentrantLock增加了一些高級功能,主要說有三點:①等待可中斷②可實現(xiàn)公平鎖③可實現(xiàn)選擇性通知。
當前Java內(nèi)存模型下,線程可以把變量保存到本地內(nèi)存(如寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還在繼續(xù)使用它在寄存器中變量值的拷貝,造成數(shù)據(jù)的不一致。
volatile
關鍵字就是解決這個問題,指示JVM這個變量不穩(wěn)定,每次使用它都要到主存中進行讀取。除此之外還有一個重要的作用是防重排。
并發(fā)執(zhí)行的三個重要特性:
1.原子性 要么所有的操作都得到執(zhí)行并且不會收到任何因素干擾而中斷,要么所有的操作都不執(zhí)行。可使用synchronized
來保證代碼原子性。
2.可見性 當對一個共享變量進行了修改后,那么另外的線程都是立即可以看到修改后的最新值。volatile
可以保證可見性。
3.有序性 代碼在執(zhí)行過程中的先后順序,Java在編譯器以及運行期間的優(yōu)化,代碼的執(zhí)行順序未必就是編寫代碼時候的順序,即指令重排。volatile
可以禁止指令重排優(yōu)化。
通常情況下,我們創(chuàng)建的變量時可以被任何一個線程訪問并修改的。如果要實現(xiàn)每一個線程都有自己的專屬本地變量該如何解決?這就需要ThreadLocal
類了。
ThreadLoca
l類主要解決的就是讓每個線程綁定自己的值,可以將ThreadLocal
類比喻成存放數(shù)據(jù)的盒子,盒子中可以存儲每個線程的私有數(shù)據(jù)。
當創(chuàng)建一個ThreadLocal
變量后,訪問這個變量的每個線程都會有這個變量的本地副本,這也是ThreadLocal
名稱的由來??梢允褂胓et()和set()方法來獲取默認值或將其值更改為當前線程所存副本的值,從而避免了線程安全問題。
實際上,ThreadLocal
類有一個靜態(tài)內(nèi)部類ThreadLocalMap
,可以把ThreadLocalMap
看作是ThreadLocal
類定制的HashMap
,最終的變量是放在了當前線程的ThreadLocalMap
中,而不是ThreadLocal
類上,可以看作ThreadLocal
類是ThreadLocalMap
的封裝,傳遞了值。
如果再同一個線程中聲明了兩個ThradLocal
對象的話,會使用Thread
內(nèi)部僅有的那個ThreadLocalMap
存放數(shù)據(jù)的,TheadLocalMap
的key就是ThreadLocal
對象,value就是ThreadLocal
對象調用set方法設置的值。
ThreadLocalMap
使用的key為ThreadLocal
的弱引用,而value
是強引用。所以ThreadLocal
沒有被外部強引用的情況下,在垃圾回收的時候,key 會被清理掉,而value不會被 清理掉。這樣一來,ThreadLocalMap
中就會出現(xiàn)key為null的Entry。假如我們不做任何措施的話,value永遠無法被GC回收,這個時候就可能會產(chǎn)生內(nèi)存泄露。ThreadLocalMap
實現(xiàn)中已經(jīng)考慮了這種情況,在調用set()、get()、remove()方法的時會清理掉key
為null
的記錄。使用完ThreadLocal
方法后最好手動調用remove()方法。
池化技術大家應該很熟悉,線程池、數(shù)據(jù)庫連接池、Http連接池等等都是對這思想的應用。池化技術的思想主要是為了減少每次獲取資源的消耗,提高對資源的利用率。 使用線程池,可以降低資源消耗、提高響應速度、提高線程的可管理性。
Runnable
接口不會返回結果或拋出檢查異常,但Callable
接口可以。 工具類Excutors
可以實現(xiàn)Runnable
對對象和Callable
對象之間的相互轉換。
@FunctionalInterface public interface Runnable{ //沒有返回值也無法拋出異常 public abstract void run(); }
execute()
方法用于提交不需要返回值的任務,所以無法判斷任務是否被線程池執(zhí)行成功與否。
submit()
方法用于提交需要返回值的任務。線程池會返回一個Future類型對象,通過這個對象可以判斷任務是否執(zhí)行成功。
通過構造器ThreadpoolExecutor
實現(xiàn)(下面介紹)。
通過工具類Executors
實現(xiàn)(不推薦) ThreadPoolExecutor: .
FixedThreadPool
:該訪法返回一個固定線程數(shù)量的線程池。該線程池中的線程數(shù)量始終不變。當有一個新的任務提交時,線程池中若有空閑線程,則立即執(zhí)行。若沒有,則新的任務會被暫存在一個任務隊列中,待有線程空閑時,便處理在任務隊列中的任務。
SingleThreadExecutor
:方法返回- 個只有一一個線程的線程池。若多余一個任務被提交到該線程池,任務會被保存在一個任務隊列中,待線程空閑,按先入先出的順序執(zhí)行隊列中的任務。
CachedThreadPool
:該方法返回一個可 根據(jù)實際情況調整線程數(shù)量的線程池。線程池的線程數(shù)量不確定,但若有空閑線程可以復用,則會優(yōu)先使用可復用的線程。若所有線程均在工作,又有新的任務提交,則會創(chuàng)建新的線程處理任務。所有線程在當前任務執(zhí)行完畢后,將返回線程池進行復用。
ThreadPoolExecutor
構造函數(shù)重要參數(shù)分析:
corePoolSize
:核?線程數(shù)線程數(shù)定義了最?可以同時運?的線程數(shù)量。
maximumPoolSize
:當隊列中存放的任務達到隊列容量的時候,當前可以同時運?的線程數(shù)量變?yōu)樽?線程數(shù)。
workQueue
:當新任務來的時候會先判斷當前運?的線程數(shù)量是否達到核?線程數(shù),如果達到的話,新任務就會被存放在隊列中。
keepAliveTime
:當線程池中的線程數(shù)量?于 corePoolSize 的時候,如果這時沒有新的任務提交,核?線程外的線程不會?即銷毀,?是會等待,直到等待的時間超過了keepAliveTime 才會被回收銷毀;
unit
:keepAliveTime 參數(shù)的時間單位。
threadFactory
:executor 創(chuàng)建新線程的時候會?到。
handler
:飽和策略 ①ThreadPoolExecutor.AbortPolicy:拋出 RejectedExecutionException 來拒絕新任務的處理。 ②ThreadPoolExecutor.CallerRunsPolicy:調?執(zhí)???的線程運?任務。會降低對于新任務提交速度,影響程序的整體性能,另外會增加隊列容量。 ③ThreadPoolExecutor.DiscardPolicy:不處理新任務,直接丟棄掉。 ④ThreadPoolExecutor.DiscardOldestPolicy:此策略將丟棄最早的未處理的任務請求。
模擬了 10 個任務,我們配置的核?線程數(shù)為 5 、等待隊列容量為 100 ,所以每次只能存在5個任務同時執(zhí)?,剩下的5個任務會被放到等待隊列中去。當前的 5 個任務之?完成后,才會之?剩下的 5 個任務。
public class MyRunnable implements Runnable { private String command; public MyRunnable(String s) { this.command = s; } @Override public void run() { System.out.println(Thread.currentThread().getName() + "開始時間:" + new Date()); processCommand(); System.out.println(Thread.currentThread().getName() + "結束時間:" + new Date()); } private void processCommand() { try { Thread.sleep(3000); //設花費3秒執(zhí)行任務 } catch (InterruptedException e) { e.printStackTrace(); } } @Override public String toString() { return this.command; } }
public class Demo { private static final int CORE_POOL_SIZE = 5;//核?線程數(shù)為 5 private static final int MAX_POOL_SIZE = 10;//最?線程數(shù) 10 private static final int QUEUE_CAPACITY = 100;//容量100 private static final Long KEEP_ALIVE_TIME = 1L;//等待時間為 1L public static void main(String[] args) { //通過ThreadPoolExecutor構造函數(shù)?定義參數(shù)創(chuàng)建 ThreadPoolExecutor executor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_CAPACITY), new ThreadPoolExecutor.CallerRunsPolicy());//飽和策略 for (int i = 0; i < 10; i++) { //創(chuàng)建WorkerThread對象(WorkerThread類實現(xiàn)了Runnable接?) Runnable worker = new MyRunnable("" + i); executor.execute(worker);//執(zhí)?Runnable } executor.shutdown();//終?線程池 while (!executor.isTerminated()) { } System.out.println("結束"); } } /*運行結果如下: pool-1-thread-3開始時間:Mon Mar 29 22:46:02 CST 2021 pool-1-thread-2開始時間:Mon Mar 29 22:46:02 CST 2021 pool-1-thread-4開始時間:Mon Mar 29 22:46:02 CST 2021 pool-1-thread-5開始時間:Mon Mar 29 22:46:02 CST 2021 pool-1-thread-1開始時間:Mon Mar 29 22:46:02 CST 2021 pool-1-thread-2結束時間:Mon Mar 29 22:46:07 CST 2021 pool-1-thread-2開始時間:Mon Mar 29 22:46:07 CST 2021 pool-1-thread-3結束時間:Mon Mar 29 22:46:07 CST 2021 pool-1-thread-3開始時間:Mon Mar 29 22:46:07 CST 2021 pool-1-thread-4結束時間:Mon Mar 29 22:46:07 CST 2021 pool-1-thread-4開始時間:Mon Mar 29 22:46:07 CST 2021 pool-1-thread-5結束時間:Mon Mar 29 22:46:07 CST 2021 pool-1-thread-5開始時間:Mon Mar 29 22:46:07 CST 2021 pool-1-thread-1結束時間:Mon Mar 29 22:46:07 CST 2021 pool-1-thread-1開始時間:Mon Mar 29 22:46:07 CST 2021 pool-1-thread-2結束時間:Mon Mar 29 22:46:12 CST 2021 pool-1-thread-3結束時間:Mon Mar 29 22:46:12 CST 2021 pool-1-thread-4結束時間:Mon Mar 29 22:46:12 CST 2021 pool-1-thread-5結束時間:Mon Mar 29 22:46:12 CST 2021 pool-1-thread-1結束時間:Mon Mar 29 22:46:12 CST 2021 結束 */
感謝各位的閱讀,以上就是“如何理解Java多線程”的內(nèi)容了,經(jīng)過本文的學習后,相信大家對如何理解Java多線程這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關知識點的文章,歡迎關注!