程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> java的線程機制(一) 創建線程

java的線程機制(一) 創建線程

編輯:關於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,如果我們向它提交多個任務,它們就會按照提交的順序排隊, 直到上一個任務執行完畢,因為它們就只有一個線程可以運行。這種方式是為了防止競爭,因為任何時刻都只 有一個任務在運行,從而不需要同步共享資源。

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved