1.進程
進程是正在運行的程序,是系統進行資源分配和調用的獨立單位,每一個進程都有他自己的內存空間和系統資源。
多進程:單進程的計算機只能一次做一件事,而多進程就可以在同一個時間段內執行多個任務,提高CPU的使用率。
問:一邊玩遊戲一邊聽音樂是同時進行的嗎?
並不是,因爲單CPU在某個時間點上只能進行一個任務,只是CPU在做着程序間的高效切換而讓我們感覺是同時進行的。
2.線程
在一個進程內又可以執行多個任務。而這每一個任務就可以看成是一個線程。
線程是程序的執行單元、執行路徑,是程序使用CPU的最基本單位。
單線程:程序只有一條執行路徑
多線程:程序有多條執行路徑
多線程的意義?
答:多線程的存在,不是爲了提高程序的執行速度,其實是爲了提高應用程序的使用率。
程序的執行其實都是在搶CPU的資源,CPU的執行權。
多個線程是在搶這個資源,而其中的某一個進程如果執行路徑比較多,就會有更高的機率搶到CPU的執行權。
我們不能保證哪一個線程能夠在哪個時刻搶到,所以線程的執行有隨機性。
並行:前者是邏輯上同時發生,指在某一個時間段內同時運行多個程序
併發:後者是物理上同時發生,指在某一個時間點同時運行多個程序(高併發問題)
3.Java程序運行原理:
由Java命令啓動JVM,JVM啓動就相當於啓動了一個進程。
接着該進程創建了一個主線程去調用main方法。
問:JVM虛擬機的啓動是單線程還是多線程的?
多線程的
原因是例如垃圾回收線程也要先啓動,否則很容易出現內存溢出。所以是多線程的。
4.如何實現多線程程序
由於線程依賴於進程而存在,所以要實現多線程必須先創建一個進程。
而進程是由系統創建的,所以必須去調用系統功能創建一個進程。
而Java是無法直接調用系統功能的,所以需要去調用底層C/C++寫好的程序來實現多線程。因此Java提供了一些類供我們實現多線程。
5.Thread類:通過查看API,可以發現有兩種實現多線程方式(實際上三種)
(1)方式1:繼承Thread類
步驟:
A:自定義MyThread類繼承Thread類
B:MyThread類重寫run()方法
不是類中的所有代碼都需要被線程執行,爲了區分哪些代碼能夠被線程執行,Java提供了Thread類中的run()用來包含那些被線程執行的代碼
C:創建對象
D:啓動線程
注意:
A:run()方法直接調用的話和普通方法一樣,都是單線程的,無論調用幾次。
B:爲了有多線程效果,應該是調用start()方法開啓線程。start()方法的作用有兩個,1是啓動線程,2是調用run()方法。
C:一個線程對象的start()方法只能同時調用一次,要想讓run()方法裏的代碼多線程運行,需要創建多個該線程對象並調用她們的start()方法。
面試題:run()方法和start()方法有什麼區別?
run():僅僅是封裝被線程執行的代碼,直接調用是普通方法
start():首先啓動線程,再由Jvm去調用run()方法
<1>如何獲取線程對象的名稱?
public final String getName():獲取線程的名稱
<2>如何設置線程對象的名稱?
A(set方法):public final void setName(String name):設置線程的名稱
B(構造方法):public MyThread(String name) {
super(name); //Thread類有該方法
} //重寫有參構造方法
<3>針對不是Thread類的子類中如何獲取線程對象呢?
public static Thread currentThread():返回當前正在執行的線程對象
用法:Thread.currentThread().getName()
<4>線程調度
線程有兩種調度模型:
分時調度模型 所有線程輪流使用 CPU 的使用權,平均分配每個線程佔用 CPU 的時間片
搶佔式調度模型 優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那麼會隨機選擇一個,優先級高的線程獲取的 CPU 時間片相對多一些。
Java使用的是搶佔式調度模型。
設置優先級方法:
public final int getPriority() :獲取線程的優先級數
public final void setPriority(int newPriority):設置線程的優先級數
線程默認優先級是5(NORMAL_PRIORITY)
線程優先級的範圍是1-10(MIN_PRIORITY ~ MAX_PRIORITY)
注意:線程優先級僅僅代表線程獲取CPU時間片的機率高,要在次數比較多,多運行幾次的情況下才能看出效果。
IllegalArgumentException:非法參數異常,表明向方法傳遞了一個不合法或者不正確的參數
<5>線程控制
線程休眠:public static void sleep(long millis):在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),此操作受到系統計時器和調度程序精度和準確性的影響。
Thread.sleep(1000); 休眠1秒
線程加入:public final void join():等待該線程終止,這時候該線程其他線程對象暫停運行,等該線程結束再運行
線程禮讓:public static void yield():暫停當前正在執行的線程對象,並執行其他線程。 作用是讓多個線程執行更加和諧,但是不能保證每個線程一人一次。
守護線程:public final void setDaemon(boolean on):將線程標記爲守護線程或者用戶線程。當正在運行的線程都是守護線程時,Java虛擬機自動退出。該方法必須在線程啓動前調用。
中斷線程:public final void stop():讓線程停止,過時了,但是還可以用
public void interrupt():中斷線程,把線程狀態終止,並拋出一個InterruptedException異常
<6>線程的生命週期(4個狀態):
新建:創建線程對象
就緒:有執行資格,沒有執行權
運行:有執行資格,有執行權
阻塞:由於一些操作讓線程處於該狀態,沒有執行資格,沒有執行權
而另一些操作可以激活線程,此時它處於就緒狀態
死亡:線程對象變成垃圾,等待被回收。
(2)方式二:實現Runnable接口
步驟:
A:自定義MyRunnable類實現接口Runnable接口
B:重寫run()方法
C:創建MyRunnable類的對象
D:創建Thread類的對象,並把C步驟的對象作爲構造參數傳遞
面試題:爲什麼有了第一種方式還要來第二種方式?
A:可以避免java單繼承帶來的侷限性
B:第二種方式適合多個相同程序的代碼
5.線程安全性
只要線程裏的操作不是原子性的(不可拆分的),那麼在對線程進行延遲操作(sleep())時,就有可能存在安全性問題。
<1>解決線程安全問題的基本思想
首先想爲什麼出現問題?(也是我們判斷是否有問題的標準)
A:是否是多線程環境
B:是否有共享數據
C:是否有多條語句操作共享數據
如何解決多線程安全問題呢?
基本思想:讓程序沒有安全問題的環境。
怎麼實現呢?
把多個語句操作共享數據的代碼給鎖起來,讓任意時刻只能有一個線程執行即可。(同步)
<2>同步代碼塊
A:格式:
synchronized(對象){
需要同步的代碼;
}
B:同步可以解決安全問題的根本原因就在那個對象上。該對象如同鎖的功能。每次只能有一個進程同時訪問該同步代碼塊。
C:同步代碼塊的鎖對象可以是 任意對象
D:同步方法的格式以及鎖對象問題
同步方法是指把同步關鍵字加在方法上 private synchronized void method(){}
同步方法的鎖是什麼呢? —— this ,因爲每個方法都自帶一個隱藏對象this
E:靜態方法及鎖對象問題
靜態方法的鎖對象是什麼呢?———類的字節碼文件 類.class
<3>同步的特點
同步的前提:存在多個線程
解決問題的時候要注意:多個線程使用的是同一個鎖對象
同步的好處:同步的出現解決了多線程的安全問題。
同步的弊端:當線程相當多時,因爲每個線程都會去判斷同步上的鎖,這是很耗費資源的,無形中會降低程序的運行效率。
<4>線程安全類(同步)
StringBuffer sb = new StringBuffer() ;
Vector<String> v = new Vector<String>() ; //我們並不用它
Hashtable<String, String> h = new Hashtable<String, String>() ;
//如何創建安全的List?
List<String> list1 = new ArrayList<String>() ; //線程不安全
List<String> list2 = Collection.synchronizedList(new ArrayList<String>()) ; //線程安全
6.Lock鎖
雖然已經可以理解同步代碼塊和同步方法的鎖對象的問題,但是我們並沒有直接看到在哪邊加上了鎖,在哪裏釋放了鎖。
爲了更清晰的表達如何加鎖和釋放鎖,JDK5提供了一個新的鎖對象Lock。
加鎖:void lock():獲取鎖
解鎖:void lock():釋放鎖
7.死鎖
(1)同步弊端
A:效率低
B:如果出現了同步嵌套,就容易產生死鎖問題
(2)死鎖問題及其代碼
是指兩個或者兩個以上的線程在執行的過程中,因爭奪資源產生的一種互相等待現象
8.線程間的通信:不同種類的線程針對同一個資源的操作
當不同種類的線程針對同一個資源進行操作的時候容易出現線程安全問題
解決方案:
A:不同種類的線程都要加鎖
B:不同種類的線程加的鎖要是同一把(對象是同一個)
等待喚醒:
Object類中提供了三種方法:(設置一個flag來判斷資源狀態)
wait():等待,線程立即釋放鎖
notify():喚醒單個線程,並不代表可以立馬執行,還得搶CPU資源
notify():喚醒所有線程
爲什麼這些方法不定義在Thread中呢?
因爲這些方法的調用必須通過鎖對象調用,而我們剛纔試用的鎖對象是任意鎖對象
所以這些方法必須定義在Object類裏。
9.線程組:把多個線程組合到一起,它可以對一批線程進行分組管理,Java允許程序直接對線程組進行控制。
線程默認的線程組是main
構造方法:
ThreadGroup(String name) :構造一個新線程組。
方法:
String getName() :返回此線程組的名稱。
void setDaemon(boolean daemon) :設置此線程組的所有線程爲守護線程(後臺線程)
void destroy() :銷燬此線程組及其所有子組。
10.線程池
程序啓動一個新線程成本是比較高的,因爲它涉及到要與操作系統進行交互。而使用線程池可以很好的提高性能,尤其是當程序中要創建大量生存期很短的線程時,更應該考慮使用線程池。
線程池裏的每一個線程代碼結束後,並不會死亡,而是再次回到線程池中成爲空閒狀態,等待下一個對象來使用。
在JDK5之前,我們必須手動實現自己的線程池,從JDK5開始,Java內置支持線程池
JDK5新增了一個Executors工廠類來產生線程池,有如下幾個方法
public static ExecutorService newCachedThreadPool()
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
這些方法的返回值是ExecutorService對象,該對象表示一個線程池,可以執行Runnable對象或者Callable對象代表的線程。它提供瞭如下方法
Future< T> submit(Runnable task)
< T> Future< T> submit(Callable< T> task)
(1)如何實現線程池?
A:創建一個線程池對象,控制要創建幾個線程對象
public static ExecutorService newFixedThreadPool(int nThreads):nThreads表示幾個線程
B:這種線程池的線程可以執行Runnable對象或者Callable對象代表的線程
即 做一個類實現Runnable接口
C:調用如下方法,添加線程到線程池
Future< T> submit(Runnable task)
D:結束線程池(如果不結束線程池,線程池裏的線程只是閒置,但會一直存在)
void shutdown() : 啓動一次順序關閉,執行以前提交的任務,但不接受新任務。
//創建一個線程池對象
ExecutorService pool = Executors.newFixedThreadPool(2) ;
//調用submit()方法
pool.submit(new MyRunnable()) ;
pool.submit(new MyRunnable()) ;
//結束線程
pool.shutdown();
11.方式3:Callable方式
帶泛型返回值的多線程方式,但是它僅限於線程池存在。
步驟和剛纔演示線程池執行Runnable對象的差不多。
好處:
A:可以有返回值
B:可以拋出異常
弊端:
A:代碼比較複雜,所以一般不用
submit()方法返回的Future< T> 這個中括號裏面的T是返回值的類型,該返回值可通過Future對象調用submit()方法來得到。
Future< Integer> f1 = pool.submit(new MyCallable(100)) ;
Integer i = f1.get() ;
12.匿名內部類方式使用多線程,如下:
/*
* 繼承Thread類方式
*/
new Thread() {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":"
+ i);
}
};
}.start();
/*
* 實現Runnable接口
*/
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":"
+ i);
}
}
}) {
}.start();
/*
* 面試題:這種情況下會報錯嗎?不會的話是運行誰的start()方法?
*/
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":"
+ i);
}
}
}) {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":"
+ i);
}
};
}.start();
13.定時器
依賴Timer和TimerTask這兩個類:
Timer:定時
public Timer():創建一個新計時器。相關的線程不 作爲守護程序運行。
public void schedule(TimerTask task , long delay):安排在指定延遲後執行指定的任務。(delay作爲毫秒值,1000是1秒,即1秒後執行task任務)
public void schedule(TimerTask task, long delay, long period):安排指定的任務從指定的延遲後開始進行重複的固定延遲執行。(第一次執行在delay時候執行任務task,然後在每隔period後執行一次task)
public void cancel():終止此計時器,丟棄所有當前已安排的任務。
TimerTask:任務
14.多線程常見面試題
(1)多線程有幾種實現方案?分別是哪幾種?
兩種。
繼承Thread類
實現Runnable接口
擴展一種:實現Callable接口。這個得和線程池結合。
(2):同步有幾種方式,分別是什麼?
兩種。
同步代碼塊
同步方法
(3):啓動一個線程是run()還是start()?它們的區別?
run():封裝了被線程執行的代碼,直接調用僅僅是普通方法的調用
start():啓動線程,並由JVM自動調用run()方法
(4):sleep()和wait()方法的區別
sleep():必須指定時間;不釋放鎖。
wait():可以不指定時間,也可以指定時間;釋放鎖。
(5):爲什麼wait(),notify(),notifyAll()等方法都定義在Object類中
因爲這些方法的調用是依賴於鎖對象的,而同步代碼塊的鎖對象是任意鎖。
而Object代碼任意的對象,所以,定義在這裏面。
(6):線程的生命週期圖
新建 – 就緒 – 運行 – 死亡
新建 – 就緒 – 運行 – 阻塞 – 就緒 – 運行 – 死亡
建議:畫圖解釋。