【Java学习笔记九】多线程
程序:計(jì)算機(jī)指令的集合,它以文件的形式存儲在磁盤上,是應(yīng)用程序執(zhí)行的藍(lán)本。
進(jìn)程:是一個程序在其自身的地址空間中的一次執(zhí)行活動。進(jìn)程是資源申請、調(diào)度和獨(dú)立運(yùn)行的單位,因此,它使用系統(tǒng)中的運(yùn)行資源。而程序不能申請系統(tǒng)資源,不能被系統(tǒng)調(diào)度,也不能作為獨(dú)立運(yùn)行的單位,它不占用系統(tǒng)的運(yùn)行資源。作為藍(lán)本的程序可以被多次加載到系統(tǒng)的不同內(nèi)存區(qū)域分別執(zhí)行,形成不同的進(jìn)程。基于進(jìn)程的特點(diǎn)是允許計(jì)算機(jī)同時運(yùn)行兩個或更多的程序。
線程:是進(jìn)程中的一個單一的順序控制流程,一個進(jìn)程在執(zhí)行過程中,可以產(chǎn)生多個線程。每個線程也有自己產(chǎn)生、存在和消亡的過程。
線程又稱為輕量級進(jìn)程,它和進(jìn)程一樣擁有獨(dú)立的執(zhí)行控制,由操作系統(tǒng)負(fù)責(zé)調(diào)度,區(qū)別在于線程沒有獨(dú)立的存儲空間,而是和所屬進(jìn)程中的其它線程共享一個存儲空間。這使得線程間的通信遠(yuǎn)較進(jìn)程簡單,而進(jìn)程之間的通信則比較困難,另外在資源的占用上,線程比進(jìn)程要小。
線程(Thread)和進(jìn)程(Process)的關(guān)系很密切,進(jìn)程和線程是兩個不同的概念,進(jìn)程的范圍大于線程
通俗的講,進(jìn)程就是一個程序,線程是這個程序能夠同時做的各件事情。例如:媒體播放器運(yùn)行時就是一個進(jìn)程,而媒體播放器同時下載文件和播放歌曲就是兩個線程。
從另一個角度講,每個進(jìn)程都擁有一組完整的屬于自己的變量,而線程則共享一個進(jìn)程中的這些數(shù)據(jù)。
主線程:程序啟動時,一個線程立即運(yùn)行,該線程稱為程序的主線程。(main()方法),其他線程都是由主線程產(chǎn)生的,主線程通常必須最后完成執(zhí)行,因此需要執(zhí)行各種關(guān)閉動作。
觀察以下程序:
public class MainThreadDemo {public static void main(String args[]) {Thread t = Thread.currentThread();System.out.println("當(dāng)前線程名稱是: " + t.getName());t.setName("MyJavaThread");System.out.println("改名后線程名稱是: " + t.getName());System.out.println("輸出當(dāng)前線程: " + t); } }代碼分析:
- Thread.currentThread()是一個靜態(tài)方法,返回正在執(zhí)行的線程對象的引用。
- getName()方法可以得到當(dāng)前引用線程的名稱
- setName(String s)可以改變線程的內(nèi)部名稱
- 當(dāng)將一個線程對象作為輸出時,將輸出[線程名稱,優(yōu)先級別,線程組名 ]
- 每個線程都屬于一個線程組,如果沒有設(shè)定,則由JVM來設(shè)定。
并發(fā)編程:在計(jì)算機(jī)編程中有一個基本概念,就是在同一時刻處理多個任務(wù)的思想。許多程序設(shè)計(jì)問題都要求程序能夠停下正在做的工作,轉(zhuǎn)而處理某個其他問題,然后再返回主進(jìn)程。多線程的任務(wù)相比傳統(tǒng)的進(jìn)程而言(只有一個主線程),可以同時有多個地方執(zhí)行代碼。
把問題切分成多個可獨(dú)立運(yùn)行的部分(任務(wù)),從而提高程序的響應(yīng)能力。在程序中,這些彼此獨(dú)立運(yùn)行的部分稱之為線程,上述概念被稱為“并發(fā)”。
Java提供了類 java.lang.Thread 來方便多線程編程,這個類提供了大量的方法方便控制線程.
Thread類最重要的方法是run(),它為Thread類的方法start()所調(diào)用,為了指定我們自己的代碼,需要覆蓋run()方法,來提供我們線程所要執(zhí)行的代碼。
創(chuàng)建線程
例如:
package Test;class FileTransThread extends Thread{ private String fileName; public FileTransThread(String fileName){ this.fileName = fileName; } public void run(){ System.out.println("傳送" + fileName); try{ Thread.sleep(1000 * 10); }catch(Exception ex){} System.out.println(fileName + "傳送完畢"); } } public class Main { public static void main(String[] args) throws Exception { FileTransThread ft1 = new FileTransThread("文件1"); FileTransThread ft2 = new FileTransThread("文件2"); FileTransThread ft3 = new FileTransThread("文件3"); ft1.start(); System.out.println("1");System.out.println("2");ft2.start();System.out.println("3");System.out.println("4");ft3.start(); System.out.println("5");System.out.println("6");} }運(yùn)行結(jié)果:(兩次完全相同的運(yùn)行,但是結(jié)果卻不同,說明線程大概是同步進(jìn)行的,而且順序是隨機(jī)的)
例如:
package Test;class FileTransRunnable implements Runnable { private String fileName; public FileTransRunnable(String fileName){ this.fileName = fileName; }public void run(){ System.out.println("傳送" + fileName); try{ Thread.sleep(1000 * 10); }catch(Exception ex){} System.out.println(fileName + "傳送完畢"); } } public class Main { public static void main(String[] args) throws Exception { FileTransRunnable ft1 = new FileTransRunnable("文件1"); FileTransRunnable ft2 = new FileTransRunnable("文件2"); FileTransRunnable ft3 = new FileTransRunnable("文件3");Thread f1=new Thread(ft1); //創(chuàng)建線程Thread f2=new Thread(ft2);Thread f3=new Thread(ft3);f1.start(); f2.start();f3.start();} }運(yùn)行對象:
- 對象可以自由地繼承自另一個類。
- 同一個runnable對象可以傳遞給多個線程,可以實(shí)現(xiàn)資源共享。
線程的啟動
新建的線程不會自動開始運(yùn)行,必須通過strat()方法啟動線程。如果不調(diào)用這個方法,線程將不會運(yùn)行。
線程的實(shí)現(xiàn)
線程從創(chuàng)建、啟動到終止的整個過程叫做一個生命周期。線程總共由五個狀態(tài)。
- 等待阻塞:運(yùn)行的線程執(zhí)行wait()方法,該線程放入等待池中。
- 同步阻塞:運(yùn)行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,該線程放入鎖池中。
- 其他阻塞:運(yùn)行的線程執(zhí)行sleep()或join()方法,或者發(fā)出了I/O請求時,該線程置為阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉(zhuǎn)入就緒狀態(tài)。
簡單來講:
線程的調(diào)度
millis參數(shù)設(shè)定睡眠的時間,以毫秒為單位。當(dāng)睡眠結(jié)束后,就轉(zhuǎn)為就緒(Runnable)狀態(tài)。sleep()方法的平臺移植性較好。
另外,join()方法還有帶超時限制的重載版本。 例如t.join(5000);則讓線程等待5000毫秒,如果超過這個時間,則停止等待,變?yōu)榭蛇\(yùn)行狀態(tài)。
多線程的互斥和同步
線程的同步
在多線程的程序中,多個線程可能會對同一個資源并發(fā)訪問。這種情況下,如果不對共享的資源進(jìn)行保護(hù),就可能產(chǎn)生問題。
所謂同步(synchronize),就是在發(fā)出一個功能調(diào)用時,在沒有得到結(jié)果之前,該調(diào)用就不返回,同時其它線程也不能調(diào)用這個方法。
通俗地講,一個線程是否能夠搶占CPU,必須考慮另一個線程中的某種條件,而不能隨便讓操作系統(tǒng)按照默認(rèn)方式分配CPU,如果條件不具備,就應(yīng)該等待另一個線程運(yùn)行,直到條件具備。
同步的原理
Java中每個對象都有一個內(nèi)置鎖,當(dāng)程序運(yùn)行到非靜態(tài)的synchronized同步方法上時,自動獲得與正在執(zhí)行代碼類的當(dāng)前實(shí)例(this實(shí)例)有關(guān)的鎖。獲得一個對象的鎖也稱為獲取鎖、鎖定對象、在對象上鎖定或在對象上同步。
一個對象只有一個鎖。所以,如果一個線程獲得該鎖,就沒有其他線程可以獲得鎖,直到第一個線程釋放(或返回)鎖。這也意味著任何其他線程都不能進(jìn)入該對象上的synchronized方法或代碼塊,直到該鎖被釋放。釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊。
同步的實(shí)現(xiàn)
在Java中通過互斥鎖標(biāo)志Synchronized關(guān)鍵字的運(yùn)用來實(shí)現(xiàn)同步。Java中同步有兩種方法:
- 實(shí)現(xiàn)方法:在要標(biāo)志為同步的方法前加上synchronized關(guān)鍵字。如:public synchronized void call(String msg){ }
- 實(shí)現(xiàn)原理:當(dāng)調(diào)用對象的同步方法時,線程取得對象鎖或監(jiān)視器,如果另一個線程試圖執(zhí)行同步方法,他就會發(fā)現(xiàn)被鎖住了,就會進(jìn)入掛起狀態(tài),直到對象監(jiān)視器上的鎖被釋放為止。當(dāng)鎖住放啊的線程從方法中返回時,只有一個排隊(duì)等候的線程可以訪問對象。
- 鎖的作用域:該方法被執(zhí)行的整個時間。
- 臨界區(qū):只希望防止多個線程同時訪問方法內(nèi)部的部分代碼,而不是防止訪問整個方法,通過這種方式分離出來的代碼段被稱為“臨界區(qū)”,即需要進(jìn)行互斥的代碼段。
- 實(shí)現(xiàn)方法:用synchronized來指定某個對象,此對象的鎖被用來對花括號內(nèi)的代碼進(jìn)行同步控制。如:
- 實(shí)現(xiàn)原理:在進(jìn)入同步代碼前,必須得到object對象的鎖,如果其他線程已經(jīng)得到這個鎖,那么就得等到鎖被釋放后才能進(jìn)入臨界區(qū)。
- 鎖的作用域:只在代碼塊運(yùn)行的時間內(nèi)
例子:
class TicketRunnable implements Runnable { private int ticketNum = 3; // 以3 張票為例 public void run() { while (true) { String tName = Thread.currentThread().getName(); // 將需要獨(dú)占 CPU的代碼用 synchronized(this)包圍起來 synchronized (this) { if (ticketNum <= 0) { System.out.println(tName + "無票"); break; } else { try { Thread.sleep(1000);// 程序休眠 1000 毫秒 }catch (Exception ex) {} ticketNum--; // 代碼行1 System.out.println(tName + "賣出一張票,還剩" + ticketNum + "張票'); } } } } } public class Main { public static void main(String[] args){ TicketRunnable tr = new TicketRunnable(); Thread th1 = new Thread(tr, "thread 1"); Thread th2 = new Thread(tr, "thread 2"); th1.start(); th2.start(); } }從以上代碼可以看出,該方法的本質(zhì)是將需要獨(dú)占 CPU 的代碼用synchronized(this)包圍起來。如前所述,一個線程進(jìn)入這段代碼之后,就在 this 上加了一個標(biāo)記,直到該線程將這段代碼運(yùn)行完畢,才釋放這個標(biāo)記。如果其他線程想要搶占 CPU,先要檢查 this 上是否有這個標(biāo)記。若有,就必須等待。
但是可以看出,該代碼實(shí)際上運(yùn)行較慢,因?yàn)橐粋€線程的運(yùn)行,必須等待另一個線程將同步代碼段運(yùn)行完畢。因此,從性能上講,線程同步是非常耗費(fèi)資源的一種操作。我們要盡量控制線程同步的代碼段范圍,理論上說,同步的代碼段范圍越小,段數(shù)越少越好,因此在某些情況下,推薦將小的同步代碼段合并為大的同步代碼段。
死鎖
如果出現(xiàn)一種極端情況,一個線程等候另一個對象,而另一個對象又在等候下一個對象,以此類推。這個“等候鏈”如果進(jìn)入封閉狀態(tài),也就是說,最后那個對象等候的是第一個對象,此時,所有線程都會陷入無休止的相互等待狀態(tài),造成死鎖。盡管這種情況并非經(jīng)常出現(xiàn),但一旦碰到,程序的調(diào)試將變得異常艱難。
發(fā)生死鎖必須同時滿足的四個條件:
死鎖的實(shí)例:(待更,腦袋不轉(zhuǎn)了)
總結(jié)
以上是生活随笔為你收集整理的【Java学习笔记九】多线程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 知道有没有这种效果的js,jQ插件?点击
- 下一篇: 【Java学习笔记十】输入输出流