Java基礎——線程(一)

線程

  • 進程:正在執行中的程序。每一個進程執行,都有一個執行的順序,該順序就是一個執行路徑,或者叫一個控制單元。
  • 線程:就是進程中的一個獨立的控制單元,線程在控制着進程的執行。

簡單一點來說進程和線程的關係,打開任務管理器可以看到很多正在執行的程序,每一個正在執行的程序就是進程,
而比如說迅雷下載數據的時候,會開闢很多條請求去找服務端請求數據(一條請求下載1~20%,另一條請求下載21~40%……這樣可以提高效率),而這些開闢的請求就是線程。

一個進程中至少有一個線程。

例如:

public class ThreadDemo {
    public static void main(String[] args) 
    {
        for (int x=0;x<800 ;x++ )
        {
            System.out.println("Hello World");
        }
    }
}
  • 在該程序運行的時候,Java虛擬機會啓動,這個時候就會多一個進程java.exe.
  • 該進程中至少有一個線程負責Java程序的運行,而且這個線程運行的代碼存在於main方法中,該線程稱之爲主線程。
  • 其實要是深追究的話,該程序運行的時候,不止一個線程,還有負責垃圾回收機制的線程,

多線程存在的意義:

  1. 可以使在運行時有多個程序同時運行的效果。
  2. 多條線程運行同一個程序,提高了程序運行的效率。

創建線程

  1. 如何自定義一個線程呢?
    步驟:
    1. 定義類繼承Thread
    2. 複寫Thread中的run方法(將自定義的代碼存儲在run方法中,讓線程運行)。
    3. 調用線程中的start方法該方法有兩個作用:啓動線程和調用run方法。

示例:運行一下代碼,

//創建線程
class Demo extends Thread
{
    public void run()
    {
        for (int x=0;x<70 ;x++ )
        {
            System.out.println("demo   run---"+x);
        }
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) 
    {
        Demo d = new Demo();
        d.start();


        for (int x=0;x<70 ;x++ )
        {
            System.out.println("Hello  World......."+x);
        }
    }
}

會出現圖中的結果:
自定義線程運行結果

由運行結果可以看到,程序執行過程中,自定義線程和主線程共同搶奪CPU資源,運行流程圖如下:

簡單的多線程流程圖

  • 發現運行的結果每次都不一樣,這是因爲多個線程都在獲取CPU的執行權,CPU執行到誰,誰就運行
  • 明確一點,在某一時刻,只能有一個程序在運行(多核除外),CPU在做着快速的切換,以達到看上去是同時運行的效果。
  • 我們可以形象的把多線程的運行行爲理解爲線程在互相搶奪CPU的執行權。
  • 這就是多線程的一個特性:隨機性。誰搶到誰執行,至於執行多長時間,CPU說了算。

爲什麼要覆蓋run方法呢?

  • Thread類用於描述線程。
  • 該類定義了一個功能,用於存儲線程要運行的代碼,該存儲功能就是run方法。
  • 也就是說Thread類中的run方法,用於存儲線程要運行的代碼。

簡短的一個小練習:

//創建兩個線程,和主線程交替執行


//那麼我先定義一個類繼承Thread
class Test extends Thread
{
    private String name;//爲了區分線程1和線程2,給線程一個自己特有的標識
    Test(String name)
    {
        this.name=name;
    }
    public void run()//1,固有格式:先覆蓋run方法,在run方法中寫上線程要執行的代碼
    {
        for (int x=0;x<60 ;x++ )
        {
            System.out.println(name+"test   run----"+x);
        }

    }
}

public class ThreadTest {
    public static void main(String[] args) 
    {
        Test t1 = new Test("one");
        Test t2 = new Test("two");//2、建立線程
        t1.start();//3、啓動線程
        t2.start();
        for (int x =0;x<60 ;x++ )//4、主函數執行的代碼
        {
            System.out.println("main  run---------"+x);
        }
    }
}

運行結果部分截圖:

線程小練習運行結果

  • 線程都是有自己默認的名稱的:就是Thread_編號,該編號從0開始。
  • 那麼既然有自己默認的名稱,這個名稱也是可以自己進行修改的。設置名稱的兩種方法:
    • this.setName();
    • super(name);(因爲Thread類中有構造函數直接可以自定義線程名稱)
  • 獲取當前線程對象:
    • this
    • static Thread currentThread();(這個用的比較多一點)兩個其實返回的是同一個對象。其實用法就是Thread.currentThread();

通過一段小程序來練習一下線程名稱的獲取:

//創建兩個線程,和主線程交替執行
//然後顯示出線程的默認名稱,

class Test extends Thread
{

    public void run()
    {
        for (int x=0;x<60 ;x++ )
        {
            System.out.println(this.getName()/*獲取名稱*/+"===run----"+x);
        }

    }
}

public class ThreadDemo3 {
    public static void main(String[] args) 
    {
        Test t1 = new Test();
        Test t2 = new Test();
        t1.start();
        t2.start();
        for (int x =0;x<60 ;x++ )
        {
            System.out.println("main  run---------"+x);
        }
    }
}

運行結果爲:
獲取線程默認名稱運行結果
發現在默認情況下,每個線程都有自己的名稱,名稱有自己的編號,編號從0開始。

接下來自定義線程的名稱:

//創建兩個線程,和主線程交替執行
//自定義線程的名稱

class Test extends Thread
{
    Test(String name)
    {
        super(name);
    }
    public void run()
    {
        for (int x=0;x<60 ;x++ )
        {
            System.out.println(Thread.currentThread().getName()+"===run----"+x);
                              //獲取當前線程對象。
        }

    }
}

public class ThreadDemo4 {
    public static void main(String[] args) 
    {
        Test t1 = new Test("one");
        Test t2 = new Test("two");
        t1.start();
        t2.start();
        for (int x =0;x<60 ;x++ )
        {
            System.out.println("main  run---------"+x);
        }
    }
}

運行結果爲:
自定義線程名稱運行結果

線程的五種狀態:

  • 被創建:已經被創建,但是沒有啓用的線程。
  • 運行:已經被創建,而且正在執行的線程(有執行資格,有執行權)
  • 臨時狀態:有執行資格但是沒有執行權(CPU在某一個時間只能執行一個線程,而這個時候在等候的其他可執行線程就處在臨時狀態上)
  • 凍結:當線程遇到sleep(time),wait指令的時候,會進入凍結狀態,(沒有執行資格,也沒有執行權,但是該線程沒有掛掉)什麼時候醒呢?等到sleep時間到,或者wait遇上notify(喚醒),該線程就會變爲臨時狀態,有了執行資格,等待CPU的執行權。
  • 消亡:當run方法執行完,或者遇到stop方法,表明該線程掛掉了。

通過模擬售票系統來理解線程的應用:

//火車站售票的例子:

class Ticket extends Thread
{
    //因爲售票系統需要開啓多線程,但是票數是固定不變的,所以需要這些線程共享一個數據,所以就把票數定義成靜態的
    private static int count =100;
    public void run()
    {
        while (true)
        {
            if (count>0)
            {
                System.out.println(Thread.currentThread().getName()+"---"+count--);
            }
        }
    }

}
public class TicketDemo {
    public static void main(String[] args) 
    {
        //這裏假設開啓四個窗口售票:
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();
        Ticket t3 = new Ticket();
        Ticket t4 = new Ticket();
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

運行結果:
售票系統模擬運行結果

由以上代碼中,我們可以看出,在定義票的總數的時候,爲了讓多個線程共享同一個數據,將數據定義成了靜態的,這樣做的缺點是生命週期太長了,一直到類沒有數據纔會被清空。所以引出了第二種創建線程的方法。

創建線程的第二種方法:

  • 1,定義類實現Runnable接口
  • 2,覆蓋Runnable接口中的run方法
    • 將線程要運行的代碼存放在該run方法中
  • 3,通過Thread類建立線程對象。
  • 4,將Runnable接口的子類對象作爲實際參數傳給Thread類的構造函數。
    • 爲什麼要將Runnable接口的子類對象傳遞給Thread的構造函數呢? 因爲自定義的run方法所屬的對象是Runnable接口的子類對象,,所以要讓線程去指定對象的run方法,就必須明確該run方法所屬對象。
  • 5,調用Thread類的start方法開啓線程,並調用Runnable接口子類中的run方法。

實例2:

//火車站售票的例子:
//創建線程的第二種方式:
class Ticket implements Runnable
{
    //這裏無需定義成靜態的
    private int count =100;
    //同樣的要覆蓋run方法
    public void run()
    {
        while (true)
        {
            if (count>0)
            {
                System.out.println(Thread.currentThread().getName()+"---"+count--);
            }
        }
    }

}
public class TicketDemo {
    public static void main(String[] args) 
    {
        //創建一個Runnable子類對象。
        //再創建一個Thread對象,將Runnable子類對象作爲實際參數傳給Thread類
        //開啓線程:
        Ticket c = new Ticket();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        Thread t3 = new Thread(c);
        Thread t4 = new Thread(c);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

運行結果:
第二種創建對象方式運行結果

實現方式和繼承方式有什麼區別呢?

  • 實現的好處:避免了單繼承的侷限性。
  • 在定義線程時,建議使用實現方式。
  • 繼承Thread:線程代碼存放Thread子類run方法中。
  • 實現Runnable:線程代碼存在接口的子類run方法中。

通過分析發現,多線程運行出現了安全問題。

多線程安全問題出現的原因圖解

  • 問題的原因是:當多條語句在操作同一個線程共享數據時,一個線程對多條語句只執行了一部分們還沒有執行完,另一個線程參與進來執行,導致了共享數據的錯誤。
  • 解決辦法:對多條操作共享數據的語句,只能讓一個線程都執行完,在執行過程中,其他線程不可以參與執行。
  • Java對於多線程的安全問題提供了專業的解決方式。就是同步代碼塊。synchronized(對象) { 需要被同步的代碼 }

這裏我們用sleep來模擬CPU切換出去的效果,再次執行以上的代碼:

//火車站售票的例子:
//創建線程的第二種方式:
class Ticket implements Runnable
{
    //這裏無需定義成靜態的
    private int count =100;
    //同樣的要覆蓋run方法
    public void run()
    {
        while (true)
        {
            if (count>0)
            {
                try
                {
                    Thread.sleep(10);//線程剛判斷完進來就睡着了,以此來模擬CPU切換的效果。
                }
                catch (InterruptedException e)//中斷異常之後再進行詳細的解說
                {
                    //這裏爲了簡單起見就不進行處理了。
                }

                System.out.println(Thread.currentThread().getName()+"---"+count--);
            }
        }
    }

}
public class TicketDemo {
    public static void main(String[] args) 
    {
        //創建一個Runnable子類對象。
        //再創建一個Thread對象,將Runnable子類對象作爲實際參數傳給Thread類
        //開啓線程:
        Ticket c = new Ticket();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        Thread t3 = new Thread(c);
        Thread t4 = new Thread(c);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

運行結果是:
線程安全問題演示

運行結果顯示,這裏還真的出現了安全問題。要怎麼解決呢?
上邊提到了,用Java中的同步代碼塊進行解決。
修改方案如下:

//火車站售票的例子:
//解決多線程安全問題,用同步代碼塊:
class Ticket implements Runnable
{
    private int count =100;
    Object obj = new Object();
    public void run()
    {
        while (true)
        {
            //同步代碼塊是要在外邊加一個鎖,只要一個線程進來了,在他沒有執行完同步代碼塊中的內容之前,
            //其他線程都進不來,
            //這個例子就好比火車上的廁所,前一個人出不來,後一個人就別想要進去。
            //用同步代碼塊需要一個對象參數,在這裏哪一類型的對象都可以,
            //需要注意的是,同步代碼塊擴住的範圍是操作共有數據的(count)部分都要擴起來
            synchronized(obj)
                {
                    if (count>0)
                    {
                        try
                        {
                            Thread.sleep(10);
                        }
                        catch (InterruptedException e)
                        {
                            //這裏爲了簡單起見就不進行處理了。
                        }

                        System.out.println(Thread.currentThread().getName()+"---"+count--);
                    }
                }
            }
        }
    }

public class TicketDemo {
    public static void main(String[] args) 
    {
        //創建一個Runnable子類對象。
        //再創建一個Thread對象,將Runnable子類對象作爲實際參數傳給Thread類
        //開啓線程:
        Ticket c = new Ticket();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        Thread t3 = new Thread(c);
        Thread t4 = new Thread(c);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

運行結果:
同步代碼塊運行結果

同步代碼塊:

synchronized(對象)
{
    //需要被同步的代碼
}

對象如同鎖。只有鎖的線程纔可以在同步中執行。
沒有持有鎖的線程及時獲取CPU執行權,也進不去,因爲沒有獲取鎖。

經典的實例說明:火車上的衛生間。

同步的前提:

  • 必須要有兩個或兩個以上的線程。
  • 必須是多個線程使用同一個鎖。

必須保證同步中只有一個線程在運行。

好處:解決了多線程的安全問題。

弊端:多個線程都需要判斷鎖,較爲消耗資源,

練習:銀行存款的例子:

//銀行儲戶存錢的例子:兩個用戶同時過來存錢,兩人都存300,每次存100 共存3次
//希望用多線程;


/*
思路:銀行只有一個,同時裏邊會有一個和,表示銀行裏現在一共有多少錢了。
        銀行還有存錢方法,add
    儲戶可以 有多個,這裏可以開啓多線程,儲戶調用add方法進行存錢,
*/
/*
步驟:1 先描述類,運行程序
      2 看哪裏有多線程產生的安全問題,進行處理
            如何找問題:
            1,明確哪些代碼是多線程運行代碼
            2,明確共享數據
            3,明確多線程運行代碼中哪些語句是操作共享數據的。

*/
class Bank
{
    private int sum;
    public void add(int n)//add方法中有兩句代碼,分析問題可能發生的情況。
    {
        sum = sum+n;
        try{Thread.sleep(10);}catch(Exception e){}
        //假如線程在這裏發生了CPU切換(用sleep方法模擬切換出去的動作),線程0進來sum值變成100-->跳出
        //線程2現在進來,因爲是共享數據,將sum 的值變爲200-->跳出
        //這個時候線程0回來了,打印sum值得時候一看是200,就打印出來了,
        //這就出現了兩次200的情況。


        System.out.println(Thread.currentThread().getName()+"*****"+sum);
    }
}
class Chus implements Runnable
{
    Bank b = new Bank();
    public void run()//這裏是多線程運行的代碼,每一個線程都會有一次循環,而且只有一句執行語句,沒有安全問題
    {
        for (int x=0;x<3 ; x++)
        {
            b.add(100);//線程運行代碼中調用到了add方法,
        }

    }

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

因爲該程序有安全問題,所以運行結果如下:
銀行存款事例不安全結果

所以需要同步代碼塊進行處理:

//處理過之後的代碼
class Bank
{
    private int sum;
    Object obj = new Object();
    public void add(int n)
    {
        synchronized(obj)
        {
            sum = sum+n;
            try{Thread.sleep(10);}catch(Exception e){}
            System.out.println(Thread.currentThread().getName()+"*****"+sum);
        }

    }
}
class Chus implements Runnable
{
    Bank b = new Bank();
    public void run()
    {
        for (int x=0;x<3 ; x++)
        {
            b.add(100);
        }

    }

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

運行結果發現:
處理完之後的運行結果

發現同步代碼塊和函數都有一個功能就是封裝代碼,所以我們可以將同步代碼塊改寫爲同步函數的形式,這樣就不用創建obj對象了。修改後的代碼如下:

class Bank
{
    private int sum;
    //Object obj = new Object();
    public synchronized void add(int n)
    {
        //synchronized(obj)
        {
            sum = sum+n;
            try{Thread.sleep(10);}catch(Exception e){}
            System.out.println(Thread.currentThread().getName()+"*****"+sum);
        }

    }
}

那麼既然學習了用同步函數的方法,也可以講之前火車票的例子用同步函數的方法進行優化代碼:

//火車站售票的例子:
//解決多線程安全問題,用同步代碼塊:
//通過上一個案例的學習,我們可以用到同步函數:

class Ticket implements Runnable
{
    private int count =1000;
    public void run()
    {
        while (true)
        {
            show();
        }

    }
    public synchronized void show()
    {
        if (count>0)
                {
                    try
                    {
                        Thread.sleep(10);
                    }
                    catch (InterruptedException e)
                    {

                    }

                    System.out.println(Thread.currentThread().getName()+"---"+count--);
                }
    }
}
public class TicketDemo {
    public static void main(String[] args) 
    {

        Ticket c = new Ticket();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        Thread t3 = new Thread(c);
        Thread t4 = new Thread(c);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

那麼問題也就來了,同步代碼塊變成同步函數之後,那個用來當鎖的對象沒有了,那麼同步函數用到的是什麼鎖呢?

函數需要被調用,那麼函數都有一個所屬對象引用,就是this
所以同步函數使用的鎖是this,

說到這裏回顧一下單例設計模式中的懶漢式,因爲懶漢式中也用到了同步

//單例設計模式

/*
餓漢式

class Single
{
    private static final Single s = new Single();
    private Single(){}
    public static Single getInstance()
    {
        return s;
    }
}
*/


/*
懶漢式

class Single
{
    private static Single s =null;
    private Single(){}
    public static Single getInstance()
    {
        if(s==null)
        {
            syschronized(Single.class)
            {
                if(s==null)
                {
                    s=new Single();
                }
            }
        }
        return s;
    }
}

注意:

  • 懶漢式和餓漢式有什麼區別:懶漢式的特點在於實例的延遲加載;
  • 懶漢式延遲加載有沒有問題?有,多線程執行的時候會有安全問題。
  • 怎麼解決?用同步的方式。
  • 加同步有哪些方法?用同步代碼塊和同步函數的方法,但是稍微有一些低效,可以使用雙重判斷的方法來稍微的提高效率。
  • 加同步的時候使用的鎖是哪一個?該類所屬的字節碼文件。(不是this,因爲靜態中不可能有this)

死鎖

什麼是死鎖?
就像是兩個人都只有一隻筷子,但是誰都不給誰自己的筷子,這樣兩個人就吃不上飯了,這就是發生了死鎖。
死鎖的產生原因?
同步發生了嵌套,就是鎖裏邊還有鎖。
死鎖事例代碼:

//死鎖演示:就是鎖裏邊嵌套鎖



class Test implements Runnable
{
    private boolean flag;
    Test(boolean flag)//給要創建的線程對象先初始化一個布爾型的值
    {
        this.flag = flag;
    }
    public void run()
    {
        if (flag)//如果爲真,先進a鎖,再進b鎖
        {
            while(true)
            {
                synchronized(MyLock.a)
                {
                    System.out.println("locka");
                    synchronized(MyLock.b)
                    {
                        System.out.println("lockb");
                    }
                }
            }
        }
        else//如果爲假,先進b鎖,再進a鎖
        {
            while(true)
            {
                synchronized(MyLock.b)
                {
                    System.out.println("lockb");
                    synchronized(MyLock.a)
                    {
                        System.out.println("locka");
                    }
                }   
            }
        }
    }
}

class MyLock
{
    static Object a = new Object();
    static Object b = new Object();
}

//如果線程0進了a鎖,CPU切換,同時
//線程1進了b鎖,CPU切換,這時線程0重新奪回執行權的時候,想要b鎖,但是b鎖被線程1佔用着,
//所以兩邊都不放資源,程序無法進行下去,形成死鎖。

public class DeadLockTest {
    public static void main(String[] args) 
    {
        Thread t1 = new Thread(new Test(true));
        Thread t2 = new Thread(new Test(false));
        t1.start();
        t2.start();
    }
}

運行結果:
死鎖實例

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