1. 程式人生 > >執行緒和內部類

執行緒和內部類

01_多執行緒的概述(瞭解)


(1)程序和執行緒
程序:
程序,正在執行的程式。是系統進行資源分配和呼叫的獨立單位。每一個程序都有它自己的記憶體空間和系統資源
 
執行緒:
1 在同一個程序內又可以執行多個任務,而每一個任務我們就可以看成是一個執行緒。
2 是程式的執行單元,執行路徑。是程式使用CPU的最基本的單位。
 a.如果程式只有一條執行路徑,那麼該程式就是單執行緒程式
 b.如果程式有多條執行路徑,那麼該程式就是多執行緒程式
 
簡單記:
程序--一個程式
執行緒--一個程式中的多個任務
 
注意:執行緒是依賴程序而存在

1.單程序單執行緒: 一個人在一個桌子上吃飯
2.單程序多執行緒:多個人在同一個桌子上一起吃飯--------- 資源共享就會發生衝突搶奪
3.多程序單執行緒:多個人每個人都在自己的桌子上吃飯

window 開桌子開銷大  鼓勵大家在一個桌子上吃飯 
linux  開桌子開銷小  鼓勵每個人都在自己的桌子上吃飯  


(2)多執行緒的意義:
一.程式細分成幾個功能相對獨立的模組,防止其中一個功能模組阻塞導致整個程式卡死(GUI程式是典型)
二.提高執行效率,比如多個核同時跑,或者單核裡面,某個執行緒進行IO操作時,另一個執行緒可以同時執行。

單執行緒和多執行緒的區別
單執行緒:安全性高,但是效率低
多執行緒:安全性低,效率高

多執行緒的應用場景
* 共屏軟體同時共享螢幕給多個電腦
* 迅雷開啟多條執行緒一起下載
* QQ同時和多個人一起視訊
* 伺服器同時處理多個客戶端請求 

(3)Java程式執行原理和JVM的啟動是多執行緒的嗎
A:Java程式執行原理
Java命令會啟動java虛擬機器,啟動JVM,等於啟動了一個應用程式,也就是啟動了一個程序。該程序會自動啟動一個 “主執行緒” ,
然後主執行緒去呼叫某個類的 main 方法。所以main方法執行在主執行緒中。

B:JVM的啟動是多執行緒的嗎
JVM啟動至少啟動了垃圾回收執行緒和主執行緒,所以是多執行緒的。





02_多執行緒的實現方式1
繼承Thread類
步驟:
a.自定義類繼承Thread類。
b.在自定義類中重寫run()方法(run()方法執行需要被執行緒執行的程式碼)。
c.建立物件。
d.啟動執行緒。

Thread成員方法
String getName()      返回該執行緒的名稱。 
void   setName(String name) 改變執行緒名稱,使之與引數 name 相同。


注意:
多次啟動一個執行緒是非法的,會報IllegalThreadStateException:非法的執行緒狀態錯誤
啟動執行緒是start()方法,而不是run()方法,run()方法裡面放需要被執行緒執行的程式碼



03_主方法是單執行緒的


注意:
主方法(main方法)是單執行緒的,並且執行在主執行緒中.
java程式執行的時候是多執行緒的,它至少啟動了垃圾回收執行緒和主執行緒

補充:
移動端開發的原則:不要把耗時操作放在主執行緒


04_多執行緒的實現方式2
實現Runnable介面(大多數使用)
步驟:
a.自定義類實現Runnable介面
b.在自定義類中重寫run()方法
c.建立自定義類的物件
d.建立Thread類的物件,並把c步驟建立的物件作為構造引數傳遞

public class MyThread2 implements Runnable{


@Override
public void run() {

for (int i = 0; i < 100; i++) {
Thread t = Thread.currentThread();
System.out.println(t.getName() + ":" + i);
}
}


}


public static void main(String[] args) {

//建立執行緒例項
MyThread2 mt = new MyThread2();
Thread t = new Thread(mt);
t.setName("李四");
//啟動執行緒
t.start();

//建立執行緒例項
Thread t2 = new Thread(mt);
t2.setName("老王");
//啟動執行緒
t2.start();
}

區別
* 檢視原始碼的區別:
* a.繼承Thread : 由於子類重寫了Thread類的run(), 當呼叫start()時, 直接找子類的run()方法
* b.實現Runnable : 建構函式中傳入了Runnable的引用, 成員變數記住了它, start()呼叫run()方法時內部判斷成員變數Runnable的引用是否為空,
不為空的話呼叫Runnable的run()方法
* 繼承Thread
* 好處是:可以直接使用Thread類中的方法,程式碼簡單
* 弊端是:如果已經有了父類,就不能用這種方法
* 實現Runnable介面
* 好處是:即使自己定義的執行緒類有了父類也沒關係,因為有了父類也可以實現介面,而且介面是可以多實現的
* 弊端是:不能直接使用Thread中的方法需要先獲取到執行緒物件後,才能得到Thread的方法,程式碼複雜
 

Thread的靜態方法
static Thread.currentThread();返回當前執行緒物件


alt +shift + m 把一段程式碼抽成一個方法


05_多執行緒模擬火車站售票出現問題


/*
 * 需求:模擬火車站售票
 * 分析
 * 首先需要有火車票的總數量(假設100張),每售出一張則數量減一
 * 當火車票的數量小於1的時候,停止售票
 * 使用多執行緒模擬多個視窗進行售票
 * 
 */

public class TicketThread implements Runnable {
int tickets = 100;//火車票數量

@Override
public void run() {
//出售火車票
while(true) {
//當火車票小於0張,則停止售票
if(tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + ":" +tickets--);
}
}
}


}

注意:
子類重寫父類方法時,如果父類沒有丟擲異常,那麼子類也不能丟擲異常,只能處理異常.(java的重寫規則)


為什麼會出現有時候打印出2個2
暫存器是CPU內部用來存放資料的一些小型儲存區域,用來暫時存放參與運算的資料和運算結果。

執行緒之間資訊共享是在記憶體中,但是變數是讀取到暫存器中來進行操作。
執行緒每次訪問變數的時候,有時候會貪圖便宜直接使用暫存器中得值,而這個值可能已經和
最新的記憶體中得值已經不一樣了。這時就會出現以上不可預期的問題。

以上現象的出現可以理解為
執行緒1在剩2張票的時候將記憶體更新為2,
這時執行緒1,2同時啟動,都讀到了這個2的最新記憶體到暫存器中,
各執行緒操作自己的暫存器,才出現以上不確定現象。




06_分析火車站售票出現問題原因


問題出現的原因:
要有多個執行緒
要有被多個執行緒所共享的資料
多個執行緒併發的訪問共享的資料


public class TicketThread implements Runnable {
int tickets = 100;//火車票數量

@Override
public void run() {
//出售火車票
while(true) {
//當火車票小於0張,則停止售票
if(tickets > 0) {
/*
* t1,t2,t3
* 假設只剩一張票
* t1過來了,他一看有票,他就進來了,但是他突然肚子不舒服,然後他就去上衛生間了
* t2也過來了,他一看也有票,他也進來了,但是他的肚子也不舒服,他也去上衛生間了

* t1上完了衛生間回來了,開始售票
* tickets = 0;
* t2也上完衛生間回來了,他也進行售票
*  tickets = -1;
*/
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + ":" +tickets--);
}
}
}


}


07_使用同步程式碼塊解決多執行緒案例中出現的問題




synchronized:同步(鎖),可以修飾程式碼塊和方法,被修飾的程式碼塊和方法一旦被某個執行緒訪問,
則直接鎖住,其他的執行緒將無法訪問
 
同步程式碼塊:
synchronized(鎖物件){
 
}


注意:鎖物件需要被所有的執行緒所共享
 
 
區別:
同步:安全性高,效率低
非同步:效率高,但是安全性低


public class TicketThread implements Runnable {
int tickets = 100;//火車票數量
Object obj = new Object();

@Override
public void run() {
//出售火車票
while(true) {
synchronized (obj) {


if(tickets > 0) {

try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + ":" +tickets--);
}
}

}
}


}


08_同步方法
如果一個方法中的程式碼都有問題,就可以使用synchronized來修飾方法,這個方法就變成了同步方法
這樣就會把整個方法加上鎖,一個執行緒訪問之後,其他執行緒都無法訪問.這樣就實現了安全性

注意:
非靜態同步方法的鎖物件是this
靜態的同步方法的鎖物件是當前類的位元組碼物件

補充:
當前類的位元組碼物件就是.class檔案,.java檔案編譯後就會生成.class檔案
我們可以通過物件呼叫getClass()方法獲取當前類的位元組碼物件.(第三天反射的時候也會講)

alt +shift + m 把一段程式碼抽成一個方法



****************************以下是補充內容(僅做了解)****************************




09_內部類的概述
成員內部類--類比成員變數---一般在成員變數的位置上
區域性內部類--類比區域性變數---一般在方法內部
匿名內部類--沒有名字的區域性內部類---常用



10_成員內部類的概述和使用
在類的成員位置,和成員變數以及成員方法所在的位置是一樣的
在內部類當中,可以直接訪問外部類的成員,包括私有成員

建立非靜態的內部類物件
//外部類名.內部類名 物件名 = 外部類的物件.內部類物件
Outer.Inner i = new Outer().new Inner();
i.function();


11_成員內部類的修飾符
/*
* 四種許可權修飾符:
* 本類  同一個包下(子類和無關類) 不同包下(子類) 不同包下(無關類)
* private: Y
* 預設: Y Y
* protected: Y Y Y
* public: Y Y Y Y
*/
 
/*
* 成員內部類的修飾符:
* 我們可以使用許可權修飾符修飾成員內部類,但是如果使用私有來修飾,則無法在其他類中訪問
* 我們可以使用static修飾成員內部類,不用再建立外部類的物件了

* 我們可以使用abstract,final修飾成員內部類
*/
 
建立靜態的內部類的物件
//外部類名.內部類名 物件名 = 外部類名.內部類物件;(外部類名習慣放在中間)
Outer2.Inner2 i = new Outer2.Inner2();
i.function();
 
 
12_區域性內部類的概述和使用
在方法內,出了方法之後就無法使用,用的非常少
 
 
13_匿名內部類的概述和格式
匿名內部類其實就是沒有名字的區域性內部類
因為匿名內部類沒有名字,所以我們沒法使用它,只能在定義匿名內部類的時候建立他的物件

格式:
new 類/介面(){
  如果是建立了繼承這個類的子類物件,我們可以重寫父類的方法
  如果是建立了實現這個介面的子類物件,我們必須要實現該介面的所有方法
};
原理:其實是建立了繼承這個類的子類物件或者是建立了實現這個介面的子類物件

class Outer {

//有名字的內部類的定義
class MyInner implements Inner{
@Override
public void function() {
System.out.println("有名字");
}
}

public void method() {

//有名字內部類的物件的建立和呼叫
MyInner my= new MyInner();
my.function();


//匿名內部類實現方式一:匿名內部類的定義,物件的建立和呼叫是一氣呵成的,這種方式只能使用一次.
new Inner() {
@Override
public void function() {
System.out.println("function");
}
}.function();


//匿名內部類實現方式二:用一個變數來接收匿名類的物件(本質是多型),這種方式的好處是可以多次呼叫
Inner i = new Inner() {
@Override
public void function() {
System.out.println("function");
}
};

i.function();
i.function();


}
}


14_匿名內部類的應用場景
作為引數進行傳遞
如果把類當做引數傳遞,並且只用一次的話,可以選擇使用匿名內部類




15_匿名內部類實現執行緒的兩種方式

public static void main(String[] args) {

//方式一:繼承Thread類
new Thread() { //1,繼承Thread類
public void run() { //2,重寫run方法
for(int i = 0; i < 1000; i++) { //3,將要執行的程式碼寫在run方法中
System.out.println("aaaaaaaaaaaaaa");
}
}
}.start(); //4,開啟執行緒

//方式二:實現Runnable介面
new Thread(new Runnable() { //1,將Runnable的子類物件傳遞給Thread的構造方法
public void run() { //2,重寫run方法
for(int i = 0; i < 1000; i++) { //3,將要執行的程式碼寫在run方法中
System.out.println("bb");
}
}
}).start(); //4,開啟執行緒
}


}




-----擴充套件瞭解

16_多執行緒並行和併發的區別


    並行
        兩個任務同時執行,就是甲任務進行的同時,乙任務也在進行。(需要多核CPU)
    併發
        兩個任務都請求執行,而處理器只能按受一個任務,就把這兩個任務安排輪流進行,由於時間間隔較短,使人感覺兩個任務都在執行。
    
    舉例:
        比如我跟兩個網友聊天,左手操作一個電腦跟甲聊,同時右手用另一臺電腦跟乙聊天,這就叫並行。
        如果用一臺電腦我先給甲發個訊息,然後立刻再給乙發訊息,然後再跟甲聊,再跟乙聊。這就叫併發。
    


    


17_執行緒排程(兩種)以及設定執行緒優先順序:
    1_分時排程模型。所有執行緒輪流使用CPU的使用權,平均分配每個執行緒佔用CPU的時間片
    2_搶佔式排程模型。優先讓優先順序高的執行緒使用CPU,若相同,則隨機選擇,優先順序高的執行緒獲取CPU的時間片相對多一些。
    Java使用的是搶佔式排程模型。
        設定執行緒優先順序:
            public final int getPriority();  //返回執行緒物件的優先順序。預設優先順序是5。
            public final void setPriority(); //設定執行緒的優先順序。  
            MAX_PRIORITY最大優先順序值是10
            MIN_PRIORITY最小優先順序是1
            NORM_PRIORITY預設優先順序是5
    執行緒優先級別高僅僅表示執行緒獲取的CPU時間片的機率高,但是要在多次執行的時候才能看到比較好的效果。
    


18_執行緒的生命週期
    1 新建:建立執行緒物件
    2 就緒:有執行資格,沒有執行權
    3 執行:有執行資格,有執行權權  
            阻塞:由於一些操作讓執行緒處於該狀態。沒有執行資格,沒有執行權,而另一些操作卻可以把它啟用,啟用後處於就緒狀態
    4 死亡:執行緒物件變成垃圾,等待回收


    新建→(start())→就緒→(獲取到了CPU的執行權)→執行→(run()結束、中斷執行緒)→死亡(等待被回收)
    執行時也許會有阻塞sleep(),wait(),時間(sleep())到後或喚醒(notify())後繞到就緒狀態,再執行
    被別的執行緒搶到執行權就回到就緒狀態