<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                # 14.3 堵塞 一個線程可以有四種狀態: (1) 新(New):線程對象已經創建,但尚未啟動,所以不可運行。 (2) 可運行(Runnable):意味著一旦時間分片機制有空閑的CPU周期提供給一個線程,那個線程便可立即開始運行。因此,線程可能在、也可能不在運行當中,但一旦條件許可,沒有什么能阻止它的運行——它既沒有“死”掉,也未被“堵塞”。 (3) 死(Dead):從自己的`run()`方法中返回后,一個線程便已“死”掉。亦可調用`stop()`令其死掉,但會產生一個異常——屬于`Error`的一個子類(也就是說,我們通常不捕獲它)。記住一個異常的“拋”出應當是一個特殊事件,而不是正常程序運行的一部分。所以不建議你使用`stop()`(在Java 1.2則是堅決反對)。另外還有一個`destroy()`方法(它永遠不會實現),應該盡可能地避免調用它,因為它非常武斷,根本不會解除對象的鎖定。 (4) 堵塞(Blocked):線程可以運行,但有某種東西阻礙了它。若線程處于堵塞狀態,調度機制可以簡單地跳過它,不給它分配任何CPU時間。除非線程再次進入“可運行”狀態,否則不會采取任何操作。 ## 14.3.1 為何會堵塞 堵塞狀態是前述四種狀態中最有趣的,值得我們作進一步的探討。線程被堵塞可能是由下述五方面的原因造成的: (1) 調用`sleep(毫秒數)`,使線程進入“睡眠”狀態。在規定的時間內,這個線程是不會運行的。 (2) 用`suspend()`暫停了線程的執行。除非線程收到`resume()`消息,否則不會返回“可運行”狀態。 (3) 用`wait()`暫停了線程的執行。除非線程收到`nofify()`或者`notifyAll()`消息,否則不會變成“可運行”(是的,這看起來同原因2非常相象,但有一個明顯的區別是我們馬上要揭示的)。 (4) 線程正在等候一些IO(輸入輸出)操作完成。 (5) 線程試圖調用另一個對象的“同步”方法,但那個對象處于鎖定狀態,暫時無法使用。 亦可調用`yield()`(`Thread`類的一個方法)自動放棄CPU,以便其他線程能夠運行。然而,假如調度機制覺得我們的線程已擁有足夠的時間,并跳轉到另一個線程,就會發生同樣的事情。也就是說,沒有什么能防止調度機制重新啟動我們的線程。線程被堵塞后,便有一些原因造成它不能繼續運行。 下面這個例子展示了進入堵塞狀態的全部五種途徑。它們全都存在于名為`Blocking.java`的一個文件中,但在這兒采用散落的片斷進行解釋(大家可注意到片斷前后的`Continued`以及`Continuing`標志。利用第17章介紹的工具,可將這些片斷連結到一起)。首先讓我們看看基本的框架: ``` //: Blocking.java // Demonstrates the various ways a thread // can be blocked. import java.awt.*; import java.awt.event.*; import java.applet.*; import java.io.*; //////////// The basic framework /////////// class Blockable extends Thread { private Peeker peeker; protected TextField state = new TextField(40); protected int i; public Blockable(Container c) { c.add(state); peeker = new Peeker(this, c); } public synchronized int read() { return i; } protected synchronized void update() { state.setText(getClass().getName() + " state: i = " + i); } public void stopPeeker() { // peeker.stop(); Deprecated in Java 1.2 peeker.terminate(); // The preferred approach } } class Peeker extends Thread { private Blockable b; private int session; private TextField status = new TextField(40); private boolean stop = false; public Peeker(Blockable b, Container c) { c.add(status); this.b = b; start(); } public void terminate() { stop = true; } public void run() { while (!stop) { status.setText(b.getClass().getName() + " Peeker " + (++session) + "; value = " + b.read()); try { sleep(100); } catch (InterruptedException e){} } } } ///:Continued ``` `Blockable`類打算成為本例所有類的一個基類。一個`Blockable`對象包含了一個名為`state`的`TextField`(文本字段),用于顯示出對象有關的信息。用于顯示這些信息的方法叫作`update()`。我們發現它用`getClass.getName()`來產生類名,而不是僅僅把它打印出來;這是由于`update(0)`不知道自己為其調用的那個類的準確名字,因為那個類是從`Blockable`派生出來的。 在`Blockable`中,變動指示符是一個`int i`;派生類的`run()`方法會為其自增。 針對每個`Bloackable`對象,都會啟動`Peeker`類的一個線程。`Peeker`的任務是調用`read()`方法,檢查與自己關聯的`Blockable`對象,看看i是否發生了變化,最后用它的`status`文本字段報告檢查結果。注意`read()`和`update()`都是同步的,要求對象的鎖定能自由解除,這一點非常重要。 (1) 睡眠 這個程序的第一項測試是用`sleep()`作出的: ``` ///:Continuing ///////////// Blocking via sleep() /////////// class Sleeper1 extends Blockable { public Sleeper1(Container c) { super(c); } public synchronized void run() { while(true) { i++; update(); try { sleep(1000); } catch (InterruptedException e){} } } } class Sleeper2 extends Blockable { public Sleeper2(Container c) { super(c); } public void run() { while(true) { change(); try { sleep(1000); } catch (InterruptedException e){} } } public synchronized void change() { i++; update(); } } ///:Continued ``` 在`Sleeper1`中,整個`run()`方法都是同步的。我們可看到與這個對象關聯在一起的`Peeker`可以正常運行,直到我們啟動線程為止,隨后`Peeker`便會完全停止。這正是“堵塞”的一種形式:因為`Sleeper1.run()`是同步的,而且一旦線程啟動,它就肯定在`run()`內部,方法永遠不會放棄對象鎖定,造成`Peeker`線程的堵塞。 `Sleeper2`通過設置不同步的運行,提供了一種解決方案。只有`change()`方法才是同步的,所以盡管`run()`位于`sleep()`內部,`Peeker`仍然能訪問自己需要的同步方法——`read()`。在這里,我們可看到在啟動了`Sleeper2`線程以后,`Peeker`會持續運行下去。 (2) 暫停和恢復 這個例子接下來的一部分引入了“掛起”或者“暫停”(`Suspend`)的概述。`Thread`類提供了一個名為`suspend()`的方法,可臨時中止線程;以及一個名為`resume()`的方法,用于從暫停處開始恢復線程的執行。顯然,我們可以推斷出`resume()`是由暫停線程外部的某個線程調用的。在這種情況下,需要用到一個名為`Resumer`(恢復器)的獨立類。演示暫停/恢復過程的每個類都有一個相關的恢復器。如下所示: ``` ///:Continuing /////////// Blocking via suspend() /////////// class SuspendResume extends Blockable { public SuspendResume(Container c) { super(c); new Resumer(this); } } class SuspendResume1 extends SuspendResume { public SuspendResume1(Container c) { super(c);} public synchronized void run() { while(true) { i++; update(); suspend(); // Deprecated in Java 1.2 } } } class SuspendResume2 extends SuspendResume { public SuspendResume2(Container c) { super(c);} public void run() { while(true) { change(); suspend(); // Deprecated in Java 1.2 } } public synchronized void change() { i++; update(); } } class Resumer extends Thread { private SuspendResume sr; public Resumer(SuspendResume sr) { this.sr = sr; start(); } public void run() { while(true) { try { sleep(1000); } catch (InterruptedException e){} sr.resume(); // Deprecated in Java 1.2 } } } ///:Continued ``` `SuspendResume1`也提供了一個同步的`run()`方法。同樣地,當我們啟動這個線程以后,就會發現與它關聯的`Peeker`進入“堵塞”狀態,等候對象鎖被釋放,但那永遠不會發生。和往常一樣,這個問題在`SuspendResume2`里得到了解決,它并不同步整個`run()`方法,而是采用了一個單獨的同步`change()`方法。 對于Java 1.2,大家應注意`suspend()`和`resume()`已獲得強烈反對,因為`suspend()`包含了對象鎖,所以極易出現“死鎖”現象。換言之,很容易就會看到許多被鎖住的對象在傻乎乎地等待對方。這會造成整個應用程序的“凝固”。盡管在一些老程序中還能看到它們的蹤跡,但在你寫自己的程序時,無論如何都應避免。本章稍后就會講述正確的方案是什么。 (3) 等待和通知 通過前兩個例子的實踐,我們知道無論`sleep()`還是`suspend()`都不會在自己被調用的時候解除鎖定。需要用到對象鎖時,請務必注意這個問題。在另一方面,`wait()`方法在被調用時卻會解除鎖定,這意味著可在執行`wait()`期間調用線程對象中的其他同步方法。但在接著的兩個類中,我們看到`run()`方法都是“同步”的。在`wait()`期間,`Peeker`仍然擁有對同步方法的完全訪問權限。這是由于`wait()`在掛起內部調用的方法時,會解除對象的鎖定。 我們也可以看到`wait()`的兩種形式。第一種形式采用一個以毫秒為單位的參數,它具有與`sleep()`中相同的含義:暫停這一段規定時間。區別在于在`wait()`中,對象鎖已被解除,而且能夠自由地退出`wait()`,因為一個`notify()`可強行使時間流逝。 第二種形式不采用任何參數,這意味著`wait()`會持續執行,直到`notify()`介入為止。而且在一段時間以后,不會自行中止。 `wait()`和`notify()`比較特別的一個地方是這兩個方法都屬于基類`Object`的一部分,不象`sleep()`,`suspend()`以及`resume()`那樣屬于`Thread`的一部分。盡管這表面看有點兒奇怪——居然讓專門進行線程處理的東西成為通用基類的一部分——但仔細想想又會釋然,因為它們操縱的對象鎖也屬于每個對象的一部分。因此,我們可將一個`wait()`置入任何同步方法內部,無論在那個類里是否準備進行涉及線程的處理。事實上,我們能調用`wait()`的唯一地方是在一個同步的方法或代碼塊內部。若在一個不同步的方法內調用`wait()`或者`notify()`,盡管程序仍然會編譯,但在運行它的時候,就會得到一個`IllegalMonitorStateException`(非法監視器狀態異常),而且會出現多少有點莫名其妙的一條消息:`current thread not owner`(當前線程不是所有人”。注意`sleep()`,`suspend()`以及`resume()`都能在不同步的方法內調用,因為它們不需要對鎖定進行操作。 只能為自己的鎖定調用`wait()`和`notify()`。同樣地,仍然可以編譯那些試圖使用錯誤鎖定的代碼,但和往常一樣會產生同樣的`IllegalMonitorStateException`異常。我們沒辦法用其他人的對象鎖來愚弄系統,但可要求另一個對象執行相應的操作,對它自己的鎖進行操作。所以一種做法是創建一個同步方法,令其為自己的對象調用`notify()`。但在`Notifier`中,我們會看到一個同步方法內部的`notify()`: ``` synchronized(wn2) { wn2.notify(); } ``` 其中,`wn2`是類型為`WaitNotify2`的對象。盡管并不屬于`WaitNotify2`的一部分,這個方法仍然獲得了`wn2`對象的鎖定。在這個時候,它為`wn2`調用`notify()`是合法的,不會得到`IllegalMonitorStateException`異常。 ``` ///:Continuing /////////// Blocking via wait() /////////// class WaitNotify1 extends Blockable { public WaitNotify1(Container c) { super(c); } public synchronized void run() { while(true) { i++; update(); try { wait(1000); } catch (InterruptedException e){} } } } class WaitNotify2 extends Blockable { public WaitNotify2(Container c) { super(c); new Notifier(this); } public synchronized void run() { while(true) { i++; update(); try { wait(); } catch (InterruptedException e){} } } } class Notifier extends Thread { private WaitNotify2 wn2; public Notifier(WaitNotify2 wn2) { this.wn2 = wn2; start(); } public void run() { while(true) { try { sleep(2000); } catch (InterruptedException e){} synchronized(wn2) { wn2.notify(); } } } } ///:Continued ``` 若必須等候其他某些條件(從線程外部加以控制)發生變化,同時又不想在線程內一直傻乎乎地等下去,一般就需要用到`wait()`。`wait()`允許我們將線程置入“睡眠”狀態,同時又“積極”地等待條件發生改變。而且只有在一個`notify()`或`notifyAll()`發生變化的時候,線程才會被喚醒,并檢查條件是否有變。因此,我們認為它提供了在線程間進行同步的一種手段。 (4) IO堵塞 若一個數據流必須等候一些IO活動,便會自動進入“堵塞”狀態。在本例下面列出的部分中,有兩個類協同通用的`Reader`以及`Writer`對象工作(使用Java 1.1的流)。但在測試模型中,會設置一個管道化的數據流,使兩個線程相互間能安全地傳遞數據(這正是使用管道流的目的)。 `Sender`將數據置入`Writer`,并“睡眠”隨機長短的時間。然而,`Receiver`本身并沒有包括`sleep()`,`suspend()`或者`wait()`方法。但在執行`read()`的時候,如果沒有數據存在,它會自動進入“堵塞”狀態。如下所示: ``` ///:Continuing class Sender extends Blockable { // send private Writer out; public Sender(Container c, Writer out) { super(c); this.out = out; } public void run() { while(true) { for(char c = 'A'; c <= 'z'; c++) { try { i++; out.write(c); state.setText("Sender sent: " + (char)c); sleep((int)(3000 * Math.random())); } catch (InterruptedException e){} catch (IOException e) {} } } } } class Receiver extends Blockable { private Reader in; public Receiver(Container c, Reader in) { super(c); this.in = in; } public void run() { try { while(true) { i++; // Show peeker it's alive // Blocks until characters are there: state.setText("Receiver read: " + (char)in.read()); } } catch(IOException e) { e.printStackTrace();} } } ///:Continued ``` 這兩個類也將信息送入自己的`state`字段,并修改`i`值,使`Peeker`知道線程仍在運行。 (5) 測試 令人驚訝的是,主要的程序片(Applet)類非常簡單,這是大多數工作都已置入`Blockable`框架的緣故。大概地說,我們創建了一個由`Blockable`對象構成的數組。而且由于每個對象都是一個線程,所以在按下`"start"`按鈕后,它們會采取自己的行動。還有另一個按鈕和`actionPerformed()`從句,用于中止所有`Peeker`對象。由于Java 1.2“反對”使用`Thread`的`stop()`方法,所以可考慮采用這種折衷形式的中止方式。 為了在`Sender`和`Receiver`之間建立一個連接,我們創建了一個`PipedWriter`和一個`PipedReader`。注意`PipedReader in`必須通過一個構造器參數同`PipedWriterout`連接起來。在那以后,我們在`out`內放進去的所有東西都可從`in`中提取出來——似乎那些東西是通過一個“管道”傳輸過去的。隨后將`in`和`out`對象分別傳遞給`Receiver`和`Sender`構造器;后者將它們當作任意類型的`Reader`和`Writer`看待(也就是說,它們被“上溯”轉換了)。 `Blockable`引用`b`的數組在定義之初并未得到初始化,因為管道化的數據流是不可在定義前設置好的(對`try`塊的需要將成為障礙): ``` ///:Continuing /////////// Testing Everything /////////// public class Blocking extends Applet { private Button start = new Button("Start"), stopPeekers = new Button("Stop Peekers"); private boolean started = false; private Blockable[] b; private PipedWriter out; private PipedReader in; public void init() { out = new PipedWriter(); try { in = new PipedReader(out); } catch(IOException e) {} b = new Blockable[] { new Sleeper1(this), new Sleeper2(this), new SuspendResume1(this), new SuspendResume2(this), new WaitNotify1(this), new WaitNotify2(this), new Sender(this, out), new Receiver(this, in) }; start.addActionListener(new StartL()); add(start); stopPeekers.addActionListener( new StopPeekersL()); add(stopPeekers); } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(!started) { started = true; for(int i = 0; i < b.length; i++) b[i].start(); } } } class StopPeekersL implements ActionListener { public void actionPerformed(ActionEvent e) { // Demonstration of the preferred // alternative to Thread.stop(): for(int i = 0; i < b.length; i++) b[i].stopPeeker(); } } public static void main(String[] args) { Blocking applet = new Blocking(); Frame aFrame = new Frame("Blocking"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(350,550); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~ ``` 在`init()`中,注意循環會遍歷整個數組,并為頁添加`state`和`peeker.status`文本字段。 首次創建好`Blockable`線程以后,每個這樣的線程都會自動創建并啟動自己的`Peeker`。所以我們會看到各個`Peeker`都在`Blockable`線程啟動之前運行起來。這一點非常重要,因為在`Blockable`線程啟動的時候,部分`Peeker`會被堵塞,并停止運行。弄懂這一點,將有助于我們加深對“堵塞”這一概念的認識。 ## 14.3.2 死鎖 由于線程可能進入堵塞狀態,而且由于對象可能擁有“同步”方法——除非同步鎖定被解除,否則線程不能訪問那個對象——所以一個線程完全可能等候另一個對象,而另一個對象又在等候下一個對象,以此類推。這個“等候”鏈最可怕的情形就是進入封閉狀態——最后那個對象等候的是第一個對象!此時,所有線程都會陷入無休止的相互等待狀態,大家都動彈不得。我們將這種情況稱為“死鎖”。盡管這種情況并非經常出現,但一旦碰到,程序的調試將變得異常艱難。 就語言本身來說,尚未直接提供防止死鎖的幫助措施,需要我們通過謹慎的設計來避免。如果有誰需要調試一個死鎖的程序,他是沒有任何竅門可用的。 (1) Java 1.2對`stop()`,`suspend()`,`resume()`以及`destroy()`的反對 為減少出現死鎖的可能,Java 1.2作出的一項貢獻是“反對”使用`Thread`的`stop()`,`suspend()`,`resume()`以及`destroy()`方法。 之所以反對使用`stop()`,是因為它不安全。它會解除由線程獲取的所有鎖定,而且如果對象處于一種不連貫狀態(“被析構”),那么其他線程能在那種狀態下檢查和修改它們。結果便造成了一種微妙的局面,我們很難檢查出真正的問題所在。所以應盡量避免使用`stop()`,應該采用`Blocking.java`那樣的方法,用一個標志告訴線程什么時候通過退出自己的`run()`方法來中止自己的執行。 如果一個線程被堵塞,比如在它等候輸入的時候,那么一般都不能象在`Blocking.java`中那樣輪詢一個標志。但在這些情況下,我們仍然不該使用`stop()`,而應換用由`Thread`提供的`interrupt()`方法,以便中止并退出堵塞的代碼。 ``` //: Interrupt.java // The alternative approach to using stop() // when a thread is blocked import java.awt.*; import java.awt.event.*; import java.applet.*; class Blocked extends Thread { public synchronized void run() { try { wait(); // Blocks } catch(InterruptedException e) { System.out.println("InterruptedException"); } System.out.println("Exiting run()"); } } public class Interrupt extends Applet { private Button interrupt = new Button("Interrupt"); private Blocked blocked = new Blocked(); public void init() { add(interrupt); interrupt.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("Button pressed"); if(blocked == null) return; Thread remove = blocked; blocked = null; // to release it remove.interrupt(); } }); blocked.start(); } public static void main(String[] args) { Interrupt applet = new Interrupt(); Frame aFrame = new Frame("Interrupt"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(200,100); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~ ``` `Blocked.run()`內部的`wait()`會產生堵塞的線程。當我們按下按鈕以后,`blocked`(堵塞)的引用就會設為`null`,使垃圾收集器能夠將其清除,然后調用對象的`interrupt()`方法。如果是首次按下按鈕,我們會看到線程正常退出。但在沒有可供“殺死”的線程以后,看到的便只是按鈕被按下而已。 `suspend()`和`resume()`方法天生容易發生死鎖。調用`suspend()`的時候,目標線程會停下來,但卻仍然持有在這之前獲得的鎖定。此時,其他任何線程都不能訪問鎖定的資源,除非被“掛起”的線程恢復運行。對任何線程來說,如果它們想恢復目標線程,同時又試圖使用任何一個鎖定的資源,就會造成令人難堪的死鎖。所以我們不應該使用`suspend()`和`resume()`,而應在自己的`Thread`類中置入一個標志,指出線程應該活動還是掛起。若標志指出線程應該掛起,便用`wait()`命其進入等待狀態。若標志指出線程應當恢復,則用一個`notify()`重新啟動線程。我們可以修改前面的`Counter2.java`來實際體驗一番。盡管兩個版本的效果是差不多的,但大家會注意到代碼的組織結構發生了很大的變化——為所有“聽眾”都使用了匿名的內部類,而且`Thread`是一個內部類。這使得程序的編寫稍微方便一些,因為它取消了`Counter2.java`中一些額外的記錄工作。 ``` //: Suspend.java // The alternative approach to using suspend() // and resume(), which have been deprecated // in Java 1.2. import java.awt.*; import java.awt.event.*; import java.applet.*; public class Suspend extends Applet { private TextField t = new TextField(10); private Button suspend = new Button("Suspend"), resume = new Button("Resume"); class Suspendable extends Thread { private int count = 0; private boolean suspended = false; public Suspendable() { start(); } public void fauxSuspend() { suspended = true; } public synchronized void fauxResume() { suspended = false; notify(); } public void run() { while (true) { try { sleep(100); synchronized(this) { while(suspended) wait(); } } catch (InterruptedException e){} t.setText(Integer.toString(count++)); } } } private Suspendable ss = new Suspendable(); public void init() { add(t); suspend.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { ss.fauxSuspend(); } }); add(suspend); resume.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { ss.fauxResume(); } }); add(resume); } public static void main(String[] args) { Suspend applet = new Suspend(); Frame aFrame = new Frame("Suspend"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e){ System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(300,100); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~ ``` `Suspendable`中的`suspended`(已掛起)標志用于開關“掛起”或者“暫停”狀態。為掛起一個線程,只需調用`fauxSuspend(`)將標志設為`true`(真)即可。對標志狀態的偵測是在`run()`內進行的。就象本章早些時候提到的那樣,`wait()`必須設為“同步”(`synchronized`),使其能夠使用對象鎖。在`fauxResume()`中,`suspended`標志被設為`false`(假),并調用`notify()`——由于這會在一個“同步”從句中喚醒`wait()`,所以`fauxResume()`方法也必須同步,使其能在調用`notify()`之前取得對象鎖(這樣一來,對象鎖可由要喚醍的那個`wait()`使用)。如果遵照本程序展示的樣式,可以避免使用`wait()`和`notify()`。 `Thread`的`destroy()`方法根本沒有實現;它類似一個根本不能恢復的`suspend()`,所以會發生與`suspend()`一樣的死鎖問題。然而,這一方法沒有得到明確的“反對”,也許會在Java以后的版本(1.2版以后)實現,用于一些可以承受死鎖危險的特殊場合。 大家可能會奇怪當初為什么要實現這些現在又被“反對”的方法。之所以會出現這種情況,大概是由于Sun公司主要讓技術人員來決定對語言的改動,而不是那些市場銷售人員。通常,技術人員比搞銷售的更能理解語言的實質。當初犯下了錯誤以后,也能較為理智地正視它們。這意味著Java能夠繼續進步,即便這使Java程序員多少感到有些不便。就我自己來說,寧愿面對這些不便之處,也不愿看到語言停滯不前。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看