3.1多線程基礎

1.進程:正在進行中的 程序(直譯)(任務管理器中的都是進程)
分配程序的空間,不執行。

2.線程:就是進程中的一個負責程序執行的控制單元(執行路徑)
一個進程中可以有多個執行路徑,稱之爲多線程。
一個進程中至少要有一個線程。

開啓多個線程是爲了同時運行多個代碼。
每一個線程都有自己運行的內容。這個內容稱爲線程要執行的任務。

其實應用程序的運行都是CPU在做着快速切換完成的。這個切換是隨機的。
管理進程的運行:中央處理器CPU,快速切換執行的進程,看起來是同時執行。
(多核(CPU)電腦可以提高多進程運行速度,但是還是得看內存大小。)

2.1
多線程的好處:解決了多部分代碼同時運行的問題。
多線程的弊端:開多了效率低。

2.2 JVM中的多線程解析:
①主線程:肯定有一條線程在執行main函數。
該線程的任務代碼都定義在main函數中。
(主線程結束時,虛擬機不一定結束,還有其他線程)
②垃圾回收線程:還有一條線程在執行垃圾回收器。(其實每個對象都具備着被回收的方法,在Object.finalize(),這個方法由對象的垃圾回收器調用此方法,垃圾回收器在System.gc()靜態方法)
該線程的任務代碼定義在垃圾回收器中。
/*//讓對象被回收,但是不是立即
class Demo
{
public void finalize() //其實沒必要覆寫
{
System.out.println(“demo ok”);
}
}

class ThreadDemo
{
public static void main(String[] args) //這是一個線程
{
new Demo();
new Demo();
System.gc(); //不是立即執行,這又是另一個線程,執行具有不確定性。
new Demo();
System.out.println(“HAHA”);
}
}
*/

等等。

2.3主線程運行示例:
主線程運行示例

3.多線程存在的意義

4.線程的創建方式
1)創建方式一:繼承Thread類
步驟:

1.定義一個類繼承Thread類。
2.覆蓋Thead類中的run方法:
    自定義的線程的任務通過Thread類中的run方法來體現。也就是說,run方法就是封裝自定義線程運行任務的函數。
    run方法中定義的就是線程要運行的任務代碼。

    開啓線程是爲了運行指定代碼,所以只有繼承Thread類,並複寫run方法。
    將運行的代碼定義在run方法中即可。

3.直接創建Thread類的子類對象。
4.調用start方法開啓線程並調用線程的任務run方法執行。

調用run和調用start的區別:

run:調用要在線程中執行的任務(代碼)。
start:開啓線程並調用線程的任務run方法執行。

代碼:

class Demo extends Thread
{
    private String name;
    Demo(String name)
    {
            this.name = name;
    }

    public void run()
    {
        int[] arr = new int[3];
        System.out.println(arr[3]);   ////發生異常,線程終止

        show();
    }

    public void show()
        {
            for (int x = 0;x < 30 ;x++ )
            {
            //  for (long  y = 0; y <= 1000000000l;y++ ){}   //long類型延遲比int明顯
                System.out.println(name+ "........x=" + x +".....name=" + Thread.currentThread().getName());   //getName()獲取線程的名字(Thread-數字(從0開始))(但線程可能沒有運行)。
    //要獲取運行時線程的名字,就得先通過靜態方法Thread.currentThread()獲得對當前正在執行的線程對象的引用。
            }
    }

}



class ThreadDemo2 
{
    public static void main(String[] args) 
    {
        Demo d1 =new Demo("旺財");   //創建對象的時候就已經完成了線程名稱的定義,所以.run()也會輸出線程名稱。
        /*  源碼:  public Thread() {
                  init(null, null, "Thread-" + nextThreadNum(), 0);
              }
           */

        Demo d2 = new Demo("xiaoqiang");

        d1.run();       //調用要在線程中執行的任務(代碼),和正常調用沒區別。
                       //這時返回的運行時線程名是  main
//          d1.start();    //創建並啓動線程。(使該線程開始執行;Java 虛擬機調用該線程的 run 方法。) 
            d2.start();
            System.out.println(4/0);     //發生算數異常,線程終止
            System.out.println("over");   //主線程,這時一共由三個線程,隨機執行

    }
}

Thread類中的方法和名稱:

方法:
1.getName():獲取線程的名字(Thread-數字)(從0開始)(但線程可能沒有運行)。
2.Thread.currentThread():靜態方法,獲得對當前正在執行的線程對象的引用。

線程的名稱:
1.主函數的線程名爲main
2.線程名自定義:
    在子類的初始化函數中第一行添加super(name),name爲自定義線程名。其實就是調用了Thread類的構造方法。

5.多線程運行圖解:

1.線程之間獨立運行,都有自己的運行空間,相互不影響。
2.線程發生異常立即跳棧,不再執行,不影響其他線程的執行。
主線程掛了,其他線程也不會立即結束,除非虛擬機關閉。

多線程運行圖解

6.線程的狀態:

CPU的執行資格:可以被CPU處理,在處理隊列中排隊
CPU的執行權:正在被CPU處理

線程的狀態

7.線程創建的第二種方式:實現Runnable接口
步驟:

1.定義類實現Runnable接口。
2.覆蓋接口中的run方法,將線程的任務代碼封裝到run方法中。
3.通過Thread類創建線程對象,並將Runnable接口的子類對象作爲構造函數的參數進行傳遞。
    爲什麼:因爲線程的任務都封裝在Runnable接口的子類對象的run方法中,
    所以要在線程對象創建時就必須明確要運行的任務。

4.調用線程對象的start方法開啓線程。

原因:

創建方式一,繼承Thread類。當需要線程的類中已經繼承了其他類,就不能再繼承Thread類了。
這個時候還要用到額外的方法創建就需要使用接口。而Thread類已經實現了Runnable接口,它默認run方法就是覆蓋的Runnable接口的run抽象方法,不執行任何操作。並且Thread類提供了可以帶Runnable接口對象的構造方法,來分配新的 Thread 對象,調用新對象的run方法。

內部實現的思想細節:
class Thread implements Runnable
{
    private Runnable r;
    Thread()
    {
    }
    Thread(Runnable r) //第二種創建方式
    {                //在新的Thread對象中覆蓋Runnable的run方法。

        this.r = r;
    }
    public void run()
    {
      if(r!=null)
        r.run();
    }
    public void start()
    {
        run();
    }
}


class SubThread extends Thread
{                       //第一種創建方式
    public void run()    //覆蓋Thread中的run()
    {
    }
}
SubThread s =new SubThread();
s.start();   //調用的是父類的start()和子類覆蓋的run()



class ThreadImpl implements Runnable
{
    public void run();   //第二種創建方式
}
Thread t = new Thread(new ThreadImpl());
t.start();


API說明:
Thread
public Thread(Runnable target)
分配新的 Thread 對象。這種構造方法與 Thread(null, target,gname) 具有相同的作用,其中的 gname 是一個新生成的名稱。自動生成的名稱的形式爲 “Thread-”+n,其中的 n 爲整數。

代碼:

class Demo  implements Runnable
{   
        public void run()
    {
            show();
        }
        void show()
    {
                for (int x = 0; x <20 ;x++ )
                {
                    System.out.println(Thread.currentThread().getName() + "......"  + x);
                }
        }

}


class ThreadDemo3 
{
    public static void main(String[] args) 
    {

        Thread t1 = new Thread(new Demo());   //Thread類是在構造方法中定義的是Runable接口中自己覆蓋的run方法,
                                                                                            //即類Thread實現了Runable接口
                /*API解釋:
                run
                    public void run()
                                如果該線程是使用獨立的 Runnable 運行對象構造的,則調用該 Runnable 對象的 run 方法;否則,該方法不執行任何操作並返回。 
                                Thread 的子類應該重寫該方法。
                */

                                                                                        //所以賦值一個實現Runable接口的子類,調用的就是子類覆蓋的的run方法。
        Thread t2 = new Thread(new Demo());
        t1.start();    //開啓線程並調用線程中的run方法執行
        t2.start();
    }
}

第二種方式的好處:

第一種方式,繼承了Thread,會變成Thread體系中的一員,具備了Thread類中的所有方法。但是用類描述事物完了後,如果僅僅只是需要將一部分代碼被對線程所操作的話,就沒有必要去具備線程對象中的所有方法。做繼承的目的僅僅是爲了覆蓋run方法,建立線程運行的任務。

所以有了第二種方式,實現Runnable接口,它的出現僅僅是將線程的任務進行了對象的封裝,不需要線程出現。如果需要有多線程,就去實現Runnable接口,把任務代碼封裝在run方法中,變成Runnable接口的子類對象,就是線程任務對象。(這是一種思想的變化)
Runnable r = new Student();

要運行多線程時,直接創建Thread對象,在創建對象的同時,明確線程任務對象就行了。
Thread t = new Thread(r);
t.start();

Thread類和其他需要多線程的類中都存在run()方法,向上抽取,而線程是事物的一個額外功能,所以抽取並實現了Runnable接口。

實現Runnable的好處:

1.將線程的任務從線程的子類中分離出來,進行了單獨的封裝。
    按照面向對象的思想將任務封裝成對象。
2.避免了Java單繼承的侷限性。

所以,創建線程的第二種方式較爲常見。

8.多線程小例子:

/*
賣票示例:

需求:四個窗口同時賣票

問題:
1.票數共用  :  就只能使用一個對象,而繼承方式需要創建四個對象,所以用接口定義方式創建線程。

2.保證每次買票過程能完成:

就是將多條操作貢獻數據的線程代碼封裝起來,當有線程在執行這些代碼的時候,其他線程不可以參與運算。
必須要當前線程把這些代碼都執行完畢後,其他線程纔可以參與運算。

在Java中,用同步代碼塊姐可以解決這個問題。

同步代碼塊的格式:
synchronized(對象)
{
        需要被同步的代碼;
}



多次啓用一個線程是非法的。
*/

class Ticket implements Runnable  // extends Thread  //繼承無法實現同時賣100張票完成。
{
    private  int num = 100;   //靜態化後,對象就沒有意義。而且要賣另外不同的100張票呢。
    Object obj = new Object(); //保證用的是同一個鎖

    public void run()
    {
    //  Object obj = new Object(); //這樣會有4把鎖,同步會失效
        sale();
    }

    public void sale()      //不能拋出異常,因爲實現的接口沒有拋
    {
        while(true)
        {

//同步代碼塊
            synchronized(obj)
            {
            if(num >0)
                {
/*              try
                {
                        Thread.sleep(10);            //線程安全問題測試
                }
                catch (InterruptedException e)
                {
                }
*/              
                System.out.println(Thread.currentThread().getName() + ":餘票:"+ --num);
                }
                }


        }
    }
}

class  TicketDemo
{
    public static void main(String[] args) 
    {
        Ticket t = new Ticket();     //創建一個線程任務對象,火車票
        Ticket tt = new Ticket();     //賣第二種票,動車票
        Thread t1 = new Thread(t,"窗口1(火車票)"); 
        Thread t2 = new Thread(t,"窗口2(火車票)");
        Thread t3 = new Thread(tt,"窗口3(動車票)");  //窗口3,4  買動車票
        Thread t4 = new Thread(tt,"窗口4(動車票)");  

        t1.start();
        t2.start();
        t3.start();
        t4.start();

//      t1.start();
//      t1.start();   //這裏會導致 主線程 發生線程狀態異常。
//      t1.start();
//      t1.start();

    }
}

問題分析:
線程安全問題的現象:

if(num >0)
System.out.println...

當num = 1 的時候:
第一個線程剛剛進去if循環準備執行輸出語句的時候,cpu不調用它了,使其進入臨時阻塞狀態。
然後cpu調用線程二進來執行輸出語句,輸出1,再--,0。
這時cpu回來調用線程一,這個時候不需要判斷直接執行了輸出語句,輸出0,再--,num的值爲 -1。顯然0號票不合理。

產生安全問題的原因:

(因爲num是共享的數據,多個線程操作時相互之間可能有影響。)

1.多個線程在操作共享數據。
(如果只有一個語句,不會出事。
兩條以上可能導致問題,因爲可能在結束一條語句時,其他線程進來了,使當前線程暫停進入臨時阻塞狀態。而等其他線程操作完,再回來執行該線程的第二條語句時,共享數據可能就已經發生了改變。)
2.操作共享數據的線程代碼有多條。

當一個線程在執行操作共享數據的多條代碼過程中,其他線程參與了運算,就會導致線程安全問題的產生。

解決問題:同步代碼塊

就是將多條操作貢獻數據的線程代碼封裝起來,當有線程在執行這些代碼的時候,其他線程不可以參與運算。
必須要當前線程把這些代碼都執行完畢後,其他線程纔可以參與運算。

在Java中,用同步代碼塊姐可以解決這個問題。

同步代碼塊的格式:
synchronized(對象)
{
        需要被同步的代碼;
}

同步的好處和弊端:

原理:在同步代碼塊中,當第一個線程進來時,獲取obj對象,開始執行代碼。這時沒有obj對象,其他線程無法進來,只有等到當前進程結束後四方obj對象。obj對象像個鎖,就叫對象鎖(同步鎖)。另外當前線程是不會一直有執行權的,只是切其他線程時它們進不來,只有再次切到當前線程,直到當前線程執行結束。

好處:解決了線程的安全問題。
弊端:相對降低了效率,因爲同步外的線程都會判斷同步鎖。

同步的前提:

同步中必須有多個線程,並使用的是同一個鎖。

9.同步函數:同步的第二種表現形式
在函數聲明中添加修飾符synchronized即可。

/*
需求:儲戶,兩個,每個都到銀行存錢,每次存100,共存三次。

*/

class Bank
{
    private int sum;     //共享數據
//  private Object obj = new Object();  //唯一對象鎖

    public synchronized void add(int num)
    {

    //  synchronized(obj){       //這就是函數的全部代碼,直接定義同步函數

            sum = sum +num;
            //這裏會出現安全隱患。
            System.out.println("sum = " + sum);     

    //}
    }
}

class Cus implements Runnable
{
    Bank b = new Bank();


    public void run()
    {   
            for (int x = 0; x < 3 ; x++ )
            {
                b.add(100);
            }

    }
}

class BankDemo 
{
    public static void main(String[] args) 
    {
        Cus c = new Cus();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);

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

驗證同步函數的鎖:

驗證結果是不是同一個鎖,即同步函數鎖用的不是obj。

而是調用該方法的對象,this

注意:
但是當同步函數設置成靜態的時候,對象鎖就是這個方法所屬類的字節碼對象。getClass()可以獲取該對象所屬類的的字節碼對象,而字節碼對象是Class類的對象。
就是同步代碼塊中寫this.getClass()可以獲得和靜態同步函數一樣的同步鎖。

另一種表示方式:類名.class;也是返回所屬類的字節碼對象。class是Class類的一個靜態屬性,而所有的類在加載時都是以Class類的對象形式加載的。

API中說明:
getClass:
返回的 Class 對象是由所表示類的 static synchronized 方法鎖定的對象。 


同步函數和同步代碼塊的區別:

    同步函數的鎖是固定的this。
    同步代碼塊的鎖是任意的對象(自定義,也不一定是obj)。

開發建議使用同步代碼塊。同步函數可以寫成同步代碼塊的簡寫形式。一簡寫,就有前提(如果用的鎖是this),有好處,有弊端。

驗證代碼:

/*
同步函數鎖驗證:
爲了驗證同步函數和同步代碼塊的鎖,
窗口一設置同步函數,
窗口二設置同步代碼塊,如果是同一個對象鎖,則不會輸出負值。  

結果是不是同一個鎖,即同步函數鎖用的不是obj。

而是調用該方法的對象,this

*/

class Ticket implements Runnable  // extends Thread  //繼承無法實現同時賣100張票完成。
{
    private  int num = 100;   //靜態化後,對象就沒有意義。而且要賣另外不同的100張票呢。
    private Object obj = new Object();         //驗證用
     boolean flag = true;

    public void run()
    {
        sale();
    }


//  public synchronized void sale()  
        public void sale()  
    {

                if(flag)
                    {
                        while(true)          //同步函數sale後,第一個線程進來就出不去了,着是死循環。所以不應該同步sale函數。
                        {                                   //解決方法:將死循環裏的代碼封裝成同步函數即可。
                            show();     //窗口一設置同步函數
                        }
                    }


                else                   //窗口二設置同步代碼塊,如果是同一個對象鎖,則不會輸出負值。
                {
                    while(true)
                    {
                    synchronized(obj)   //改成synchronized(this),就不會出現錯誤值
                             //如果同步函數是靜態的,則改成synchronized(this.getClass()),通過getClass()獲得本類的 static synchronized 方法鎖定的對象。 
                                                 //或者synchronized(Ticket.class),等效

                    {
                        if(num >0)
                        {
                            try{Thread.sleep(10); }catch (InterruptedException e){}  //線程安全問題測試

                            System.out.println(Thread.currentThread().getName() + ":餘票(obj):"+ --num);
                            }
                        }
                    }
            }       }       



    private synchronized void show()    //同步鎖不是obj,是this,即new Ticket()。
                                    //如果改成靜態的,則同步鎖對象就是所屬類的字節碼對象。
    {
        if(num >0)
            {
                try{Thread.sleep(10); }catch (InterruptedException e){}  //線程安全問題測試

                System.out.println(Thread.currentThread().getName() + ":餘票:(function)"+ --num);
            }
    }
}


class  SynFunctionLockDemo
{
    public static void main(String[] args) 
    {
        Ticket t = new Ticket();     //創建一個線程任務對象,火車票

        Thread t1 = new Thread(t,"窗口1");   //爲了驗證同步函數和同步代碼塊的鎖。設置同步函數
        Thread t2 = new Thread(t,"窗口2");//設置同步代碼塊

        t1.start();   
//這裏爲了讓線程一能夠有完成 flag =true的賦值動作,讓主線程暫停一下。
        try{Thread.sleep(100); }catch (InterruptedException e){}     //線程安全問題測試
        t.flag = false;
        t2.start();

    }
}

10.單例模式涉及的多線程問題。

關於懶漢式單例設計模式在多線程中的
面試問題:
1.安全麼?   不安全
2.寫個同步函數,效率高麼?同步鎖是哪一個?怎麼解決?
不高,因爲每次獲取對象都要判斷同步鎖。
靜態同步函數的同步鎖是所在類的字節碼對象,可以通過類名.class來獲取。
解決辦法:通過同步代碼塊把創建對象的代碼封裝起來,再放到對象是否爲空的判斷裏,這樣就只用判斷一次同步鎖,提高效率。
/*
多線程下的單例:

*/

//餓漢式
class Single
{
        private Single(){}
        private static Single s = new Single();
        public  static void getInstance()
        {
            return s;    //一句話,不存在安全隱患。
        }
}

//懶漢式
class Single
{
    private Single(){}
    private static Single s =null;
    public static/* synchronized*/ getInstance()   //解決辦法1:加同步函數,但是每次拿對象都要判斷同步鎖,效率低。
    {
        if(s==null)    //解決辦法2:通過同步代碼塊把創建對象的代碼封裝起來,再放到判斷裏,這樣就只用判斷一次同步鎖。
            {                                                               //加判斷是解決效率問題
                    synchronized(Single.class)   //加同步是解決安全問題      
                    {
                    if(s == null)
                              //線程0進來,切換到線程1,線程1暫停,再切到線程0,建立返回一個對象,
                                //然後切換到線程1,不用判斷,又建立一個對象。此時無法保證對象的唯一性了。存在安全隱患。
                        s = new Single();

                    }       
            }
            reruen s;
    }
}


class  SingleDemo  
{
    public static void main(String[] args) 
    {
        System.out.println("Hello World!");
    }
}

11.死鎖示例:

/*
死鎖:常見情景之一:同步嵌套

*/
class Ticket implements Runnable  
{
    private  int num = 100;  
    private Object obj = new Object();      
    boolean flag = true;

    public void run()
    {
        sale();
    }


        public void sale()  
    {

                if(flag)
                    {
                        while(true)         
                        {       
                            synchronized(obj)
                            {
                                show();     
                            }
                        }
                    }


                else           
                {
                    while(true)
                    {

                        show();   //線程二拿着this鎖進obj鎖,同時發生導致程序死鎖
                    }
            }   

            }       



    private  synchronized void show()  //線程一拿着obj鎖進this鎖,同時發生導致程序死鎖
    {       
        synchronized(obj)   
                {
                        if(num >0)
                            {
                            try{Thread.sleep(10); }catch (InterruptedException e){}

                            System.out.println(Thread.currentThread().getName() + ":餘票(obj):"+ --num);
                            }
                    }   


    }
}


class  DeadLockDemo
{
    public static void main(String[] args) 
    {
        Ticket t = new Ticket();  

        Thread t1 = new Thread(t,"窗口1");  
        Thread t2 = new Thread(t,"窗口2");

        t1.start();   

        try{Thread.sleep(100); }catch (InterruptedException e){}    
        t.flag = false;
        t2.start();

    }
}

死鎖程序:面試用

class Test implements Runnable
{
    private boolean flag;
    Test(boolean flag)
    {
        this.flag = flag;
    }

    public void run()
    {

                if(flag)
                {
                    while(true)     //增加死鎖概率
                    {
                            synchronized(MyLock.locka)
                            {
                                System.out.println(Thread.currentThread().getName()+ "...if         locka...");
                                    synchronized(MyLock.lockb)
                                    {
                                        System.out.println(Thread.currentThread().getName()+"...if          lockb...");
                                    }
                            }
                        }
                    }


                else
                {
                    while(true)
                    {
                            synchronized(MyLock.lockb)                      
                            {
                                System.out.println(Thread.currentThread().getName()+"...else            lockb...");
                                    synchronized(MyLock.locka)
                                    {
                                        System.out.println(Thread.currentThread().getName()+"...else            locka...");
                                    }
                            }
                    }   
            }
    }
}

class MyLock
{
    public static final Object locka = new Object();
    public static final Object lockb = new Object();
}

class DeadLockTest
{
    public static void main(String[] args)
    {
            Test a = new Test(true);     //因爲是boolean型變量,值是固定的 。所以多個內容沒有影響
            Test b = new Test(false);

            Thread t1 = new Thread(a);
            Thread t2 = new Thread(b);

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