19、多線程

 在日常生活中,很多事情都是同時進行的。例如,人可以同時進行呼吸、血液循環、思考問題等活動。在使用計算機的過程中,應用程序也可以同時運行,用戶可以使用計算機一邊聽歌,一邊打遊戲。在應用程序中,不同的程序塊也是可以同時運行的,這種多個程序塊同時運行的現象被稱做併發執行。
 多線程就是指一個應用程序中有多條併發執行的線索,每條線索都被稱作一個線程,它們會交替執行,彼此之間可以進行通信。
1.1 進程
 什麼是進程?
  在一個操作系統中,每個獨立執行的程序都可被稱爲一個進程,也就是"正在運行的程序"。目前大部分計算機上安裝的都是多任務操作系統,即能同時執行多個應用程序。
1.2線程
 通過上面的學習可以知道,每個運行的程序都是一個進程,在一個進程中還可以有多個執行單元同時運行,這些執行單元可以看作程序執行的一條條線索,被稱爲線程。操作系統中的每一個進程中都至少存在一個線程。當一個Java程序啓動時,就會產生一個進程,該進程會默認創建一個線程,在這個線程上會運行main()方法中的代碼。
 在以前的程序學習中,代碼都是按照調用順序依次往下執行,沒有出現兩段程序代碼交替運行的效果,這樣的程序稱作單線程程序。如果希望程序中實現多段程序代碼交替運行的效果,則需要創建多個線程,即多線程程序。多線程程序在運行時,每個線程之間都是獨立的,它們可以併發執行。

  如圖所示的多條線程,看似是同時執行的,其實不然,它們和進程一樣,也是由CPU輪流執行的,只不過CPU運行速度很快,故而給人同時執行的感覺。


2.線程的創建
 在Java程序中如何實現多線程?
  Java提供了兩種多線程實現方式,一種是繼承java.lang包下的Thread類,覆寫Thread類的run()方法,在run()方法中實現運行在線程上的代碼;另一種是實現java.lang.Runnable接口,同樣是在run()方法中實現運行在線程上的代碼。
2.1繼承Thread類創建多線程
 public class Test1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread(); //創建線程MyThread的線程對象
        myThread.start(); //開啓線程
        while (true) { //通過死循環語句打印輸出
            System.out.println("main()方法在運行");
            
        }
    }
}
class MyThread extends Thread{
    public void run() {
        while (true) { //通過死循環語句打印輸出
            System.out.println("MyThread類的run()方法在運行");
        }
    }
}
通過繼承Thread類,並且重寫Thread類中的run()方法便可實現多線程。在Thread類中,提供了一個start()方法用於啓動新線程,線程啓動後,系統會自動調用run()方法。


運行結果:
main()方法在運行
main()方法在運行
main()方法在運行
main()方法在運行
main()方法在運行
main()方法在運行
main()方法在運行
main()方法在運行
MyThread類的run()方法在運行
MyThread類的run()方法在運行
MyThread類的run()方法在運行
MyThread類的run()方法在運行
MyThread類的run()方法在運行
MyThread類的run()方法在運行


2.2實現Runnable接口創建多線程
 上面通過繼承Thread類實現了多線程,但是這種方式有一定的侷限性。因爲Java中只支持單繼承,一個類一旦繼承了某個父類就無法再繼承Thread類。
 爲了克服這種弊端,Thread類提供了另外一個構造方法Thread(Runnable target),其中Runnable是一個接口,它只有一個run()方法。當通過Thread(Runnable target)構造方法創建線程對象時,只需爲該方法傳遞一個實現了Runnable接口的實例對象,這樣創建的線程將調用實現了Runnable接口中的run()方法作爲運行代碼,而不需要調用Thread類中的run()方法。
public class Test1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread(); //創建MyThrad的實例對象
        Thread thread = new Thread(myThread); //創建線程對象
        thread.start(); //開啓線程,執行線程中的run()方法
        while (true) {
            System.out.println("main方法在運行");
        }
    }
}
class MyThread implements Runnable{


    public void run() { //線程的代碼塊,當調用start()方法時,線程從此處開始執行
        while (true) {
            System.out.println("MyThread類的run()方法在運行");
        }
    }
    
}
運行結果:
main方法在運行
main方法在運行
main方法在運行
main方法在運行
main方法在運行
MyThread類的run()方法在運行
MyThread類的run()方法在運行
3.多線程同步
 3.1線程安全
public class TestSP2 {
    public static void main(String[] args) {
        SaleThread saleThread = new SaleThread(); //創建SaleThread實例對象saleThread
        new Thread(saleThread,"線程 1").start(); //創建線程對象並命名爲線程 1,開啓線程
        new Thread(saleThread,"線程 2").start(); //創建線程對象並命名爲線程 2,開啓線程
        new Thread(saleThread,"線程 3").start(); //創建線程對象並命名爲線程 3,開啓線程
        new Thread(saleThread,"線程 4").start(); //創建線程對象並命名爲線程 4,開啓線程
    }
}
class SaleThread implements Runnable {
    private int tickets = 10; //10張票
    public void run() {
        while (tickets>0) { 
            
                try {
                    Thread.sleep(10); //經過此處的線程休眠10毫秒
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"---賣出的票"+tickets--);
            }
        }
    
}
運行結果:
線程 4---賣出的票10
線程 3---賣出的票10
線程 1---賣出的票9
線程 2---賣出的票8
線程 1---賣出的票7
線程 4---賣出的票6
線程 3---賣出的票7
線程 2---賣出的票5
線程 4---賣出的票4
線程 1---賣出的票3
線程 3---賣出的票4
線程 2---賣出的票2
線程 3---賣出的票1
線程 1---賣出的票1
線程 4---賣出的票0
線程 2---賣出的票-1


 最後幾行打印售出的票爲0和負數,這種現象是不應該出現的,因爲在售票程序中做了判斷,只有當票號大於0時纔會進行售票。運行結果中之所以出現了負數的票號是因爲多線程在售票時出現了安全問題。接下來對問題進行簡單的分析。
 在售票成績程序的while循環中添加了sleep()方法,這樣就模擬了售票過程中線程的延遲。由於線程有延遲,當票號減爲1時,假設線程1此時出售1號票,對票號進行判斷後,進入while循環,在售票之前通過sleep()方法讓線程休眠,這時線程2會進行售票,由於此時票號仍爲1,因此線程2也會進入循環,同理,四個線程都會進入while循環,休眠結束後,四個線程都會進行售票,這樣就相當於將票號減了四次,結果中出現了0、-1、-2這樣的票號。
 3.2同步代碼塊
 通過上面的學習瞭解到,線程安全問題其實就是由多個線程同時處理共享資源所導致的。要想解決上述代碼中的問題,必須保證下面用於處理共享資源的代碼在任何時刻只能有一個線程訪問。
 爲了實現這種限制,Java中提供了同步機制。當多個線程使用同一個共享資源時,可以將處理共享資源的代碼放置在一個代碼塊中,使用synchronized關鍵字來修飾,被稱作同步代碼塊,其語法格式如下:
  synchronized (lock) {
    操作共享資源代碼塊
  }
 上面的代碼中,lock是一個鎖對象,它是同步代碼塊的關鍵。當線程執行同步代碼塊是,首先會檢查鎖對象的標誌位,默認情況下標誌位爲1,此時線程會執行同步代碼塊,同時將鎖對象的標誌位置爲0.當一個新的線程執行到這段同步代碼塊是,由於鎖對象的標誌位爲0,新線程會發生阻塞,等待當前線程執行完同步代碼塊後,鎖對象的標誌位被置爲1,新線程才能激怒同步代碼塊執行其中的代碼。循環往復,直到共享資源被處理完爲止。這個過程就好比一個公用電話亭,只有前一個打完電話出來後,後面的人纔可以打。
 public class TestSP3 {
    public static void main(String[] args) {
        Ticket1 ticket = new Ticket1(); //創建Ticket1實例對象ticket
        new Thread(ticket,"線程 1").start(); //創建線程對象並命名爲線程 1,開啓線程
        new Thread(ticket,"線程 2").start(); //創建線程對象並命名爲線程 2,開啓線程
        new Thread(ticket,"線程 3").start(); //創建線程對象並命名爲線程 3,開啓線程
        new Thread(ticket,"線程 4").start(); //創建線程對象並命名爲線程 4,開啓線程
    }
}
class Ticket1 implements Runnable {
    private int tickets = 10; //10張票
    Object lock = new Object(); //定義任意一個對象,用作同步代碼塊的鎖
    public void run() {
        while (true) { 
            synchronized (lock) {
                try {
                    Thread.sleep(10); //經過此處的線程休眠10毫秒
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                if (tickets>0) {
                    System.out.println(Thread.currentThread().getName()+"---賣出的票"+tickets--);
                } else {
                    break;
                }
                
            }
        }
    }
}
運行結果:
線程 1---賣出的票10
線程 1---賣出的票9
線程 4---賣出的票8
線程 4---賣出的票7
線程 4---賣出的票6
線程 4---賣出的票5
線程 4---賣出的票4
線程 4---賣出的票3
線程 4---賣出的票2
線程 4---賣出的票1
上訴的代碼將有關tickets變量的操作全部都放到同步代碼塊中。爲了保證線程的持續執行,將同步代碼塊放在死循環中,直到ticket<0時跳出循環。因此,從運行結果中可以看出,售出的票不在出現0和負數的情況,這是因爲售票的代碼實現了同步,之前出現的線程安全問題得以解決。運行結果中並沒有出現 線程2,3 售票的語句,出現這樣的現象是很正常的,這是因爲線程在獲得鎖對象時有一定的隨機性,在整個程序的運行期間,線程2,3 始終未獲得鎖對象。
3.3同步方法
 通過3.2的學習,瞭解到同步代碼塊可以有效的解決線程的安全問題,當把共享資源的操作放在synvhronized定義的區域內時,便爲這些操作加了同步鎖。在方法前面同樣可以使用synchronized關鍵字來修飾,被修飾的方法爲同步方法,它能實現同步代碼塊同樣的功能,具體語法格式如下:
  synchronized 返回值類型 方法名 ([參數1,.....]){}


  public class TestSP4 {
    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2(); //創建Ticket1對象
        //創建並開啓四個線程
        new Thread(ticket,"線程一").start();
        new Thread(ticket,"線程二").start();
        new Thread(ticket,"線程三").start();
        new Thread(ticket,"線程四").start();
    }
}


class Ticket2 implements Runnable {
    private int tickets = 10; // 10張票


    public void run() {
        while (true) {
            saleTicket(); //調用售票方法
            if (tickets <= 0) {
                break;
            }
        }
    }


    private synchronized void saleTicket() {
        if(tickets>0){
            try {
                Thread.sleep(10); //經過此處的線程休眠10毫秒
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
                System.out.println(Thread.currentThread().getName()+"---賣出的票"+tickets--);
        }
    }
}
運行結果:
線程一---賣出的票10
線程一---賣出的票9
線程一---賣出的票8
線程一---賣出的票7
線程一---賣出的票6
線程一---賣出的票5
線程一---賣出的票4
線程一---賣出的票3
線程一---賣出的票2
線程一---賣出的票1
4.多線程通信
 4.1問題引入
 public class Storage1 {
    //數據存儲數組
    private int[] cells = new int[10];
    
    //inPos表示存入時數組下標,outPos表示取出時數組下標
    private int inPos,outPos;
    
    //定義一個put()方法向數組中存入數據
    public void put(int num){
        cells[inPos]=num;
        System.out.println("在cells["+inPos+"]中放入數據---"+cells[inPos]);
        inPos++; //存完元素讓位置加1
        if(inPos==cells.length)
            inPos=0; //當inPos爲數組長度時,將其置爲0
    }
    
    //定義一個get() 方法從數組中取出數據
    public void get() {
        int data = cells[outPos];
        System.out.println("從celss["+outPos+"]中取出數據"+data);
        outPos++; //取完元素讓位置加1
        if(outPos==cells.length)
            outPos=0;
    }
}


class Input1 implements Runnable{ //輸入線程類


    private Storage1 st;
    private int num; //定義一個變量num
    
    Input1(Storage1 st) { //通過構造方法接受一個Storage對象
        this.st = st;
    }
    public void run() {
        while(true){
            st.put(num++); //將num存入數組,每次存入後num自增
        }
    }


}


class Output1 implements Runnable{ //輸出線程類


    private Storage1 st;
    private int num; //定義一個變量num
    
    Output1(Storage1 st) { //通過構造方法接受一個Storage對象
        this.st = st;
    }
    public void run() {
        while(true){
            st.get(); //循環取出元素
        }
    }


}


public class TestStor1 {    
    public static void main(String[] args) {
        Storage1 st = new Storage1(); //創建數據存儲類對象
        Input1 input = new Input1(st); //創建Input對象傳入Storage1對象
        Output1 output = new Output1(st); //創建Output對象傳入Storage1對象
        
        new Thread(input).start(); //開啓新線程
        new Thread(output).start(); //開啓新線程
    }
}
運行結果:
在cells[6]中放入數據---25706
在cells[7]中放入數據---25707
從celss[4]中取出數據25594
從celss[5]中取出數據25705
從celss[6]中取出數據25706
 其中特殊標記的兩行運行結果表示在取出數字25594後,緊接着取出的是25705,這樣的現象明顯是不對的。我們希望出現的運行結果是依次取出遞增的自然數。之所以出現這種現象是因爲在Input線程存入數字25595時,Output線程並沒有及時取出數據,Input線程一直在持續地存入數據,直到將數組放滿,又從數組的第一位置開始存入25700,25701,25702,25703,25704,25705...,當Output線程再次取出數據時,取出的不再是25595而是25705。
 4.2問題如何解決
  想解決上述問題,就需要控制多個線程按照一定的順序輪流執行,此時需要讓線程間進行通信。在Object類中提供了wait()、notify()、notifyAll()、方法用於解決線程間的通信問題,由於Java中所有類都是Object類的子類或間接子類,因此任何類的實例對象都可以直接使用這些方法。
 
 public class Storage2 {
    // 數據存儲數組
    private int[] cells = new int[10];


    // inPos表示存入時數組下標,outPos表示取出時數組下標
    private int inPos, outPos;
    private int count;


    public synchronized void put(int num) {
        try {
            //如果放入數據等於cells的長度,此線程等待
            while (count == cells.length) {
                this.wait();
            }
            cells[inPos] = num; //向數組中放入數據
            System.out.println("在cells[" + inPos + "]中放入數據---" + cells[inPos]);
            inPos++; // 存完元素讓位置加1
            if (inPos == cells.length)
                inPos = 0; // 當inPos爲數組長度時,將其置爲0
            count++; //每放一個數據count加1
            this.notify();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }


    }


    public synchronized void get() {
        try {
            //如果count爲0,此線程等待
            while (count == 0) {
                this.wait();
            }
            int data=cells[outPos]; //向數組中取出數據
            System.out.println("在Cells[" + outPos + "]中取出數據---" + data);
            cells[outPos]=0; //取出後,當前位置的數據置0
            outPos++;
            if (outPos == cells.length)
                outPos = 0; // 當inPos爲數組長度時,將其置爲0
            count--; //每取出一個數據count減1
            this.notify();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }


    }
}


運行結果:
在Cells[5]中取出數據---41625
在Cells[6]中取出數據---41626
在Cells[7]中取出數據---41627
在Cells[8]中取出數據---41628
在Cells[9]中取出數據---41629
在Cells[0]中取出數據---41630
在cells[1]中放入數據---41631
在cells[2]中放入數據---41632
在cells[3]中放入數據---41633
在cells[4]中放入數據---41634
在cells[5]中放入數據---41635
在cells[6]中放入數據---41636
在cells[7]中放入數據---41637
在cells[8]中放入數據---41638
在cells[9]中放入數據---41639
在cells[0]中放入數據---41640
在Cells[1]中取出數據---41631
在Cells[2]中取出數據---41632
在Cells[3]中取出數據---41633
在Cells[4]中取出數據---41634
在Cells[5]中取出數據---41635
在Cells[6]中取出數據---41636
在Cells[7]中取出數據---41637
在Cells[8]中取出數據---41638
在Cells[9]中取出數據---41639
在Cells[0]中取出數據---41640
在cells[1]中放入數據---41641
在cells[2]中放入數據---41642
在cells[3]中放入數據---41643
在cells[4]中放入數據---41644
在cells[5]中放入數據---41645
在cells[6]中放入數據---41646
在cells[7]中放入數據---41647
在cells[8]中放入數據---41648
在cells[9]中放入數據---41649
在cells[0]中放入數據---41650
在Cells[1]中取出數據---41641
在Cells[2]中取出數據---41642
在Cells[3]中取出數據---41643
在Cells[4]中取出數據---41644
在Cells[5]中取出數據---41645
在Cells[6]中取出數據---41646
在Cells[7]中取出數據---41647
在Cells[8]中取出數據---41648
在Cells[9]中取出數據---41649
在Cells[0]中取出數據---41650
在cells[1]中放入數據---41651
在cells[2]中放入數據---41652
在cells[3]中放入數據---41653
在cells[4]中放入數據---41654
在cells[5]中放入數據---41655
在cells[6]中放入數據---41656
在cells[7]中放入數據---41657
在cells[8]中放入數據---41658
在cells[9]中放入數據---41659
在cells[0]中放入數據---41660
在Cells[1]中取出數據---41651
在Cells[2]中取出數據---41652
在Cells[3]中取出數據---41653
在Cells[4]中取出數據---41654
在Cells[5]中取出數據---41655
在Cells[6]中取出數據---41656
在Cells[7]中取出數據---41657
在Cells[8]中取出數據---41658
在Cells[9]中取出數據---41659
在Cells[0]中取出數據---41660
在cells[1]中放入數據---41661
在cells[2]中放入數據---41662
在cells[3]中放入數據---41663
在cells[4]中放入數據---41664
在cells[5]中放入數據---41665
在cells[6]中放入數據---41666
在cells[7]中放入數據---41667
在cells[8]中放入數據---41668
在cells[9]中放入數據---41669


首先通過使用synchronized關鍵字將put()方法和get()方法修飾爲同步方法,之後每操作一次數據,便調用一次notify()方法喚醒對應同步鎖上等待的線程。當存入數據時,如果count的值與cells數組的長度相同,說明數組已經添滿,此時就需要調用同步鎖的wait()方法使存入數據的線程進入等待狀態。同理,當取出數據時如果count的值爲0,說明數組已被取空,此時就需要調用同步鎖的wait()方法,使取出數據的線程進入等待狀態。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章