1.多執行緒
1.1.多執行緒介紹
學習多執行緒之前,我們先要了解幾個關於多執行緒有關的概念。
程式:正在執行的程式。確切的來說,當一個程式進入記憶體執行,即變成一個程式,程式是處於執行過程中的程式,並且具有一定獨立功能,程式是系統進行資源分配和排程的一個獨立單位。程式是正在執行的程式,程式負責給程式分配記憶體空間,而每一個程式都是由程式程式碼組成的,這些程式碼在程式中執行的流程就是執行緒。
執行緒:執行緒是程式中的一個執行單元,負責當前程式中程式的執行,一個程式中至少有一個執行緒。一個程式中是可以有多個執行緒的,這個應用程式也可以稱之為多執行緒程式。
簡而言之:一個程式執行後至少有一個程式,一個程式中可以包含多個執行緒,但至少有一個執行緒
。什麼是多執行緒呢?即就是一個程式中有多個執行緒在同時執行。
1.2.多執行緒執行原理
大部分作業系統都支援多程式併發執行,現在的作業系統幾乎都支援同時執行多個任務。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟體,同時還開著畫圖板,dos視窗等軟體。感覺這些軟體好像在同時執行著。
其實這些軟體在某一時刻,只會執行一個程式。這是為什麼呢?這是由於CPU(中央處理器)在做著高速的切換而導致的
。對於CPU而言,它在某個時間點上,只能執行一個程式,即就是說只能執行一個程式,CPU不斷地在這些程式之間切換。只是我們自己感覺不到。為什麼我們會感覺不到呢?這是因為CPU的執行速度相對我們的感覺實在太快了,雖然CPU在多個程式之間輪換執行,但我們自己感到好像多個程式在同時執行。
多執行緒真的能提高效率嗎?其實並不是這樣的,因為我們知道,CPU會在多個程式之間做著切換,如果我們開啟的程式過多,CPU切換到每一個程式的時間也會變長,我們也會感覺機器執行變慢。所以合理的使用多執行緒可以提高效率,但是大量使用,並不能給我們帶來效率上的提高。
1.3.主執行緒
回想我們以前學習中寫過的程式碼,當我們在dos命令列中輸入java空格類名回車後,啟動JVM,並且載入對應的class檔案。虛擬機器並會從main方法開始執行我們的程式程式碼,一直把main方法的程式碼執行結束。如果在執行過程遇到迴圈時間比較長的程式碼,那麼在迴圈之後的其他程式碼是不會被執行的。如下程式碼演示:
class Demo
{
String name;
Demo(String name)
{
this.name = name;
}
void show()
{
for (int i=1;i<=20 ;i++ )
{
System.out.println("name="+name+",i="+i);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Demo d = new Demo("小強");
Demo d2 = new Demo("旺財");
d.show();
d2.show();
System.out.println("Hello World!");
}
}
複製程式碼
若在上述程式碼中show方法中的迴圈執行次數很多,這時書寫在d.show();下面的程式碼是不會執行的,並且在dos視窗會看到不停的輸出name=小強,i=值,這樣的語句。為什麼會這樣呢?
原因是:jvm啟動後,必然有一個執行路徑(執行緒)從main方法開始的。一直執行到main方法結束。這個執行緒在java中稱之為主執行緒。當主執行緒在這個程式中執行時,如果遇到了迴圈而導致程式在指定位置停留時間過長,無法執行下面的程式。
可不可以實現一個主執行緒負責執行其中一個迴圈,由另一個執行緒負責其他程式碼的執行。實現多部分程式碼同時執行。這就是多執行緒技術可以解決的問題。
1.4.如何建立執行緒
1.4.1.建立執行緒方式一:繼承Thread類
該如何建立執行緒呢?通過API中的英文Thread的搜尋,查到Thread類。通過閱讀Thread類中的描述。建立新執行執行緒有兩種方法。一種方法是將類宣告為 Thread 的子類。該子類應重寫 Thread 類的 run 方法。接下來可以分配並啟動該子類的例項。
建立執行緒的步驟:
1. 定義一個類繼承Thread。
2. 重寫run方法。
3. 建立子類物件,就是建立執行緒物件。
4. 呼叫start方法,開啟執行緒並讓執行緒執行,同時還會告訴jvm去呼叫run方法。
class Demo extends Thread //繼承Thread
{
String name;
Demo(String name)
{
this.name = name;
}
//複寫其中的run方法
public void run()
{
for (int i=1;i<=20 ;i++ )
{
System.out.println("name="+name+",i="+i);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
//建立兩個執行緒任務
Demo d = new Demo("小強");
Demo d2 = new Demo("旺財");
//d.run(); 這裡仍然是主執行緒在呼叫run方法,並沒有開啟兩個執行緒
//d2.run();
d2.start();//開啟一個執行緒
d.run();//主執行緒在呼叫run方法
}
}
複製程式碼
列印部分結果:由於多執行緒操作,輸出資料會有所不同
name=旺財,i=1
name=小強,i=1
name=旺財,i=2
name=小強,i=2
name=小強,i=3
name=旺財,i=3
name=旺財,i=4
name=旺財,i=5
name=旺財,i=6
name=旺財,i=7
..........
思考:執行緒物件呼叫 run方法和呼叫start方法區別?
執行緒物件呼叫run方法不開啟執行緒。僅是物件呼叫方法。執行緒物件呼叫start開啟執行緒,並讓jvm呼叫run方法在開啟的執行緒中執行。
1.4.2.繼承Thread類原理
為什麼要繼承Thread類,並呼叫其的start方法才能開啟執行緒呢?
繼承Thread類:因為Thread類描述執行緒事物,具備執行緒應該有功能。
那為什麼不直接建立Thread類的物件呢?
1 Thread t1 = new Thread();
2 t1.start();//這樣做沒有錯,但是該start呼叫的是Thread類中的run方法,而這個run方法沒有做什麼事情,更重要的是這個run方法中並沒有定義我們需要讓執行緒執行的程式碼。
複製程式碼
建立執行緒的目的是什麼?
是為了建立單獨的執行路徑,讓多部分程式碼實現同時執行。也就是說執行緒建立並執行需要給定的程式碼(執行緒的任務)。對於之前所講的主執行緒,它的任務定義在main函式中。自定義執行緒需要執行的任務都定義在run方法中。Thread類中的run方法內部的任務並不是我們所需要,只有重寫這個run方法,既然Thread類已經定義了執行緒任務的位置,只要在位置中定義任務程式碼即可。所以進行了重寫run方法動作。
1.5.多執行緒的記憶體圖解
多執行緒執行時,到底在記憶體中是如何執行的呢?
以上個程式為例,進行圖解說明:
多執行緒執行時,在棧記憶體中,其實每一個執行執行緒都有一片自己所屬的棧記憶體空間。進行方法的壓棧和彈棧。
當執行執行緒的任務結束了,執行緒自動在棧記憶體中釋放了。但是當所有的執行執行緒都結束了,那麼程式就結束了。
1.6.獲取執行緒名稱
開啟的執行緒都會有自己的獨立執行棧記憶體,那麼這些執行的執行緒的名字是什麼呢?該如何獲取呢?既然是執行緒的名字,按照物件導向的特點,是哪個物件的屬性和誰的功能,那麼我們就去找那個物件就可以了。查閱Thread類的API文件發現有個方法是獲取當前正在執行的執行緒物件。還有個方法是獲取當前執行緒物件的名稱。既然找到了,我們就可以試試。
Thread.currentThread()獲取當前執行緒物件
Thread.currentThread().getName();獲取當前執行緒物件的名稱
class Demo extends Thread //繼承Thread
{
String name;
Demo(String name)
{
this.name = name;
}
//複寫其中的run方法
public void run()
{
for (int i=1;i<=20 ;i++ )
{
System.out.println("name="+name+","+Thread.currentThread().getName()+",i="+i);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
//建立兩個執行緒任務
Demo d = new Demo("小強");
Demo d2 = new Demo("旺財");
d2.start();//開啟一個執行緒
d.run();//主執行緒在呼叫run方法
}
}
複製程式碼
原來主執行緒的名稱: main
自定義的執行緒: Thread-0 執行緒多個時,數字順延。Thread-1......
進行多執行緒程式設計時不要忘記了Java程式執行時從主執行緒開始,main方法的方法體就是主執行緒的執行緒執行體。
1.7.建立執行緒的第二種方式
掌握瞭如何建立執行緒物件,以及開啟執行緒後,記得在查閱API時,還說了有第二種開啟執行緒的方式,那麼第二種是什麼呢?
1.7.1.實現Runnable介面
繼續檢視API發現,建立執行緒的另一種方法是宣告實現 Runnable 介面的類。該類然後實現 run 方法。然後可以分配該類的例項,在建立 Thread 時作為一個引數來傳遞並啟動。
怎麼還要實現Runnable介面,Runable是啥玩意呢?繼續API搜尋。
檢視Runnable介面說明文件:Runnable 介面應該由那些打算通過某一執行緒執行其例項的類來實現。類必須定義一個稱為 run 的無引數方法。
總結:
建立執行緒的第二種方式:實現Runnable介面。
1、定義類實現Runnable介面。
2、覆蓋介面中的run方法。。
3、建立Thread類的物件
4、將Runnable介面的子類物件作為引數傳遞給Thread類的建構函式。
5、呼叫Thread類的start方法開啟執行緒。
程式碼演示:
class Demo implements Runnable
{
private String name;
Demo(String name)
{
this.name = name;
}
//覆蓋了介面Runnable中的run方法。
public void run()
{
for(int i=1; i<=20; i++)
{ System.out.println("name="+name+"..."+Thread.currentThread().getName()+"..."+i);
}
}
}
class ThreadDemo2
{
public static void main(String[] args)
{
//建立Runnable子類的物件。注意它並不是執行緒物件。
Demo d = new Demo("Demo");
//建立Thread類的物件,將Runnable介面的子類物件作為引數傳遞給Thread類的建構函式。
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
//將執行緒啟動。
t1.start();
t2.start();
System.out.println(Thread.currentThread().getName()+"----->");
System.out.println("Hello World!");
}
}
複製程式碼
輸出結果:
1.7.2.實現Runnable的原理
為什麼需要定一個類去實現Runnable介面呢?繼承Thread類和實現Runnable介面有啥區別呢?
實現Runnable介面,避免了繼承Thread類的單繼承侷限性
。覆蓋Runnable介面中的run方法,將執行緒任務程式碼定義到run方法中。建立Thread類的物件,只有建立Thread類的物件才可以建立執行緒。執行緒任務已被封裝到Runnable介面的run方法中,而這個run方法所屬於Runnable介面的子類物件,所以將這個子類物件作為引數傳遞給Thread的建構函式,這樣,執行緒物件建立時就可以明確要執行的執行緒的任務。
1.7.3.實現Runnable的好處
第二種方式實現Runnable介面避免了單繼承的侷限性,所以較為常用。實現Runnable介面的方式,更加的符合物件導向,執行緒分為兩部分,一部分執行緒物件,一部分執行緒任務。繼承Thread類,執行緒物件和執行緒任務耦合在一起。一旦建立Thread類的子類物件,既是執行緒物件,有又有執行緒任務。實現runnable介面,將執行緒任務單獨分離出來封裝成物件,型別就是Runnable介面型別。Runnable介面對執行緒物件和執行緒任務進行解耦。
1.8.執行緒狀態圖
查閱API關於IllegalThreadStateException這個異常說明資訊發現,這個異常的描述資訊為:指示執行緒沒有處於請求操作所要求的適當狀態時丟擲的異常。這裡面說適當的狀態,啥意思呢?難道是說執行緒還有狀態嗎?
1、新建(new):執行緒物件被建立後就進入了新建狀態。如:Thread thread = new Thread();
2、就緒狀態(Runnable):也被稱為“可執行狀態”。執行緒物件被建立後,其他執行緒呼叫了該物件的start()方法,從而啟動該執行緒。如:thread.start(); 處於就緒狀態的執行緒隨時可能被CPU排程執行。
3、執行狀態(Running):執行緒獲取CPU許可權進行執行。需要注意的是,執行緒只能從就緒狀態進入到執行狀態。
4、阻塞狀態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用許可權,暫時停止執行。直到執行緒進入就緒狀態,才有機會進入執行狀態。阻塞的三種情況:
- 等待阻塞:通過呼叫執行緒的wait()方法,讓執行緒等待某工作的完成。
- 同步阻塞:執行緒在獲取synchronized同步鎖失敗(因為鎖被其他執行緒佔用),它會進入同步阻塞狀態。
- 其他阻塞:通過呼叫執行緒的sleep()或join()或發出了I/O請求時,執行緒會進入到阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。
5、死亡狀態(Dead):執行緒執行完了或因異常退出了run()方法,該執行緒結束生命週期。
1.8.1.sleep,wait,yield,join的區別
-
sleep()方法
在指定時間內讓當前正在執行的執行緒暫停執行,但不會釋放“鎖標誌”。不推薦使用。 sleep()使當前執行緒進入阻塞狀態,在指定時間內不會執行。
-
wait()方法
在其他執行緒呼叫物件的notify或notifyAll方法前,導致當前執行緒等待。執行緒會釋放掉它所佔有的“鎖標誌”,從而使別的執行緒有機會搶佔該鎖。 當前執行緒必須擁有當前物件鎖。如果當前執行緒不是此鎖的擁有者,會丟擲IllegalMonitorStateException異常。 喚醒當前物件鎖的等待執行緒使用notify或notifyAll方法,也必須擁有相同的物件鎖,否則也會丟擲IllegalMonitorStateException異常。 waite()和notify()必須在synchronized函式或synchronized block中進行呼叫。如果在non-synchronized函式或non-synchronized block中進行呼叫,雖然能編譯通過,但在執行時會發生IllegalMonitorStateException的異常。
-
yield方法
暫停當前正在執行的執行緒物件。 yield()只是使當前執行緒重新回到可執行狀態,所以執行yield()的執行緒有可能在進入到可執行狀態後馬上又被執行。 yield()只能使同優先順序或更高優先順序的執行緒有執行的機會。 呼叫yield方法並不會讓執行緒進入阻塞狀態,而是讓執行緒重回就緒狀態,它只需要等待重新獲取CPU執行時間,這一點是和sleep方法不一樣的。
-
join方法
等待該執行緒終止。 等待呼叫join方法的執行緒結束,再繼續執行。如:t.join();//主要用於等待t執行緒執行結束,若無此句,main則會執行完畢,導致結果不可預測
1.9.執行緒的安全問題
帶女朋友看電影,需要買票,電影院要賣票,模擬電影院的買票操作
假設我們想要的電影是 “功夫熊貓3”,本次電影的座位共100個(本廠電影只能賣100張票)
模擬電影院的售票視窗,實現多個視窗同時賣 “功夫熊貓3”這場電影票(多個視窗一起賣這100張票)
需要視窗:採用執行緒物件
需要票:Runnable介面子類來模擬
public class ThreadDemo {
public static void main(String[] args) {
//建立票物件
Ticket ticket = new Ticket();
//建立3個視窗
Thread t1 = new Thread(ticket, "視窗1");
Thread t2 = new Thread(ticket, "視窗2");
Thread t3 = new Thread(ticket, "視窗3");
t1.start();
t2.start();
t3.start();
}
}
public class Ticket implements Runnable {
//共100票
int ticket = 100;
@Override
public void run() {
//模擬賣票
while(true){
//t1,t2,t3
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
}
複製程式碼
總結:上面程式出新了問題
票出現了重複的票
錯誤的票 0
1.10.同步的鎖
- 同步程式碼塊: 在程式碼塊宣告上 加上synchronized
synchronized (鎖物件) {
可能會產生執行緒安全問題的程式碼
}
複製程式碼
同步程式碼塊中的鎖物件可以是任意的物件,多個執行緒物件使用的是同一個鎖物件
把Ticket.java進行了程式碼修改
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//定義所物件
Object lock = new Object();
@Override
public void run() {
//模擬賣票
while(true){
//同步程式碼塊
synchronized (lock){
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
}
}
複製程式碼
當使用了同步程式碼塊後,上述的執行緒的安全問題,解決了。
- 同步方法:在方法宣告上加上synchronized
public synchronized void method(){
可能會產生執行緒安全問題的程式碼
}
複製程式碼
同步方法中的鎖物件是 this
//同步方法,鎖物件this
public synchronized void method(){
//this.name = name;
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
複製程式碼
- 靜態同步方法: 在方法宣告上加上synchronized
public static synchronized void method(){
可能會產生執行緒安全問題的程式碼
}
複製程式碼
1.11.死鎖
同步鎖的另一個弊端:當執行緒任務中出現了多個同步(多個鎖)時,如果同步中巢狀了其他的同步。這時容易引發一種現象:死鎖。這種情況能避免就避免掉。
synchronzied(A鎖){
synchronized(B鎖){
}
}
複製程式碼
/*
* 定義鎖物件
*/
public class MyLock {
public static final Object lockA = new Object();
public static final Object lockB = new Object();
}
/*
* 執行緒任務類
*/
public class ThreadTask implements Runnable {
int x = new Random().nextInt(1);//0,1
//指定執行緒要執行的任務程式碼
@Override
public void run() {
while(true){
if (x%2 ==0) {
//情況一
synchronized (MyLock.lockA) {
System.out.println("if-LockA");
synchronized (MyLock.lockB) {
System.out.println("if-LockB");
System.out.println("if大口吃肉");
}
}
} else {
//情況二
synchronized (MyLock.lockB) {
System.out.println("else-LockB");
synchronized (MyLock.lockA) {
System.out.println("else-LockA");
System.out.println("else大口吃肉");
}
}
}
x++;
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
//建立執行緒任務類物件
ThreadTask task = new ThreadTask();
//建立兩個執行緒
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
//啟動執行緒
t1.start();
t2.start();
}
}
複製程式碼
1.12.Lock介面
查閱API,發現Lock介面,比同步更厲害,有更多操作;
lock():獲取鎖
unlock():釋放鎖;
提供了一個更加面對物件的鎖,在該鎖中提供了更多的顯示的鎖操作。使用Lock介面,以及其中的lock()方法和unlock()方法替代同步。
如下程式碼演示:
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//建立Lock鎖物件
Lock ck = new ReentrantLock();
@Override
public void run() {
//模擬賣票
while(true){
//synchronized (lock){
ck.lock();
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
ck.unlock();
//}
}
}
}
複製程式碼
1.13.執行緒的匿名內部類的使用
方式1
new Thread() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()
+ "...X...." + x);
}
}
}.start();
方式2
Runnable r = new Runnable() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()
+ "...Y...." + x);
}
}
};
new Thread(r).start();
複製程式碼
2.多執行緒檔案上傳
實現伺服器端可以同時接收多個客戶端上傳的檔案。 我們要修改伺服器端程式碼
/*
* 檔案上傳 伺服器端
*
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
//1,建立伺服器,等待客戶端連線
ServerSocket serverSocket = new ServerSocket(6666);
//實現多個客戶端連線伺服器的操作
while(true){
final Socket clientSocket = serverSocket.accept();
//啟動執行緒,完成與當前客戶端的資料互動過程
new Thread(){
public void run() {
try{
//顯示哪個客戶端Socket連線上了伺服器
InetAddress ipObject = clientSocket.getInetAddress();//得到IP地址物件
String ip = ipObject.getHostAddress(); //得到IP地址字串
System.out.println("小樣,抓到你了,連線我!!" + "IP:" + ip);
//7,獲取Socket的輸入流
InputStream in = clientSocket.getInputStream();
//8,建立目的地的位元組輸出流 D:\\upload\\192.168.74.58(1).jpg
BufferedOutputStream fileOut = new BufferedOutputStream(new FileOutputStream("D:\\upload\\"+ip+"("+System.currentTimeMillis()+").jpg"));
//9,把Socket輸入流中的資料,寫入目的地的位元組輸出流中
byte[] buffer = new byte[1024];
int len = -1;
while((len = in.read(buffer)) != -1){
//寫入目的地的位元組輸出流中
fileOut.write(buffer, 0, len);
}
//-----------------反饋資訊---------------------
//10,獲取Socket的輸出流, 作用:寫反饋資訊給客戶端
OutputStream out = clientSocket.getOutputStream();
//11,寫反饋資訊給客戶端
out.write("圖片上傳成功".getBytes());
out.close();
fileOut.close();
in.close();
clientSocket.close();
} catch(IOException e){
e.printStackTrace();
}
};
}.start();
}
//serverSocket.close();
}
}
複製程式碼
3.總結
- 建立執行緒的方式
方式1,繼承Thread執行緒類
步驟
1, 自定義類繼承Thread類
2, 在自定義類中重寫Thread類的run方法
3, 建立自定義類物件(執行緒物件)
4, 呼叫start方法,啟動執行緒,通過JVM,呼叫執行緒中的run方法
方式2,實現Runnable介面
步驟
1, 建立執行緒任務類 實現Runnable介面
2, 線上程任務類中 重寫介面中的run方法
3, 建立執行緒任務類物件
4, 建立執行緒物件,把執行緒任務類物件作為Thread類構造方法的引數使用
5, 呼叫start方法,啟動執行緒,通過JVM,呼叫執行緒任務類中的run方法
- 同步鎖
多個執行緒想保證執行緒安全,必須要使用同一個鎖物件
A.同步程式碼塊
synchronized (鎖物件){
可能產生執行緒安全問題的程式碼
}
複製程式碼
同步程式碼塊的鎖物件可以是任意的物件
B.同步方法
public synchronized void method()
可能產生執行緒安全問題的程式碼
}
複製程式碼
同步方法中的鎖物件是 this
C.靜態同步方法
public synchronized static void method()
可能產生執行緒安全問題的程式碼
}
複製程式碼
靜態同步方法中的鎖物件是 類名.class
參考:
java程式設計思想
黑馬教學視訊
www.cnblogs.com/dz-boss/p/1… (個人部落格)