一步一步掌握java的線程機制(一)----創建線程


      現在將1年前寫的有關線程的文章再重新看了一遍,發現過去的自己還是照本宣科,畢竟是剛學java的人,就想將java的精髓之一---線程進制掌握到手,還是有點難度。等到自己已經是編程一年級生了,還是無法將線程這個高級的概念完全貫通,所以,現在趁着自己還在校,盡量的掌握多點有關線程機制的知識。

      我們以一個簡單的例子開始下手:

public class SwingTypeTester extends JFrame implements CharacterSource{
    protected RandomCharacterGenerator producer;
    private CharacterDisplayCanvas displayCanvas;
    private CharacterDisplayCanvas feedbackCanvas;
    private JButton quitButton;
    private JButton startButton;
    private CharacterEventHandler handler;

    public SwingTypeTester() {
        initComponents();
    }

    private void initComponents() {
        handler = new CharacterEventHandler();
        displayCanvas = new CharacterDisplayCanvas();
        feedbackCanvas = new CharacterDisplayCanvas();
        quitButton = new JButton();
        startButton = new JButton();
        add(displayCanvas, BorderLayout.NORTH);
        add(feedbackCanvas, BorderLayout.CENTER);
        JPanel p = new JPanel();
        startButton.setLabel("Start");
        quitButton.setLabel("Quit");
        p.add(startButton);
        p.add(quitButton);
        
        add(p, BorderLayout.SOUTH);
        addWindowListener(new WindowAdapter(){
            public void windowClosing(WindowEvent evt){
                quit();
            }
        });
        
        feedbackCanvas.addKeyListener(new KeyAdapter(){
            public void keyPressed(KeyEvent ke){
                char c = ke.getKeyChar();
                if(c != KeyEvent.CHAR_UNDEFINED){
                    newCharacter((int)c);
                }
            }
        });
        
        startButton.addActionListener(new ActionListener() {
            
            @Override
            public void actionPerformed(ActionEvent arg0) {
                 producer = new RandomCharacterGenerator();
                 displayCanvas.setCharacterSource(producer);
                 producer.start();
                 startButton.setEnabled(false);
                 feedbackCanvas.setEnabled(true);
                 feedbackCanvas.requestFocus();
            }
        });
        
        quitButton.addActionListener(new ActionListener() {
            
            @Override
            public void actionPerformed(ActionEvent e) {
                 quit();
            }
        });
        pack();
    }
    
    private void quit(){
        System.exit(0);
    }
    
    public void addCharacterListener(CharacterListener cl){
        handler.addCharacterListener(cl);
    }
    
    public void removeCharacterListener(CharacterListener cl){
        handler.removeCharacterListener(cl);
    }
    
    public void newCharacter(int c){
        handler.fireNewCharacter(this,  c);
    }
    
    public void nextCharacter(){
        throw new IllegalStateException("We don't produce on demand");
    }
    
    public static void main(String[] args){
        new SwingTypeTester().show();
    }
}

      

       這是一個java的Swing小例子,就是每隔一段時間就會顯示一個隨機的字母或者數字。具體的源碼我會放在后面,現在只是對其中涉及到線程的部分進行重點講解。

      使用到線程的地方就只有那個顯示下一個字母或者數字的功能,它需要在前一個字母或者數字在顯示一段時間后顯示出來,並且它的產生是不斷進行的,除非我們按下停止按鈕。這是需要一個線程不斷在運行的:

public class RandomCharacterGenerator extends Thread implements CharacterSource {
    static char[] chars;
    static String charArray = "abcdefghijklmnopqrstuvwxyz0123456789";
    static {
        chars = charArray.toCharArray();
    }

    Random random;
    CharacterEventHandler handler;

    public RandomCharacterGenerator() {
        random = new Random();
        handler = new CharacterEventHandler();
    }

    public int getPauseTime() {
        return (int) (Math.max(1000, 5000 * random.nextDouble()));
    }

    @Override
    public void addCharacterListener(CharacterListener cl) {
        handler.addCharacterListener(cl);
    }

    @Override
    public void removeCharacterListener(CharacterListener cl) {
        handler.removeCharacterListener(cl);
    }

    @Override
    public void nextCharacter() {
        handler.fireNewCharacter(this,
                (int) chars[random.nextInt(chars.length)]);
    }

    public void run() {
        for (;;) {
            nextCharacter();
            try {
                Thread.sleep(getPauseTime());
            } catch (InterruptedException ie) {
                return;
            }
        }
    }
}

       雖然方法多,但是這個類只有一個方法run()方法是值得我們注意的。
       開啟線程的方式是非常簡單的,只要聲明一個Thread,然后在適當的時候start就行。創建Thread的方式可以像是這樣,創建一個Thread的子類,然后實現它的run()方法,在run()方法中進行該線程的主要工作。當然,我們也可以在需要線程的地方才創建一個Thread,但是這里的情況就是我們的Thread類還需要實現其他接口(當然,這個設計並不好,但我們會以這個例子的逐步完善工作,將一些線程的基本知識融會進去)。

       要想明白線程機制,我們還是得從一些基本內容的概念下手。感謝一年前的我,雖然文章寫得不咋樣,但是作為一個勤奮的記錄員,還是將一些基本知識都記錄下來,省得我去找。

       線程和進程是兩個完全不同的概念,進程是運行在自己的地址空間內的自包容的程序,而線程是在進程中的一個單一的順序控制流,因此,單個進程可以擁有多個線程。

       還有一個抽象的概念,就是任務和線程的區別。線程似乎是進程內的一個任務,但實際上在概念上兩者並不一樣。准確點講,任務是由執行線程來驅動的,而任務是附着在線程上的。

       現在正式講講線程的創建。

       正如我們前面講的,任務是由執行線程驅動的,沒有附着任務的線程根本就不能說是線程,所以我們在創建線程的時候,將任務附着到線程上。所謂的任務,對應的就是Runnable,我們要在這個類中編寫相應的run()方法來描述這個任務所要執行的命令,接着就是將任務附着到線程上。像是這樣:

Thread thread = new Thread(new Runnable(){
       @Override
       public void run(){
          ...
       }
});

      接着我們只要通過start()啟動該Thread就行。
      如果我們在main()方法中啟動線程,我們就會發現,就算線程還沒有執行完畢,剩下的代碼還是會被運行。這是因為我們的main()方法也是一個線程,我們可以在main線程中啟動一個線程。所以,任何線程都可以開啟另一個線程。

      這樣的話,問題也就來了:如果一個程序中開啟了多個線程,那么,它們的執行順序是怎樣的,畢竟程序的內存空間是有限的,不可能允許無限多個線程同時進行。事實就是,它們是交替進行的,而且還是我們無法控制的,是由線程調度器控制的,而且每次執行的順序都是不一樣的!

      這就是多線程最大的問題,如果我們的程序設計不好,在這樣的情況下,就很容易出現問題,而且是我們所無法把握的問題。

      另一種方式就是上面使用的:創建一個Thread的子類,然后實現run()方法,接着同樣是通過start()來開啟它。

     這兩種方式到底應該采取哪種好呢?如果不想類的管理太麻煩,建議還是采取第一種方式,而且這也是我們在大部分的情況下所采用的,它充分使用了java的匿名內部類,但如果還想我們的Thread能夠體現出其他行為而不單單只是個執行任務的線程,那么可以采取第二種方式,這樣我們可以通過實現接口的方式讓Thread具有更多的功能,但是必須注意,Thread的子類只能承載一個任務,但是第一種方式卻可以非常自由的根據需要創建相應的任務。

     除了上面兩種方法,java還提供了第三種方法:Executor(執行器)。

     Executor會在客戶端和任務之間提供一個間接層,由這個間接層來執行任務,並且允許管理異步任務的執行,而無需通過顯式的管理線程的生命周期。

ExecutorService exec = Executors.newCachedThreadPool();
exec.executor(new RunnableClass);

      其中,CachedThreadPool是一種線程池。線程池在多線程處理技術中是一個非常重要的概念,它會將任務添加到隊列中,然后在創建線程后自動啟動這些任務。線程池的線程都是后台線程,每個線程都是用默認的堆棧大小,以默認的優先級運行,並處於多線程單元中。線程池中的線程數目是有一個最大值,但這並不意味着只能運行這樣多的線程,它的真正意思是同時能夠運行的最大線程數目,所以可以等待其他線程運行完畢后再啟動。

      線程池都有一個線程池管理器,用於創建和管理線程池,還有一個工作線程,也就是線程池中的線程。我們必須提供給線程池中的工作線程一個任務,這些任務都是實現了一個任務接口,也就是Runnable。線程池還有一個重要的組成:任務隊列,用於存放沒有處理的任務,這就是一種緩沖機制。

      通過線程池的介紹,我們可以知道,使用到線程池的情況就是這樣:需要大量的線程來完成任務,並且完成任務的時間比較短,就像是我們現在的服務器,同時間接受多個請求並且處理這些請求。

      java除了上面的CachedThreadPool,還有另一種線程池:FixedThreadPool。CachedThreadPool會在執行過程中創建與所需數量相同的線程,然后在它回收舊線程的時候停止創建新的線程,也就是說,它每次都要保證同時運行的線程的數量不能超過所規定的最大數目。而FixedThreadPool是一次性的預先分配所要執行的線程,像是這樣:

ExecutorService exec = Executors.newFixedThreadPool(5);

      就是無論要分配的線程的數目是多少,都是運行5個線程。這樣的好處是非常明顯的,就是用於限制線程的數目。CachedThreadPool是按需分配線程,直到有的線程被回收,也就是出現空閑的時候才會停止創建新的線程,這個過程對於內存來說,代價是非常高昂的,因為我們不知道實際上需要創建的線程數量是多少,只會一直不斷創建新線程。

      看上去似乎FixedThreadPool比起CachedThreadPool更加好用,但實際上使用更多的是CachedThreadPool,因為一般情況下,無論是什么線程池,現有線程都有可能會被自動復用,而CachedThreadPool在線程結束的時候就會停止創建新的線程,也就是說,它能確保結束掉的線程的確是結束掉了,不會被重新啟動,而FixedThreadPool無法保證這點。

      接下來我們可以看看使用上面兩種線程池的簡單例子:

public void main(String[] args){
     ExecutorService cachedExec = Executors.newCachedThreadPool();
     for(int i = 0; i < 5; i++){
        cachedExec.execute(new RunnableClass);
     }
     cachedExec.shutdown();

     ExecutorService fixedExec = Executors.newFixedThreadPool(3);
     for(int i = 0; i < 5; i++){
         fixedExec.execute(new RunnableClass);
     }
     fixedExec.shutdown();
}

       CachedThreadPool會不斷創建線程直到有線程空閑下來為止,而FixedThreadPool會用3個線程來執行5個任務。
       在java中,還有一種執行線程的模式:SingleThreadExecutor。顧名思義,該執行器只有一個線程。它就相當於數量為1的FixedThreadPool,如果我們向它提交多個任務,它們就會按照提交的順序排隊,直到上一個任務執行完畢,因為它們就只有一個線程可以運行。這種方式是為了防止競爭,因為任何時刻都只有一個任務在運行,從而不需要同步共享資源。

       競爭是線程機制中一個非常重要的現象,有關於它的解決貫穿了整個線程機制的發展,而且可怕的是,就算是合理的解決方案,也無法保證我們已經完全避免了這個問題,因為無法預知的錯誤仍然存在於不遠的將來。

    

  

      

     

     


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM