現在將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,如果我們向它提交多個任務,它們就會按照提交的順序排隊, 直到上一個任務執行完畢,因為它們就只有一個線程可以運行。這種方式是為了防止競爭,因為任何時刻都只 有一個任務在運行,從而不需要同步共享資源。
競爭是線程機制中一個非常重要的現象,有關於它的 解決貫穿了整個線程機制的發展,而且可怕的是,就算是合理的解決方案,也無法保證我們已經完全避免了這 個問題,因為無法預知的錯誤仍然存在於不遠的將來。