小白學習Java第九天,多線程的理解與相關應用

一、併發與並行

  • 併發:指兩個或多個事件在同一個時間段內發生。
  • 並行:指兩個或多個事件在同一時刻發生(同時發生)。
    在這裏插入圖片描述

在操作系統中,安裝了多個程序,併發指的是在一段時間內宏觀上有多個程序同時運行,這在單 CPU 系統中,每一時刻只能有一道程序執行,即微觀上這些程序是分時的交替運行,只不過是給人的感覺是同時運行,那是因爲分時交替運行的時間是非常短的。
而在多個 CPU 系統中,則這些可以併發執行的程序便可以分配到多個處理器上(CPU),實現多任務並行執行,即利用每個處理器來處理一個可以併發執行的程序,這樣多個程序便可以同時執行。目前電腦市場上說的多核 CPU,便是多核處理器,核 越多,並行處理的程序越多,能大大的提高電腦運行的效率。

注意:單核處理器的計算機肯定是不能並行的處理多個任務的,只能是多個任務在單個CPU上併發運行。同理,線程也是一樣的,從宏觀角度上理解線程是並行運行的,但是從微觀角度上分析卻是串行運行的,即一個線程一個線程的去運行,當系統只有一個CPU時,線程會以某種順序執行多個線程,我們把這種情況稱之爲線程調度。

二、線程與進程

進程:是指一個內存中運行的應用程序,每個進程都有一個獨立的內存空間,一個應用程序可以同時運行多個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;系統運行一個程序即是一個進程從創建、運行到消亡的過程。
線程:線程是進程中的一個執行單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應用程序也可以稱之爲多線程程序。
簡而言之:一個程序運行後至少有一個進程,一個進程中可以包含多個線程 。
在這裏插入圖片描述
在這裏插入圖片描述
線程調度:

  • 分時調度:所有線程輪流使用 CPU 的使用權,平均分配每個線程佔用 CPU 的時間。
  • 搶佔式調度: 優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那麼會隨機選擇一個(線程隨機性),Java使用的爲搶佔式調度。

搶佔式調度詳解:
大部分操作系統都支持多進程併發運行,現在的操作系統幾乎都支持同時運行多個程序。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟件,同時還開着畫圖板,dos窗口等軟件。此時,這些程序是在同時運行,”感覺這些軟件好像在同一時刻運行着“。
實際上,CPU(中央處理器)使用搶佔式調度模式在多個線程間進行着高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運行。其實,多線程程序並不能提高程序的運行速度,但能夠提高程序運行效率,讓CPU的使用率更高。

三、創建線程的兩種方式

1、、Java使用java.lang.Thread類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是完成一定的任務,實際上就是執行一段程序流即一段順序執行的代碼。Java使用線程執行體來代表這段程序流。Java中通過繼承Thread類來創建啓動多線程的步驟如下:
1)、定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務,因此把run()方法稱爲線程執行體。
2)、 創建Thread子類的實例,即創建了線程對象
3)、 調用線程對象的start()方法來啓動該線程

public class Demo01 {
	public static void main(String[] args) {
		//創建自定義線程對象
		MyThread mt = new MyThread("新的線程!");
		//開啓新線程
		mt.start();
		//在主方法中執行for循環
		for (int i = 0; i < 10; i++) {
			System.out.println("main線程!"+i);
		}
	}
}
public class MyThread extends Thread {
	//定義指定線程名稱的構造方法
	public MyThread(String name) {
		//調用父類的String參數的構造方法,指定線程的名稱
		super(name);
	}
	/**
	 * 重寫run方法,完成該線程執行的邏輯
	 */
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在執行!"+i);
		}
	}
}

2、採用 java.lang.Runnable 也是非常常見的一種,我們只需要重寫run方法即可。
步驟如下:
1)、定義Runnable接口的實現類,並重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
2)、創建Runnable實現類的實例,並以此實例作爲Thread的target來創建Thread對象,該Thread對象纔是真正 的線程對象。
3)、調用線程對象的start()方法來啓動線程。

public class MyRunnable implements Runnable{
	 @Override 
	 public void run() { 
	 	for (int i = 0; i < 20; i++) { 		 
	 	   System.out.println(Thread.currentThread().getName()+" "+i); 
	 	   }
	 } 
 }
 public class Demo { 
 public static void main(String[] args) { 
 //創建自定義類對象 線程任務對象 
	 MyRunnable mr = new MyRunnable(); 
 //創建線程對象 
 	 Thread t = new Thread(mr, "小強"); 
 	 t.start(); 
 	 for (int i = 0; i < 20; i++) { 
 	 	System.out.println("旺財 " + i); 
 	 	} 
 	} 
}

通過實現Runnable接口,使得該類有了多線程類的特徵。run()方法是多線程程序的一個執行目標。所有的多線程 代碼都在run方法裏面。Thread類實際上也是實現了Runnable接口的類。
在啓動的多線程的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出對象,然後調用Thread 對象的start()方法來運行多線程代碼。
實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的。因此,不管是繼承Thread類還是實現 Runnable接口來實現多線程,最終還是通過Thread的對象的API來控制線程的,熟悉Thread類的API是進行多線程 編程的基礎。
3、 Thread和Runnable的區別
實現Runnable接口創建多線程程序的好處:
1)避免了單繼承的侷限性
一個類只能繼承一個類(一個人只能有一個親爹),類繼承了Thread類就不能繼承其他的類。
實現了Runnable接口,還可以繼承其他的類,實現其他的接口。
2)增強了程序的擴展性,降低了程序的耦合性(解耦)。
實現Runnable接口的方式,把設置線程任務和開啓新線程進行了分離(解耦),傳遞不同的實現類,實現不同的內容。
3)實現類中,重寫了run方法:用來設置線程任務,創建Thread類對象,調用start方法:用來開啓新線程。

四、匿名內部類方式實現線程的創建

使用線程的內匿名內部類方式,可以方便的實現每個線程執行不同的線程任務操作。 使用匿名內部類的方式實現Runnable接口,重新Runnable接口中的run方法:
匿名內部類方式實現線程的創建

匿名:沒有名字
內部類:寫在其他類內部的類

匿名內部類作用:簡化代碼
    把子類繼承父類,重寫父類的方法,創建子類對象合一步完成
    把實現類實現類接口,重寫接口中的方法,創建實現類對象合成一步完成
匿名內部類的最終產物:子類/實現類對象,而這個類沒有名字

格式:
    new 父類/接口(){
        重複父類/接口中的方法
    };
public class Demo01InnerClassThread {
    public static void main(String[] args) {
        //線程的父類是Thread
        // new MyThread().start();
        new Thread(){
            //重寫run方法,設置線程任務
            @Override
            public void run() {
                for (int i = 0; i <20 ; i++) {
                    System.out.println(Thread.currentThread().getName()+"-->"+"1");
                }
            }
        }.start();

        //線程的接口Runnable
        //Runnable r = new RunnableImpl();//多態
        Runnable r = new Runnable(){
            //重寫run方法,設置線程任務
            @Override
            public void run() {
                for (int i = 0; i <20 ; i++) {
                    System.out.println(Thread.currentThread().getName()+"-->"+"程序員");
                }
            }
        };
        new Thread(r).start();

        //簡化接口的方式
        new Thread(new Runnable(){
            //重寫run方法,設置線程任務
            @Override
            public void run() {
                for (int i = 0; i <20 ; i++) {
                    System.out.println(Thread.currentThread().getName()+"-->"+"2");
                }
            }
        }).start();
    }

五、線程安全

1、如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
我們通過一個案例,演示線程的安全問題: 電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “葫蘆娃大戰奧特曼”,本次電影的座位共100個 (本場電影只能賣100張票)。
在這裏插入圖片描述

public class RunnableImpl implements Runnable{
    //定義一個多個線程共享的票源
    private  int ticket = 100;
    //設置線程任務:賣票
    @Override
    public void run() {
        //使用死循環,讓賣票操作重複執行
        while(true){
            //先判斷票是否存在
            if(ticket>0){
                //提高安全問題出現的概率,讓程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,賣票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                ticket--;
            }
        }
    }
}
public class DemoTicket {
    public static void main(String[] args) {
        //創建Runnable接口的實現類對象
        RunnableImpl run = new RunnableImpl();
        //創建Thread類對象,構造方法中傳遞Runnable接口的實現類對象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //調用start方法開啓多線程
        t0.start();
        t1.start();
        t2.start();
    }
}

發現程序出現了兩個問題:
1)相同的票數,比如5這張票被賣了兩回。
2)不存在的票,比如0票與-1票,是不存在的。
這種問題,幾個窗口(線程)票數不同步了,這種問題稱爲線程不安全
2、線程同步
有三種方式完成同步操作:
1)同步代碼塊。
2)同步方法。
3)鎖機制。
同步代碼塊: synchronized 關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。
格式:

  synchronized(鎖對象){
        可能會出現線程安全問題的代碼(訪問了共享數據的代碼)
    }
注意:
    1.通過代碼塊中的鎖對象,可以使用任意的對象
    2.但是必須保證多個線程使用的鎖對象是同一個
    3.鎖對象作用:
        把同步代碼塊鎖住,只讓一個線程在同步代碼塊中執行
public class RunnableImpl implements Runnable{
    //定義一個多個線程共享的票源
    private  int ticket = 100;

    //創建一個鎖對象
    Object obj = new Object();

    //設置線程任務:賣票
    @Override
    public void run() {
        //使用死循環,讓賣票操作重複執行
        while(true){
           //同步代碼塊
            synchronized (obj){
                //先判斷票是否存在
                if(ticket>0){
                    //提高安全問題出現的概率,讓程序睡眠
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //票存在,賣票 ticket--
                    System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                    ticket--;
                }
            }
        }
    }
}

同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法外等着。
使用步驟:
1.把訪問了共享數據的代碼抽取出來,放到一個方法中
2.在方法上添加synchronized修飾符
格式:定義方法的格式

  修飾符 synchronized 返回值類型 方法名(參數列表){
        可能會出現線程安全問題的代碼(訪問了共享數據的代碼)
public class RunnableImpl implements Runnable{
    //定義一個多個線程共享的票源
    private static int ticket = 100;
    //設置線程任務:賣票
    @Override
    public void run() {        System.out.println("this:"+this);//this:com.itheima.demo08.Synchronized.RunnableImpl@58ceff1
        //使用死循環,讓賣票操作重複執行
        while(true){
            payTicketStatic();
        }
    }

    /*
        靜態的同步方法
        鎖對象是誰?
        不能是this
        this是創建對象之後產生的,靜態方法優先於對象
        靜態方法的鎖對象是本類的class屬性-->class文件對象(反射)
     */
    public static /*synchronized*/ void payTicketStatic(){
        synchronized (RunnableImpl.class){
            //先判斷票是否存在
            if(ticket>0){
                //提高安全問題出現的概率,讓程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //票存在,賣票 ticket--             System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                ticket--;
            }
        }

    }

    /*
        定義一個同步方法
        同步方法也會把方法內部的代碼鎖住
        只讓一個線程執行
        同步方法的鎖對象是誰?
        就是實現類對象 new RunnableImpl()
        也是就是this
     */
    public /*synchronized*/ void payTicket(){
        synchronized (this){
            //先判斷票是否存在
            if(ticket>0){
                //提高安全問題出現的概率,讓程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,賣票 ticket--                System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                ticket--;
            }
        }

    }
}

解決線程安全問題的三種方案:使用Lock鎖
java.util.concurrent.locks.Lock接口
Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。
Lock接口中的方法:
void lock()獲取鎖。
void unlock() 釋放鎖。
java.util.concurrent.locks.ReentrantLock implements Lock接口

使用步驟:
    1.在成員位置創建一個ReentrantLock對象
    2.在可能會出現安全問題的代碼前調用Lock接口中的方法lock獲取鎖
    3.在可能會出現安全問題的代碼後調用Lock接口中的方法unlock釋放鎖
public class RunnableImpl implements Runnable{
    //定義一個多個線程共享的票源
    private  int ticket = 100;

    //1.在成員位置創建一個ReentrantLock對象
    Lock l = new ReentrantLock();
 @Override
    public void run() {
        //使用死循環,讓賣票操作重複執行
        while(true){
           //2.在可能會出現安全問題的代碼前調用Lock接口中的方法lock獲取鎖
           l.lock();
            //先判斷票是否存在
            if(ticket>0){
                //提高安全問題出現的概率,讓程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,賣票 ticket--
          System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                ticket--;
            }
            //3.在可能會出現安全問題的代碼後調用Lock接口中的方法unlock釋放鎖
            l.unlock();
        }
    }
}

線程狀態圖解:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章