Java進階篇-多線程

概述:

1,什麼是進程?什麼是線程?
	進程是一個應用程序(1個進程是一個軟件)
	線程是一個進程中的執行場景/執行單元。
	一個進程可以啓動多個線程
2,對於Java程序來說,當在DOS命令窗口中輸入:
	java HelloWorld 回車之後
	會先啓動JVM,而JVM則是一個進程
	JVM再啓動一個主線程調用main方法。
	同時再啓動一個垃圾回收線程負責看護,回收垃圾。
	最起碼,現在的java程序中至少有兩個線程併發。
	一個是垃圾回收線程,一個是執行main方法的主線程
3,進程與線程關係:
	進程A和進程B的內存獨立不共享
	線程A和線程B:
		在Java中線程A和線程B,堆內存和方法區內存共享。
		但是棧內存獨立,一個線程一個棧
		假設啓動10個線程,會有10個棧空間,每個棧之間互不干擾,各自執行各自的,這就是多線程併發。
		Java中之所以有多線程機制,目的就是爲了提高程序的處理效率。
4,單核處理器無法完全做到多線程併發,表面上看似爲多線程,實則爲線程交替運行。
5、關於線程對象的生命週期?
	新建狀態
	就緒狀態
	運行狀態
	阻塞狀態
	死亡狀態

Java語言實現線程的兩種方式:

直接繼承Thread類

第一種方式:編寫一個類,直接繼承java.lang.Thread,重寫run方法。
    怎麼創建線程對象? new就行了。
   怎麼啓動線程呢? 調用線程對象的start()方法。

注意:
    亙古不變的道理:
        方法體當中的代碼永遠都是自上而下的順序依次逐行執行的。
		// 定義線程類
		public class MyThread extends Thread{
			public void run(){
		
		}
	}
	// 創建線程對象
	MyThread t = new MyThread();
	// 啓動線程。
	t.start();
代碼演示:
public class ThreadTest02 {
    public static void main(String[] args) {
        // 這裏是main方法,這裏的代碼屬於主線程,在主棧中運行。
        // 新建一個分支線程對象
        MyThread t = new MyThread();
        // 啓動線程
        //t.run(); // 不會啓動線程,不會分配新的分支棧。(這種方式就是單線程。)
        // start()方法的作用是:啓動一個分支線程,在JVM中開闢一個新的棧空間,這段代碼任務完成之後,瞬間就結束了。
        // 這段代碼的任務只是爲了開啓一個新的棧空間,只要新的棧空間開出來,start()方法就結束了。線程就啓動成功了。
        // 啓動成功的線程會自動調用run方法,並且run方法在分支棧的棧底部(壓棧)。
        // run方法在分支棧的棧底部,main方法在主棧的棧底部。run和main是平級的。
        t.start();
        // 這裏的代碼還是運行在主線程中。
        for(int i = 0; i < 1000; i++){
            System.out.println("主線程--->" + i);
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        // 編寫程序,這段程序運行在分支線程中(分支棧)。
        for(int i = 0; i < 1000; i++){
            System.out.println("分支線程--->" + i);
        }
    }
}

Thread常用方法:

1、怎麼獲取當前線程對象?
    Thread t = Thread.currentThread();
    返回值t就是當前線程。	
2、獲取線程對象的名字
    String name = 線程對象.getName();	
3、修改線程對象的名字
    線程對象.setName("線程名字");	
4、當線程沒有設置名字的時候,默認的名字有什麼規律?(瞭解一下)
    Thread-0
    Thread-1
    Thread-2
    Thread-3
    .....
代碼演示:
public class ThreadTest05 {
    public void doSome(){
        // 這樣就不行了
        //this.getName();
        //super.getName();
        // 但是這樣可以
        String name = Thread.currentThread().getName();
        System.out.println("------->" + name);
    }

    public static void main(String[] args) {
        ThreadTest05 tt = new ThreadTest05();
        tt.doSome();

        //currentThread就是當前線程對象
        // 這個代碼出現在main方法當中,所以當前線程就是主線程。
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName()); //main

        // 創建線程對象
        MyThread2 t = new MyThread2();
        // 設置線程的名字
        t.setName("t1");
        // 獲取線程的名字
        String tName = t.getName();
        System.out.println(tName); //Thread-0

        MyThread2 t2 = new MyThread2();
        t2.setName("t2");
        System.out.println(t2.getName()); //Thread-1\
        t2.start();

        // 啓動線程
        t.start();
    }
}

class MyThread2 extends Thread {
    public void run(){
        for(int i = 0; i < 100; i++){
            // currentThread就是當前線程對象。當前線程是誰呢?
            // 當t1線程執行run方法,那麼這個當前線程就是t1
            // 當t2線程執行run方法,那麼這個當前線程就是t2
            Thread currentThread = Thread.currentThread();
            System.out.println(currentThread.getName() + "-->" + i);

            //System.out.println(super.getName() + "-->" + i);
            //System.out.println(this.getName() + "-->" + i);
        }
    }
}

實現Runnable接口

第二種方式:編寫一個類,實現java.lang.Runnable接口,實現run方法。
	// 定義一個可運行的類
	public class MyRunnable implements Runnable {
		public void run(){
		
		}
	}
	// 創建線程對象
	Thread t = new Thread(new MyRunnable());
	// 啓動線程
	t.start();

注意:第二種方式實現接口比較常用,因爲一個類實現了接口,它還可以去繼承
其它的類,更靈活。
代碼演示:
public class ThreadTest03 {
    public static void main(String[] args) {
        // 創建一個可運行的對象
        //MyRunnable r = new MyRunnable();
        // 將可運行的對象封裝成一個線程對象
        //Thread t = new Thread(r);
        Thread t = new Thread(new MyRunnable()); // 合併代碼
        // 啓動線程
        t.start();

        for(int i = 0; i < 100; i++){
            System.out.println("主線程--->" + i);
        }
    }
}

// 這並不是一個線程類,是一個可運行的類。它還不是一個線程。
class MyRunnable implements Runnable {

    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            System.out.println("分支線程--->" + i);
        }
    }
}

代碼演示(採用匿名內部類):
public class ThreadTest04 {
    public static void main(String[] args) {
        // 創建線程對象,採用匿名內部類方式。
        // 這是通過一個沒有名字的類,new出來的對象。
        Thread t = new Thread(new Runnable(){
            @Override
            public void run() {
                for(int i = 0; i < 100; i++){
                    System.out.println("t線程---> " + i);
                }
            }
        });

        // 啓動線程
        t.start();

        for(int i = 0; i < 100; i++){
            System.out.println("main線程---> " + i);
        }
    }
}

Sleep方法

關於線程的sleep方法:
    static void sleep(long millis)
    1、靜態方法:Thread.sleep(1000);
    2、參數是毫秒
    3、作用:讓當前線程進入休眠,進入“阻塞狀態”,放棄佔有CPU時間片,讓給其它線程使用。
        這行代碼出現在A線程中,A線程就會進入休眠。
        這行代碼出現在B線程中,B線程就會進入休眠。
    4、Thread.sleep()方法,可以做到這種效果:
        間隔特定的時間,去執行一段特定的代碼,每隔多久執行一次。
代碼演示:
public class ThreadTest06 {
    public static void main(String[] args) {

        // 讓當前線程進入休眠,睡眠5秒
        // 當前線程是主線程!!!
        /*try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/

        // 5秒之後執行這裏的代碼
        //System.out.println("hello world!");

        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);

            // 睡眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
代碼演示(Sleep面試題):
/*
關於Thread.sleep()方法的一個面試題:
 */
public class ThreadTest07 {
    public static void main(String[] args) {
        // 創建線程對象
        Thread t = new MyThread3();
        t.setName("t");
        t.start();

        // 調用sleep方法
        try {
            // 問題:這行代碼會讓線程t進入休眠狀態嗎?
            t.sleep(1000 * 5); // 在執行的時候還是會轉換成:Thread.sleep(1000 * 5);
                                     // 這行代碼的作用是:讓當前線程進入休眠,也就是說main線程進入休眠。
                                     // 這樣代碼出現在main方法中,main線程睡眠。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 5秒之後這裏纔會執行。
        System.out.println("hello World!");
    }
}

class MyThread3 extends Thread {
    public void run(){
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

終止線程Sleep睡眠

終斷t線程的睡眠(這種終斷睡眠的方式依靠了java的異常處理機制。)
        t.interrupt(); // 干擾,一盆冷水過去!
重點:run()當中的異常不能throws,只能try catch
代碼演示:
/*
sleep睡眠太久了,如果希望半道上醒來,你應該怎麼辦?也就是說怎麼叫醒一個正在睡眠的線程??
    注意:這個不是終斷線程的執行,是終止線程的睡眠。
 */
public class ThreadTest08 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable2());
        t.setName("t");
        t.start();

        // 希望5秒之後,t線程醒來(5秒之後主線程手裏的活兒幹完了。)
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 終斷t線程的睡眠(這種終斷睡眠的方式依靠了java的異常處理機制。)
        t.interrupt(); // 干擾,一盆冷水過去!
    }
}

class MyRunnable2 implements Runnable {

    // 重點:run()當中的異常不能throws,只能try catch
    // 因爲run()方法在父類中沒有拋出任何異常,子類不能比父類拋出更多的異常。
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "---> begin");
        try {
            // 睡眠1年
            Thread.sleep(1000 * 60 * 60 * 24 * 365);
        } catch (InterruptedException e) {
            // 打印異常信息
            //e.printStackTrace();
        }
        //1年之後纔會執行這裏
        System.out.println(Thread.currentThread().getName() + "---> end");

        // 調用doOther
        //doOther();
    }

    // 其它方法可以throws
    /*public void doOther() throws Exception{

    }*/
}

合理的終止一個線程的執行

代碼演示:
/*
怎麼合理的終止一個線程的執行。這種方式是很常用的。
 */
public class ThreadTest10 {
    public static void main(String[] args) {
        MyRunable4 r = new MyRunable4();
        Thread t = new Thread(r);
        t.setName("t");
        t.start();

        // 模擬5秒
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 終止線程
        // 你想要什麼時候終止t的執行,那麼你把標記修改爲false,就結束了。
        r.run = false;
    }
}

class MyRunable4 implements Runnable {

    // 打一個布爾標記
    boolean run = true;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++){
            if(run){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{
                // return就結束了,你在結束之前還有什麼沒保存的。
                // 在這裏可以保存呀。
                //save....

                //終止當前線程
                return;
            }
        }
    }
}

線程的調度

關於線程的調度

1.1、常見的線程調度模型有哪些?

	搶佔式調度模型:
		那個線程的優先級比較高,搶到的CPU時間片的概率就高一些/多一些。
		java採用的就是搶佔式調度模型。

	均分式調度模型:
		平均分配CPU時間片。每個線程佔有的CPU時間片時間長度一樣。
		平均分配,一切平等。
		有一些編程語言,線程調度模型採用的是這種方式。

1.2、java中提供了哪些方法是和線程調度有關係的呢?

	實例方法:
		void setPriority(int newPriority) 設置線程的優先級
		int getPriority() 獲取線程優先級
		最低優先級1
		默認優先級是5
		最高優先級10
		優先級比較高的獲取CPU時間片可能會多一些。(但也不完全是,大概率是多的。)
	
	靜態方法:
		static void yield()  讓位方法
		暫停當前正在執行的線程對象,並執行其他線程
		yield()方法不是阻塞方法。讓當前線程讓位,讓給其它線程使用。
		yield()方法的執行會讓當前線程從“運行狀態”回到“就緒狀態”。
		注意:在回到就緒之後,有可能還會再次搶到。
	
	實例方法:
		void join()  
		合併線程
		class MyThread1 extends Thread {
			public void doSome(){
				MyThread2 t = new MyThread2();
				t.join(); // 當前線程進入阻塞,t線程執行,直到t線程結束。當前線程纔可以繼續。
			}
		}

		class MyThread2 extends Thread{
			
		}

線程優先級

實例方法:
	void setPriority(int newPriority) 設置線程的優先級
	int getPriority() 獲取線程優先級
	最低優先級1
	默認優先級是5
	最高優先級10
	優先級比較高的獲取CPU時間片可能會多一些。(但也不完全是,大概率是多的。)
	最高優先級(10):Thread.MAX_PRIORITY
	最低優先級(1):Thread.MIN_PRIORITY
	默認優先級(5):Thread.NORM_PRIORITY
代碼演示:
/*
瞭解:關於線程的優先級
 */
public class ThreadTest11 {
    public static void main(String[] args) {
        // 設置主線程的優先級爲1
        Thread.currentThread().setPriority(1);
        // 獲取當前線程對象,獲取當前線程的優先級
        Thread currentThread = Thread.currentThread();
        // main線程的默認優先級是:5
        //System.out.println(currentThread.getName() + "線程的默認優先級是:" + currentThread.getPriority());

        Thread t = new Thread(new MyRunnable5());
        t.setPriority(10);
        t.setName("t");
        t.start();

        // 優先級較高的,只是搶到的CPU時間片相對多一些。
        // 大概率方向更偏向於優先級比較高的。
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

class MyRunnable5 implements Runnable {

    @Override
    public void run() {
        // 獲取線程優先級
        //System.out.println(Thread.currentThread().getName() + "線程的默認優先級:" + Thread.currentThread().getPriority());
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

讓位:Thread.yield()

靜態方法:
	static void yield()  讓位方法
	暫停當前正在執行的線程對象,並執行其他線程
	yield()方法不是阻塞方法。讓當前線程讓位,讓給其它線程使用。
	yield()方法的執行會讓當前線程從“運行狀態”回到“就緒狀態”。
	注意:在回到就緒之後,有可能還會再次搶到。
代碼演示:
public class ThreadTest12 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable6());
        t.setName("t");
        t.start();

        for(int i = 1; i <= 10000; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

class MyRunnable6 implements Runnable {

    @Override
    public void run() {
        for(int i = 1; i <= 10000; i++) {
            //每100個讓位一次。
            if(i % 100 == 0){
                Thread.yield(); // 當前線程暫停一下,讓給主線程。
            }
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

線程合併:Join

實例方法:
	合併線程:void join()  
	class MyThread1 extends Thread {
		public void doSome(){
			MyThread2 t = new MyThread2();
			t.join(); // 當前線程進入阻塞,t線程執行,直到t線程結束。當前線程纔可以繼續。
		}
	}	
	class MyThread2 extends Thread{					
	}
代碼演示:
public class ThreadTest13 {
    public static void main(String[] args) {
        System.out.println("main begin");

        Thread t = new Thread(new MyRunnable7());
        t.setName("t");
        t.start();

        //合併線程
        try {
            t.join(); // t合併到當前線程中,當前線程受阻塞,t線程執行直到結束。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("main over");
    }
}

class MyRunnable7 implements Runnable {

    @Override
    public void run() {
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

死鎖

死鎖代碼要會寫。
一般面試官要求你會寫。
只有會寫的,纔會在以後的開發中注意這個事兒。
因爲死鎖很難調試。
代碼演示:
public class DeadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        // t1和t2兩個線程共享o1,o2
        Thread t1 = new MyThread1(o1,o2);
        Thread t2 = new MyThread2(o1,o2);

        t1.start();
        t2.start();
    }
}

class MyThread1 extends Thread{
    Object o1;
    Object o2;
    public MyThread1(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o1){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2){

            }
        }
    }
}

class MyThread2 extends Thread {
    Object o1;
    Object o2;
    public MyThread2(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){

            }
        }
    }
}

線程安全:

2、關於多線程併發環境下,數據的安全問題。

2.1、爲什麼這個是重點?
	以後在開發中,我們的項目都是運行在服務器當中,
	而服務器已經將線程的定義,線程對象的創建,線程
	的啓動等,都已經實現完了。這些代碼我們都不需要
	編寫。

	最重要的是:你要知道,你編寫的程序需要放到一個
	多線程的環境下運行,你更需要關注的是這些數據
	在多線程併發的環境下是否是安全的。(重點:*****)

2.2、什麼時候數據在多線程併發的環境下會存在安全問題呢?
	三個條件:
		條件1:多線程併發。
		條件2:有共享數據。
		條件3:共享數據有修改的行爲。

	滿足以上3個條件之後,就會存在線程安全問題。

2.3、怎麼解決線程安全問題呢?
	當多線程併發的環境下,有共享數據,並且這個數據還會被修改,此時就存在
	線程安全問題,怎麼解決這個問題?
		線程排隊執行。(不能併發)。
		用排隊執行解決線程安全問題。
		這種機制被稱爲:線程同步機制。

		專業術語叫做:線程同步,實際上就是線程不能併發了,線程必須排隊執行。
	
	怎麼解決線程安全問題呀?
		使用“線程同步機制”。
	
	線程同步就是線程排隊了,線程排隊了就會犧牲一部分效率,沒辦法,數據安全
	第一位,只有數據安全了,我們纔可以談效率。數據不安全,沒有效率的事兒。

2.4、說到線程同步這塊,涉及到這兩個專業術語:

	異步編程模型:
		線程t1和線程t2,各自執行各自的,t1不管t2,t2不管t1,
		誰也不需要等誰,這種編程模型叫做:異步編程模型。
		其實就是:多線程併發(效率較高。)

		異步就是併發。

	同步編程模型:
		線程t1和線程t2,在線程t1執行的時候,必須等待t2線程執行
		結束,或者說在t2線程執行的時候,必須等待t1線程執行結束,
		兩個線程之間發生了等待關係,這就是同步編程模型。
		效率較低。線程排隊執行。

		同步就是排隊。
3、Java中有三大變量?【重要的內容。】

實例變量:在堆中。
靜態變量:在方法區。
局部變量:在棧中。

以上三大變量中:
	局部變量永遠都不會存在線程安全問題。
	因爲局部變量不共享。(一個線程一個棧。)
	局部變量在棧中。所以局部變量永遠都不會共享。

	實例變量在堆中,堆只有1個。
	靜態變量在方法區中,方法區只有1個。
	堆和方法區都是多線程共享的,所以可能存在線程安全問題。

	局部變量+常量:不會有線程安全問題。
	成員變量:可能會有線程安全問題。
4、如果使用局部變量的話:
	建議使用:StringBuilder。
	因爲局部變量不存在線程安全問題。選擇StringBuilder。
	StringBuffer效率比較低。

	ArrayList是非線程安全的。
	Vector是線程安全的。
	HashMap HashSet是非線程安全的。
	Hashtable是線程安全的。

線程同步代碼塊

線程同步機制的語法是:
     synchronized(){
         // 線程同步代碼塊。
     }
     synchronized後面小括號中傳的這個“數據”是相當關鍵的。
     這個數據必須是多線程共享的數據。才能達到多線程排隊。
     ()中寫什麼?
         那要看你想讓哪些線程同步。
         假設t1、t2、t3、t4、t5,有5個線程,	
         你只希望t1 t2 t3排隊,t4 t5不需要排隊。怎麼辦?
         你一定要在()中寫一個t1 t2 t3共享的對象。而這個
         對象對於t4 t5來說不是共享的。
  在java語言中,任何一個對象都有“一把鎖”,其實這把鎖就是標記。(只是把它叫做鎖。)
      100個對象,100把鎖。1個對象1把鎖。

synchronized的三種寫法

synchronized有三種寫法:
第一種:同步代碼塊
	靈活
	synchronized(線程共享對象){
		同步代碼塊;
	}
第二種:在實例方法上使用synchronized
	 public synchronized void withdraw(double money){ 
		}
	表示共享對象一定是this
	並且同步代碼塊是整個方法體。	
		缺點:	
		     synchronized出現在實例方法上,一定鎖的是this。
		     沒得挑。只能是this。不能是其他的對象了。
	     	所以這種方式不靈活。
	     	synchronized出現在實例方法上,表示整個方法體都需要同步,
	     	可能會無故擴大同步的範圍,導致程序的執行效率降低。
	     	所以這種方式不常用。
	    優點:
	       代碼寫的少了。節儉了。
	   	建議:
	 		  如果共享的對象就是this,並且需要同步的代碼塊是整個方法體, 建議使用這種方式。
    
第三種:在靜態方法上使用synchronized
	表示找類鎖。
	類鎖永遠只有1把。
	就算創建了100個對象,那類鎖也只有一把。

對象鎖:1個對象1把鎖,100個對象100把鎖。
類鎖:100個對象,也可能只是1把類鎖。
代碼演示:

銀行賬戶

/*
銀行賬戶
    使用線程同步機制,解決線程安全問題。
 */
public class Account {
    // 賬號
    private String actno;
    // 餘額
    private double balance; //實例變量。

    //對象
    Object obj = new Object(); // 實例變量。(Account對象是多線程共享的,Account對象中的實例變量obj也是共享的。)

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款的方法
    public void withdraw(double money){

        //int i = 100;
        //i = 101;

        // 以下這幾行代碼必須是線程排隊的,不能併發。
        // 一個線程把這裏的代碼全部執行結束之後,另一個線程才能進來。
        /*        
            這裏的共享對象是:賬戶對象。
            賬戶對象是共享的,那麼this就是賬戶對象吧!!!
            不一定是this,這裏只要是多線程共享的那個對象就行。
            以下代碼的執行原理?
                1、假設t1和t2線程併發,開始執行以下代碼的時候,肯定有一個先一個後。
                2、假設t1先執行了,遇到了synchronized,這個時候自動找“後面共享對象”的對象鎖,
                找到之後,並佔有這把鎖,然後執行同步代碼塊中的程序,在程序執行過程中一直都是
                佔有這把鎖的。直到同步代碼塊代碼結束,這把鎖纔會釋放。
                3、假設t1已經佔有這把鎖,此時t2也遇到synchronized關鍵字,也會去佔有後面
                共享對象的這把鎖,結果這把鎖被t1佔有,t2只能在同步代碼塊外面等待t1的結束,
                直到t1把同步代碼塊執行結束了,t1會歸還這把鎖,此時t2終於等到這把鎖,然後
                t2佔有這把鎖之後,進入同步代碼塊執行程序。

                這樣就達到了線程排隊執行。
                這裏需要注意的是:這個共享對象一定要選好了。這個共享對象一定是你需要排隊
                執行的這些線程對象所共享的。
         */
        //Object obj2 = new Object();
        //synchronized (this){
        //synchronized (obj) {
        //synchronized ("abc") { // "abc"在字符串常量池當中。
        //synchronized (null) { // 報錯:空指針。
        //synchronized (obj2) { // 這樣編寫就不安全了。因爲obj2不是共享對象。
            double before = this.getBalance();
            double after = before - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
        //}
    }
}

線程類

public class AccountThread extends Thread {

    // 兩個線程必須共享同一個賬戶對象。
    private Account act;

    // 通過構造方法傳遞過來賬戶對象
    public AccountThread(Account act) {
        this.act = act;
    }

    public void run(){
        // run方法的執行表示取款操作。
        // 假設取款5000
        double money = 5000;
        // 取款
        // 多線程併發執行這個方法。
        //synchronized (this) { //這裏的this是AccountThread對象,這個對象不共享!
        synchronized (act) { // 這種方式也可以,只不過擴大了同步的範圍,效率更低了。
            act.withdraw(money);
        }

        System.out.println(Thread.currentThread().getName() + "對"+act.getActno()+"取款"+money+"成功,餘額" + act.getBalance());
    }
}

主程序main測試

public class Test {
    public static void main(String[] args) {
        // 創建賬戶對象(只創建1個)
        Account act = new Account("act-001", 10000);
        // 創建兩個線程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);

        // 設置name
        t1.setName("t1");
        t2.setName("t2");
        // 啓動線程取款
        t1.start();
        t2.start();
    }
}

開發中應該怎麼解決線程安全問題?

是一上來就選擇線程同步嗎?synchronized
	不是,synchronized會讓程序的執行效率降低,用戶體驗不好。
	系統的用戶吞吐量降低。用戶體驗差。在不得已的情況下再選擇
	線程同步機制。

第一種方案:儘量使用局部變量代替“實例變量和靜態變量”。

第二種方案:如果必須是實例變量,那麼可以考慮創建多個對象,這樣
實例變量的內存就不共享了。(一個線程對應1個對象,100個線程對應100個對象,
對象不共享,就沒有數據安全問題了。)

第三種方案:如果不能使用局部變量,對象也不能創建多個,這個時候
就只能選擇synchronized了。線程同步機制。

守護線程

	java語言中線程分爲兩大類:
		一類是:用戶線程
		一類是:守護線程(後臺線程)
		其中具有代表性的就是:垃圾回收線程(守護線程)。

	守護線程的特點:
		一般守護線程是一個死循環,所有的用戶線程只要結束,
		守護線程自動結束。
	
	注意:主線程main方法是一個用戶線程。

	守護線程用在什麼地方呢?
		每天00:00的時候系統數據自動備份。
		這個需要使用到定時器,並且我們可以將定時器設置爲守護線程。
		一直在那裏看着,沒到00:00的時候就備份一次。所有的用戶線程
		如果結束了,守護線程自動退出,沒有必要進行數據備份了。
代碼演示
public class ThreadTest14 {
    public static void main(String[] args) {
        Thread t = new BakDataThread();
        t.setName("備份數據的線程");
        // 啓動線程之前,將線程設置爲守護線程
        t.setDaemon(true);
        t.start();
        // 主線程:主線程是用戶線程
        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class BakDataThread extends Thread {
    public void run(){
        int i = 0;
        // 即使是死循環,但由於該線程是守護者,當用戶線程結束,守護線程自動終止。
        while(true){
            System.out.println(Thread.currentThread().getName() + "--->" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

定時器

定時器的作用:
	間隔特定的時間,執行特定的程序。

	每週要進行銀行賬戶的總賬操作。
	每天要進行數據的備份操作。

	在實際的開發中,每隔多久執行一段特定的程序,這種需求是很常見的,
	那麼在java中其實可以採用多種方式實現:
		
		可以使用sleep方法,睡眠,設置睡眠時間,沒到這個時間點醒來,執行
		任務。這種方式是最原始的定時器。(比較low)

		在java的類庫中已經寫好了一個定時器:java.util.Timer,可以直接拿來用。
		不過,這種方式在目前的開發中也很少用,因爲現在有很多高級框架都是支持
		定時任務的。

		在實際的開發中,目前使用較多的是Spring框架中提供的SpringTask框架,
		這個框架只要進行簡單的配置,就可以完成定時器的任務。
代碼演示:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/*
使用定時器指定定時任務。
 */
public class TimerTest {
    public static void main(String[] args) throws Exception {

        // 創建定時器對象
        Timer timer = new Timer();
        //Timer timer = new Timer(true); //守護線程的方式

        // 指定定時任務
        //timer.schedule(定時任務, 第一次執行時間, 間隔多久執行一次);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime = sdf.parse("2020-03-14 09:34:30");
        //timer.schedule(new LogTimerTask() , firstTime, 1000 * 10);
        // 每年執行一次。
        //timer.schedule(new LogTimerTask() , firstTime, 1000 * 60 * 60 * 24 * 365);

        //匿名內部類方式
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                // code....
            }
        } , firstTime, 1000 * 10);

    }
}

// 編寫一個定時任務類
// 假設這是一個記錄日誌的定時任務
class LogTimerTask extends TimerTask {

    @Override
    public void run() {
        // 編寫你需要執行的任務就行了。
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime = sdf.format(new Date());
        System.out.println(strTime + ":成功完成了一次數據備份!");
    }
}

實現線程的第三種方式:實現Callable接口。(JDK8新特性。)

	這種方式實現的線程可以獲取線程的返回值。
	之前講解的那兩種方式是無法獲取線程返回值的,因爲run方法返回void。
	思考:系統委派一個線程去執行一個任務,該線程執行完任務之後,可能會有一個執行結果,我們怎麼能拿到這個執行結果呢?
使用第三種方式:實現Callable接口方式。
   這種方式的優點:可以獲取到線程的執行結果。
   這種方式的缺點:效率比較低,在獲取t線程執行結果的時候,當前線程受阻塞,效率較低。
代碼演示:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask; // JUC包下的,屬於java的併發包,老JDK中沒有這個包。新特性。

/*

 */
public class ThreadTest15 {
    public static void main(String[] args) throws Exception {

        // 第一步:創建一個“未來任務類”對象。
        // 參數非常重要,需要給一個Callable接口實現類對象。
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception { // call()方法就相當於run方法。只不過這個有返回值
                // 線程執行一個任務,執行之後可能會有一個執行結果
                // 模擬執行
                System.out.println("call method begin");
                Thread.sleep(1000 * 10);
                System.out.println("call method end!");
                int a = 100;
                int b = 200;
                return a + b; //自動裝箱(300結果變成Integer)
            }
        });

        // 創建線程對象
        Thread t = new Thread(task);

        // 啓動線程
        t.start();

        // 這裏是main方法,這是在主線程中。
        // 在主線程中,怎麼獲取t線程的返回結果?
        // get()方法的執行會導致“當前線程阻塞”
        Object obj = task.get();
        System.out.println("線程執行結果:" + obj);

        // main方法這裏的程序要想執行必須等待get()方法的結束
        // 而get()方法可能需要很久。因爲get()方法是爲了拿另一個線程的執行結果
        // 另一個線程執行是需要時間的。
        System.out.println("hello world!");
    }
}

關於Object類中的wait和notify方法。(生產者和消費者模式!)

第一:wait和notify方法不是線程對象的方法,是java中任何一個java對象
都有的方法,因爲這兩個方式是Object類中自帶的。
	wait方法和notify方法不是通過線程對象調用,
	不是這樣的:t.wait(),也不是這樣的:t.notify()..不對。

第二:wait()方法作用?
	Object o = new Object();
	o.wait();

	表示:讓正在o對象上活動的線程進入等待狀態,無期限等待,直到被喚醒爲止。
		o.wait();方法的調用,會讓“當前線程(正在o對象上活動的線程)”進入等待狀態。

第三:notify()方法作用?
	Object o = new Object();
	o.notify();
	表示:喚醒正在o對象上等待的線程。
	
	還有一個notifyAll()方法:
		這個方法是喚醒o對象上處於等待的所有線程。
		1、使用wait方法和notify方法實現“生產者和消費者模式”

什麼是“生產者和消費者模式”?
    生產線程負責生產,消費線程負責消費。
    生產線程和消費線程要達到均衡。
    這是一種特殊的業務需求,在這種特殊的情況下需要使用wait方法和notify方法。

1、wait和notify方法不是線程對象的方法,是普通java對象都有的方法。

2、wait方法和notify方法建立在線程同步的基礎之上。因爲多線程要同時操作一個倉庫。有線程安全問題。

3、wait方法作用:o.wait()讓正在o對象上活動的線程t進入等待狀態,並且釋放掉t線程之前佔有的o對象的鎖。

4、notify方法作用:o.notify()讓正在o對象上等待的線程喚醒,只是通知,不會釋放o對象上之前佔有的鎖。
代碼演示:

import java.util.ArrayList;
import java.util.List;

/*

模擬這樣一個需求:
    倉庫我們採用List集合。
    List集合中假設只能存儲1個元素。
    1個元素就表示倉庫滿了。
    如果List集合中元素個數是0,就表示倉庫空了。
    保證List集合中永遠都是最多存儲1個元素。

    必須做到這種效果:生產1個消費1個。
 */
public class ThreadTest16 {
    public static void main(String[] args) {
        // 創建1個倉庫對象,共享的。
        List list = new ArrayList();
        // 創建兩個線程對象
        // 生產者線程
        Thread t1 = new Thread(new Producer(list));
        // 消費者線程
        Thread t2 = new Thread(new Consumer(list));

        t1.setName("生產者線程");
        t2.setName("消費者線程");

        t1.start();
        t2.start();
    }
}

// 生產線程
class Producer implements Runnable {
    // 倉庫
    private List list;

    public Producer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        // 一直生產(使用死循環來模擬一直生產)
        while(true){
            // 給倉庫對象list加鎖。
            synchronized (list){
                if(list.size() > 0){ // 大於0,說明倉庫中已經有1個元素了。
                    try {
                        // 當前線程進入等待狀態,並且釋放Producer之前佔有的list集合的鎖。
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能夠執行到這裏說明倉庫是空的,可以生產
                Object obj = new Object();
                list.add(obj);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 喚醒消費者進行消費
                list.notifyAll();
            }
        }
    }
}

// 消費線程
class Consumer implements Runnable {
    // 倉庫
    private List list;

    public Consumer(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        // 一直消費
        while(true){
            synchronized (list) {
                if(list.size() == 0){
                    try {
                        // 倉庫已經空了。
                        // 消費者線程等待,釋放掉list集合的鎖
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能夠執行到此處說明倉庫中有數據,進行消費。
                Object obj = list.remove(0);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 喚醒生產者生產。
                list.notifyAll();
            }
        }
    }
}

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