黑馬程序員_Java基礎_線程基礎,創建,同步(單例設計模式的同步),死鎖

 一,進程與線程

1,進程定義:進程就是指正在執行的程序,怎樣查看正在執行的進程呢?我們在使用電腦的時候,其實就有多個正在執行的程序,通過Ctri+Alt+Del 組合鍵可以進入windows任務管理器查看進程,我們進入後會看到很多.exe,這些就是我們的電腦當前正在執行的程序,也就是一個個的進程。

每一個程序執行的都有一個執行順序,該順序是一個執行路徑,或者叫一個控制單元。

 

2,線程:是進程中一個獨立控制單元,控制着進程的執行。一個進程中至少有一個線程。我們之前寫的程序都是單線程的程序。我們在編譯java文件的時候,會啓動javac進程,啓動java命令時,會啓動JVM執行.class文件。這個進程中至少有一個負責線程的執行,這個線程運行的代碼就是main函數裏面的代碼,該線程稱之爲主線程。

 

注意:通常我們可以理解爲這樣的程序時單線程程序,實際上並不止就這一個線程。原因是還有一個線程就是JVM的垃圾回收機制中控制垃圾回收的線程,在主線程執行的時候會啓動,回收內存中不再使用的對象的內存,其實最少有兩個線程。

 

多線程最常見的應用就是下載軟件,下載軟件在下載東西的時候,就是多個線程同時向服務器發送請求,同時多條路勁在下載文件。其實多線程在執行的時候並不是多個線程同時執行,而是CPU在多個線程之間進行這快速的切換,中間的事件間隔我們可以忽略,因爲太快了,所以我們認爲是同時在執行,其實這種執行叫做併發執行。

 

二,線程的五種狀態:


(1)被創建:創建Thread類的子類,將要運行的代碼放在run方法中,調用start方法創建線程,並調用run方法,此時線程進入運行狀態。

(2)運行:運行狀態就是run方法中的代碼執行過程,調用stop方法終止整個線程,run方法結束。運行狀態時調用sleep方法或者wait方法,是線程進入(3)凍結狀態,此時線程放棄了執行權,當睡眠時間或者從凍結狀態調用notify方法,能從凍結狀態轉化爲運行狀態。

(4)阻塞:這個狀態比較特殊,這個狀態線程具有執行權,但是在等待CPU資源,這個狀態有可能在run中的代碼運行一部分還沒運行完時,CPU去執行其他線程中的代碼去了。凍結狀態被叫醒後不一定直接進入運行狀態,也有可能進入阻塞狀態。當然阻塞狀態也有可能進入凍結狀態。凍結狀態:沒有了執行權,當然某個線程睡眠或者等待的時候。

(5)消亡:也就是該線程結束,run中的代碼執行完。如果中途要關閉,則通過調用stop方法,否則線程執行完自動消亡。

 

三,自定義線程:

參考java文檔的Thread類時,發現:

創建新執行線程有兩種方法。一種方法是將類聲明爲 Thread 的子類。該子類應重寫 Thread 類的 run 方法。接下來可以分配並啓動該子類的實例。例如,計算大於某一規定值的質數的線程可以寫成:

     class PrimeThread extends Thread {

         long minPrime;

         PrimeThread(long minPrime) {

             this.minPrime = minPrime;

         }

 

         public void run() {

             // compute primes larger than minPrime

              . . .

         }

     }

然後,下列代碼會創建並啓動一個線程:

     PrimeThread p = new PrimeThread(143);

     p.start();

通過API的解釋可以看出創建一個線程的方式一繼承Thread類:

1,創建一個類,繼承Thread

2,重寫Thread類中的run方法

3,調用線程的啓動方法,start,該方法的作用有兩個,一個是啓動線程和調用run方法。

示例一: 

class RunDemo extends Thread {
    public void run() {
        for(int i=0;i<70;i++) {
            System.out.println("Demo run ......");
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        RunDemo d = new RunDemo();
        d.start();
        
        for(int i=0;i<70;i++) {
            System.out.println("main run...");
        }
    }
}

個程序在執行的時候應該是Demo runmain run是交替執行的。執行過程是主線程啓動,main函數執行,然後RunDemo線程啓動,run方法和main方法裏面的for循環併發執行。

4,爲什麼定義一個繼承Thread類的類的線程時候,要調用start方法,而不調用run方法呢?原因是如果調用了run方法,那麼就不是一個獨立的控制單元控制一段代碼塊的執行了,就成了方法的調用,程序中相當於只有一個main線程。程序會按順序執行。

 

示例二:定義一個線程類,然後在main方法中啓動兩個自定義線程,交替執行線程中的內容。

class RunDemo extends Thread {
    //private String name;
    RunDemo(String name) {
        //this.name = name;
        super(name);//調用父類的構造函數給自定義線程賦一個名字
    }
    public void run() {
        for(int i=0;i<70;i++) {
            System.out.println(Thread.currentThread().getName() + " run ......" + i);
            //System.out.println(this.getName() + " run ......" + i);等價於上面這個
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        RunDemo d = new RunDemo("one");
        RunDemo d1 = new RunDemo("two");
        d.start();
        d1.start();
        /*for(int i=0;i<70;i++) {
            System.out.println("main run...");
        }*/
    }
}


注意:每個線程都有默認的名字,Thread-編號,編號是從0開始的。

Thread.currentThread().getName()可以獲得線程對象的名字,通過setName或者構造函數可以設置名字,其他操作查看java文檔。

 

自定義線程方式二:

創建線程的另一種方法是聲明實現 Runnable 接口的類。該類然後實現 run 方法。然後可以分配該類的實例,在創建 Thread 時作爲一個參數來傳遞並啓動。採用這種風格的同一個例子如下所示:

     class PrimeRun implements Runnable {

         long minPrime;

         PrimeRun(long minPrime) {

             this.minPrime = minPrime;

         }

 

         public void run() {

             // compute primes larger than minPrime

              . . .

         }

     }

然後,下列代碼會創建並啓動一個線程:

     PrimeRun p = new PrimeRun(143);

     new Thread(p).start();

實現Runnable接口的方式創建線程步驟:

1,定義一個類實現Runnable接口;

2,覆蓋Runnable接口中的run方法;目的:將線程要運行的代碼存放在該run方法中;

3,通過Thread類建立線程對象;

4,將Runnable接口的子類對象作爲實際參數傳遞給Thread類的構造函數;原因是:run方法是屬於Runnable接口的子類對象,所以要讓線程指定所指定對象的run方法,就必須明確run方法所屬的對象;

5,調用Thread類的run方法,啓動線程,並調用Runnable接口子類的run方法。

 

示例:

    需求:模擬火車站窗口購票系統,一共100張票

    分析:利用多線程的原理,火車票多個窗口同時在賣一定數量的火車票,當窗口1賣了1號座位的車票,其他窗口就不能再賣1號座位的票了;那個窗口賣的是幾號座位的票取決於cpu的執行順序,多個窗口賣火車票,相當於多個線程在同時執行;如果使用方式一創建線程必定出問題,假設四個窗口,每個窗口都要創建一個Thread子類的對象,這樣一共就是400張票。如果只創建一個對象,讓該對象運行四次,那麼運行時必定會出現錯誤提示,線程狀態錯誤。所以使用這種方式創建時不可以的。解決方法是使用第二種創建線程的方式:

代碼如下:

class Demon2 implements Runnable {
    private int tickets = 100;
    Object obj = new Object();
    public void run() {
        while(true) {
            if(tickets>0) {
                System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);
            }
        }
    }
}
class ThreadTest2 {
    public static void main(String[] args) {
        //創建Runnable接口子類對象
        Demon2 d = new Demon2();
        //創建線程對象,將Runnable接口的子類對象傳給線程
        Thread t1 = new Thread(d);
        Thread t2 = new Thread(d);
        Thread t3 = new Thread(d);
        Thread t4 = new Thread(d);
        //啓動線程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

總結:實現方式和繼承方式的區別:

繼承Thread,線程代碼存放在Tread子類的的run方法中。

實現Runnable,線程代碼存放在接口的子類的run方法中。

實現的好處:避免了單線程的侷限性。定義線程的時候,第一種方式不建議使用。建議使用第二種方式。

,線程的同步:

1,上述賣票系統,在判斷tickets之後讓該線程睡眠1秒鐘,這時候通過分析發現會打印出0-1-2-3等錯誤座位,這就是多線程存在的安全問題。原因:當多條語句在操作同一個線程共享數據時,一個線程的多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行,導致共享數據的錯誤;

 

解決方法:

對多條操作共享數據的語句,只能讓一個線程執行完,在執行過程中,其他線程不可以參與進來執行;

2Java對多線程的安全問題有專門的解決方法:就是同步代碼塊;

synchronized(對象) {

需要被同步的代碼塊

}

對象如同鎖,持有鎖的線程可以在同步中執行。沒有持有鎖的線程即使獲得cpu的執行權,也進不去,因爲沒有鎖;哪些代碼需要同步,就看哪些語句在操作共享數據;

 

3,同步的前提是:

(1)要有兩個或兩個以上的線程;(2)必須是多個線程使用同一個鎖;(3)必須保證同步中只能有一個線程在執行;

請看下面示例:

class Demon2 implements Runnable {
    private int tickets = 100;
    Object obj = new Object();
    public void run() {
        while(true) {
            synchronized(obj) {  //重點部分,加鎖了。每個線程進來之前都會判斷該鎖是否開啓,
                                //如果開啓就進入,然後將鎖關閉,這樣後來的線程就沒法進入,等之前進來的程序執行完後才能進來
                if(tickets > 0) {
                    try{Thread.sleep(10);}catch(Exception e) {}
                    System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);
                }
            }
        }
    }
}
class ThreadTest2 {
    public static void main(String[] args) {
        Demon2 d = new Demon2();
        Thread t1 = new Thread(d);
        Thread t2 = new Thread(d);
        t1.start();
        t2.start();
    }
}

4,在函數上加鎖:

    雖然鎖的方法有兩種,就是鎖住共享數據部分或者鎖住函數,但是這裏如果直接給函數上鎖的話,一旦一個線程進去之後就不能出來,所以不能直接在run函數上加鎖,要先將共享數據封裝到一個函數內部,然後多該封裝函數加鎖。


class Demon2 implements Runnable {
    private int tickets = 1000;
    //Object obj = new Object();
    boolean flag = true;
    public void run() {
        if(flag) {
            while(true) {
                synchronized(this) {  //如果改成自定義的obj,那麼這兩個進程使用的鎖就不是同一個鎖,不滿足同步的條件
                    if(tickets > 0) {
                        try{Thread.sleep(10);}catch(Exception e) {}
                        System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);
                    }
                }
            }
        }
        else
            while(true){
                show();
            }
    }
    public synchronized void show() {//這裏的鎖使用的對象時this
        if(tickets > 0) {
            try{Thread.sleep(10);}catch(Exception e) {}
            System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);
        }
    }
}
class ThreadTest2 {
    public static void main(String[] args) {
        Demon2 d = new Demon2();
        Thread t1 = new Thread(d);
        Thread t2 = new Thread(d);
        t1.start();
t1.flag = flase;
        t2.start();
    }
}
上面的程序,如果將show方法改成靜態,那麼所的對象就是類對象的字節碼,Demo2.classsynchronized(this)要將this改成Demo2.class程序才能正常運行.原因是靜態在類一加載就進內存了,類進內存的時候是沒有類的,但一定有該類對應的字節碼文件,他也是一個對象。所以靜態同步使用的鎖是該方法所在類的字節碼文件對象。類名.class
 

五,結合線程同步的單例設計模式。


//餓漢式
class Single {
    private static final Single s = new Single();
    private Single() {}
    public static Single getInstance() {
        return s;
    }
}
//帶延遲加載的懶漢式
class Single2 {
    private static Single2 s = null;
    private Single2() {}
    public static Single2 getInstance() {
        if(s == null) {
            synchronized (Single2.class) {
                if(s == null) {
                    s = new Single2();
                }
            }
        }
        return s;
    }
}

重點:面試中可能會問到:懶漢式和餓漢式的區別是什麼?回答:懶漢式的特點在於延遲加載。懶漢式的延遲加載有沒有什麼問題?回答:如果是多線程訪問時會出現安全問題。解決方法是同步來解決。用同步代碼塊和同步函數都可以,但是效率比較低。用雙重判斷的方式能夠解決效率問題。同步的時候的鎖是屬於該類所屬的字節碼文件對象。

 

六,死鎖。

所謂的死鎖就是指,兩個進程各自拿着各自的鎖而不是放資源,而每個線程要想運行,就必須拿到對方的鎖,這時候就會出現死鎖的問題。

關於死鎖的示例:


class ThreadDead implements Runnable {
    private boolean flag;
    public ThreadDead(boolean flag) {
        this.flag = flag;
    }
    public void run() {
        while(true) {
            if(flag) {
                synchronized (MyLock.lock1) {
                    System.out.println("if lock1 run...");
                    synchronized (MyLock.lock2) {
                        System.out.println("if lock2 run...");
                    }
                }
            }
            else {
                synchronized (MyLock.lock2) {
                    System.out.println("else lock2 run...");
                    synchronized (MyLock.lock1) {
                        System.out.println("else lock1 run...");
                    }
                }
            }
        }
    }
}
class MyLock {
    static Object lock1 = new Object();
    static Object lock2 = new Object();
}
public class DeadLock {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ThreadDead(true));
        Thread t2 = new Thread(new ThreadDead(false));
        t1.start();
        t2.start();
    }
}





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