乒乓球基本技術動作口訣
|
一、頭訣 |
1、 特點 球速急、落點長、沖力大,發至對方右大角或中左位置,對對方威脅較大。
2、 要點①拋球不宜太高;②提高擊球瞬間的揮拍速度;③第一落點要靠近本方臺面的端線;④擊球點與網同高或稍低于網。
(二)反手發急球與發急下旋球
1、 特點 球速快、弧線低,前沖大,迫使對方后退接球,有利于搶攻,常與發急下旋球配合使用。
2、 要點①擊球點應在身體的左前側與網同高或比網稍低;②注意手腕的抖動發力;③第一落點在本方臺區的端線附近。
(三)發短球
1、 特點 擊球動作小,出手快,球落到對方臺面后的第二跳下不出臺,使對方不易發力搶拉、沖或搶攻。
2、 要點 ①拋球不宜太高;②擊球時,手腕的力量大于前臂的力量;③發球的第一落點在球臺中區,不要離網太近;④發球動作盡可能與發長球相似,使對方不易判斷。
(四)正手發轉與不轉球
1、 特點 球速較慢,前沖力小,主要用相似的發球動作,制造旋轉變化去迷惑對方,造成對方接發球失誤或為自己搶攻創造機會。
2、 要點①拋球不宜太高;②發轉球時,拍面稍后抑,切球的中下部;越是加轉球,越應注意手臂的前送動作;③發不轉球時,擊球瞬間減小拍面后仰角度,增加前推的力量。
(五)正手發左側上(下)旋球
1、 特點 左側上(下)旋轉力較強,對方擋球時向其右側上(下)方反彈,一般站在中線偏左或側身發球。
2、 要點:①發球時要收腹,擊球點不可遠離身體;②盡量加大由右向左揮動的幅度和弧線,以增強側旋強度。③發左側上旋時,擊球瞬間手腕快速內收,球拍從球的正中向左上方摩擦。④發左側下旋時,拍面稍后仰,球拍從球的中下部向左下方摩擦。
(六) 反手發右側上(下)旋球
1. 特點 右側上(下)旋球力強,對方擋住后,向其左側上(下)反彈。發球落點以左方斜線長球配合中右近網短球為佳。
2. 要點 ①注意收腹和轉腰動作;②充分利用手腕轉動配合前臂發力;③發右側上旋球時,擊球瞬間球拍從球的中部向右上方摩擦,手腕有一個上勾動作;④發右側下旋球時,拍面稍后仰,擊球瞬間球拍從球的中下部向右側下摩擦。
(七)下蹲發球
1.特點 下蹲發球屬于上手類發球,我國運動員早在50年代就開始使用。橫拍選手發下蹲球比直拍選手方便些,直拍選手發球時需變化握拍方法,即將食指移放到球拍的背面。下蹲發球可以發出左側旋和右側旋,在對方不適應的情況下,威脅很大,關鍵時候發出高質量的球,往往能直接得分。
2. 要點①注意拋球和揮拍擊球動作的配合,掌握好擊球時間。②發球要有質量,發球動作要利落,以防在還未完全站起時已被對方搶攻③發下蹲右側上、下旋球時,左腳稍前,身體略向右偏轉,揮拍路線為從左后方向右前方。拍觸球中部向右側上摩擦為右側上旋;從球中下部向右側下摩擦為右側下旋。④發下蹲左側上、下旋球時,站位稍平,身體基本正對球臺,揮拍路線為從右后方向左前方。拍觸球右中部向左上方摩擦為左側上旋;從球中部向左下部摩擦為左側下旋。⑤發左(右)側上、下旋球時,要特別注意快速做半圓形摩擦球的動作。
(八)正手高拋發球
1、 特點 最顯著的特點是拋球高,增大了球下降時對拍的正壓力,發出的球速度快,沖力大,旋轉變化多,著臺后拐彎飛行。但高拋發球動作復雜,有一定的難度。
2、 要點:①拋球勿離臺及身體太遠。②擊球點與網同高或比網稍低,在近腰的中右處(15厘米)為好③盡量加大向內擺動的幅度和弧線。④發左側上、下旋球與低拋發球同。⑤觸球后,附加一個向右前方的回收動作,可增加對方的判斷(結合發右側旋球,更有威力)。
仔細觀察鮑羅斯的反手拉球,大致上主要用于以下三種情況:一是反手位的第一板上手,二是正手變反手位時,三是反手位的機會比較好時。而這三種球也恰恰是中國運動員,特別是水平不高的運動員常感到很別扭的球。
中國隊的打法歷來講究積極主動,體現在打法上的最明顯特征就是突出正手單面進攻的使用率和殺傷力。曾有一段時間,歐洲人在比較清楚地把握了中國人的慣性套路后,以接發球“晃撇”反手底線長球和“調右壓左”,從正手突破的戰術,一度使中國隊陷入非常被動的境地,常常令中國隊“撥”不出手來,難以形成有效的正手單面攻,造成失利。所以,無論是橫板快攻還是弧圈打法,在訓練過程中,掌握一板比較高質量的反手拉是非常有必要的,可以為發揮正手的殺傷力創造更多的機會。
談橫板反手拉球,不能不說一說它的基本要領。常有業余愛好者問,正手拉球時,一腳在前,一腳在后,那么反手拉球時,是否需要進行重心交換呢?可以說,其基本的道理是差不多的。但反手拉球由于受到身體解剖結構的限制,不能像正手拉球時過于側向的站位,一般要接近于兩腳的平行,只要能夠完成基本的重心交換即可。在拉球引拍時,也不能像正手拉一樣有很大的幅度,適當地把肘關節抬起來一點,以增加手臂的發力距離,這是與正手拉本質的區別;在接觸球的一瞬間,重心往上撥,盡可能向前發力,以使球體現足夠的向前質量。
在練習和運用反手拉球過程中,要注意,作為具有中國人特點打法的運動員,其進攻和上手的指導思想都應建立在正手的基礎上,再運用反手,千萬不能形成左來左打,右來右打的打法風格,有失積極意義,畢竟正手和反手的殺傷力是不同的,這也是中國隊在長期的實踐中摸索的打法風格的成功經驗之一。但作為輔助技術手段,鮑羅斯反手拉球的運用和所產生的效果是值得我們借鑒和學習的。
可以擴展到一切的業務對象。
包括基礎方法,操作。
甚至安全對象本身
有點神學的味道。
可能是因為采用其他被創造者不知道的機制,所以被創造者無法理解和知曉。
您認為把 NIO 和 Servlet API 組合在一起是不可能的?請再好好想一下。在本文中,Java 開發人員 Taylor Cowan 向您展示了如何把生產者/消費者模型應用到消費者非阻塞 I/O,從而輕松地讓 Servlet API 全新地兼容 NIO。在這個過程中,您將會看到采用了什么來創建實際的基于 Servlet 并實現了 NIO 的 Web 服務器;您也將發現在企業環境中,那個服務器是如何以標準的 Java I/O 服務器(Tomcat 5.0)為基礎而創建的。
NIO 是帶有 JDK 1.4 的 Java 平臺的最有名(如果不是最出色的)的添加部分之一。下面的許多文章闡述了 NIO 的基本知識及如何利用非阻塞通道的好處。但它們所遺漏的一件事正是,沒有充分地展示 NIO 如何可以提高 J2EE Web 層的可伸縮性。對于企業開發人員來說,這些信息特別密切相關,因為實現 NIO 不像把少數幾個 import 語句改變成一個新的 I/O 包那樣簡單。首先,Servlet API 采用阻塞 I/O 語義,因此默認情況下,它不能利用非阻塞 I/O。其次,不像 JDK 1.0 中那樣,線程不再是“資源獨占”(resource hog),因此使用較少的線程不一定表明服務器可以處理更多的客戶機。
在本文中,為了創建基于 Servlet 并實現了 NIO 的 Web 服務器,您將學習如何解決 Servlet API 與非阻塞 I/O 的不配合問題。我們將會看到在多元的 Web 服務器環境中,這個服務器是如何針對標準 I/O 服務器(Tomcat 5.0)進行伸縮的。為符合企業中生存期的事實,我們將重點放在當保持 socket 連接的客戶機數量以指數級增長時,NIO 與標準 I/O 相比較的情況如何。
注意,本文針對某些 Java 開發人員,他們已經熟悉了 Java 平臺上 I/O 編程的基礎知識。有關非阻塞 I/O 的介紹,請參閱 參考資料 部分。
大家都知道,線程是比較昂貴的。在 Java 平臺的早期(JDK 1.0),線程的開銷是一個很大負擔,因此強制開發人員自定義生成解決方案。一個常見的解決方案是使用 VM 啟動時創建的線程池,而不是按需創建每個新線程。盡管最近在 VM 層上提高了線程的性能,但標準 I/O 仍然要求分配惟一的線程來處理每個新打開的 socket。就短期而言,這工作得相當不錯,但當線程的數量增加超過了 1K,標準 I/O 的不足就表現出來了。由于要在線程間進行上下文切換,因此 CPU 簡直變成了超載。
由于 JDK 1.4 中引入了 NIO,企業開發人員最終有了“單線程”模型的一個內置解決方案:多元 I/O 使得固定數量的線程可以服務不斷增長的用戶數量。
多路復用(Multiplexing)指的是通過一個載波來同時發送多個信號或流。當使用手機時,日常的多路復用例子就發生了。無線頻率是稀有的資源,因此無線頻率提供商使用多路復用技術通過一個頻率發送多個呼叫。在一個例子中,把呼叫分成一些段,然后給這些段很短的持續時間,并在接收端重新裝配。這就叫做 時分多路復用(time-division multiplexing),即 TDM。
在 NIO 中,接收端相當于“選擇器”(參閱 java.nio.channels.Selector
)。不是處理呼叫,選擇器是處理多個打開的 socket。就像在 TDM 中那樣,選擇器重新裝配從多個客戶機寫入的數據段。這使得服務器可以用單個線程管理多個客戶機。
![]() ![]() |
![]()
|
對于 NIO,非阻塞讀寫是必要的,但它們并不是完全沒有麻煩。除了不會阻塞之外,非阻塞讀不能給呼叫方任何保證。客戶機或服務器應用程序可能讀取完整信息、部分消息或者根本讀取不到消息。另外,非阻塞讀可能讀取到太多的消息,從而強制為下一個呼叫準備一個額外的緩沖區。最后,不像流那樣,讀取了零字節并不表明已經完全接收了消息。
這些因素使得沒有輪詢就不可能實現甚至是簡單的 readline
方法。所有的 servlet 容器必須在它們的輸入流上提供 readline
方法。因此,許多開發人員放棄了創建基于 Servlet 并實現了 NIO 的 Web 應用程序服務器。不過這里有一個解決方案,它組合了 Servlet API 和 NIO 的多元 I/O 的能力。
在下面的幾節中,您將學習如何使用 java.io.PipedInput
和 PipedOutputStream
類來把生產者/消費者模型應用到消費者非阻塞 I/O。當讀取非阻塞通道時,把它寫到正由第二個線程消費的管道。注意,這種分解映射線程不同于大多數基于 Java 的客戶機/服務器應用程序。這里,我們讓一個線程單獨負責處理非阻塞通道(生產者),讓另一個線程單獨負責把數據作為流消費(消費者)。管道也為應用程序服務器解決了非阻塞 I/O 問題,因為 servlet 在消費 I/O 時將采用阻塞語義。
![]() ![]() |
![]()
|
示例服務器展示了 Servlet API 和 NIO 不兼容的生產者/消費者解決方案。該服務器與 Servlet API 非常相似,可以為成熟的基于 NIO 應用程序服務器提供 POC (proof of concept),是專門編寫來衡量 NIO 相對于標準 Java I/O 的性能的。它處理簡單的 HTTP get
請求,并支持來自客戶機的 Keep-Alive 連接。這是重要的,因為多路復用 I/O 只證明在要求服務器處理大量打開的 scoket 連接時是有意的。
該服務器被分成兩個包: org.sse.server
和 org.sse.http
包中有提供主要 服務器
功能的類,比如如下的一些功能:接收新客戶機連接、閱讀消息和生成工作線程以處理請求。 http
包支持 HTTP 協議的一個子集。詳細闡述 HTTP 超出了本文的范圍。有關實現細節,請從 參考資料 部分下載代碼示例。
現在讓我們來看一下 org.sse.server
包中一些最重要的類。
![]() ![]() |
![]()
|
Server
類擁有多路復用循環 —— 任何基于 NIO 服務器的核心。在清單 1 中,在服務器接收新客戶機或檢測到正把可用的字節寫到打開的 socket 前, select()
的調用阻塞了。這與標準 Java I/O 的主要區別是,所有的數據都是在這個循環中讀取的。通常會把從特定 socket 中讀取字節的任務分配給一個新線程。使用 NIO 選擇器事件驅動方法,實際上可以用單個線程處理成千上萬的客戶機,不過,我們還會在后面看到線程仍有一個角色要扮演。
每個 select()
調用返回一組事件,指出新客戶機可用;新數據準備就緒,可以讀取;或者客戶機準備就緒,可以接收響應。server 的 handleKey()
方法只對新客戶機( key.isAcceptable()
)和傳入數據 ( key.isReadable()
) 感興趣。到這里,工作就結束了,轉入 ServerEventHandler
類。
清單 1. Server.java 選擇器循環
public void listen() { SelectionKey key = null; try { while (true) { selector.select(); Iterator it = selector.selectedKeys().iterator(); while (it.hasNext()) { key = (SelectionKey) it.next(); handleKey(key); it.remove(); } } } catch (IOException e) { key.cancel(); } catch (NullPointerException e) { // NullPointer at sun.nio.ch.WindowsSelectorImpl, Bug: 4729342 e.printStackTrace(); } } |
![]() ![]() |
![]()
|
ServerEventHandler
類響應服務器事件。當新客戶機變為可用時,它就實例化一個新的 Client
對象,該對象代表了那個客戶機的狀態。數據是以非阻塞方式從通道中讀取的,并被寫到 Client
對象中。 ServerEventHandler
對象也維護請求隊列。為了處理(消費)隊列中的請求,生成了不定數量的工作線程。在傳統的生產者/消費者方式下,為了在隊列變為空時線程會阻塞,并在新請求可用時線程會得到通知,需要寫 Queue
。
為了支持等待的線程,在清單 2 中已經重寫了 remove()
方法。如果列表為空,就會增加等待線程的數量,并阻塞當前線程。它實質上提供了非常簡單的線程池。
清單 2. Queue.java
public class Queue extends LinkedList { private int waitingThreads = 0; public synchronized void insert(Object obj) { addLast(obj); notify(); } public synchronized Object remove() { if ( isEmpty() ) { try { waitingThreads++; wait();} catch (InterruptedException e) {Thread.interrupted();} waitingThreads--; } return removeFirst(); } public boolean isEmpty() { return (size() - waitingThreads <= 0); } } |
工作線程的數量與 Web 客戶機的數量無關。不是為每個打開的 socket 分配一個線程,相反,我們把所有請求放到一個由一組 RequestHandlerThread
實例所服務的通用隊列中。理想情況下,線程的數量應該根據處理器的數量和請求的長度或持續時間進行調整。如果請求通過資源或處理需求花了很長時間,那么通過添加更多的線程,可以提高感知到的服務質量。
注意,這不一定提高整體的吞吐量,但確實改善了用戶體驗。即使在超載的情況下,也會給每個線程一個處理時間片。這一原則同樣適用于基于標準 Java I/O 的服務器;不過這些服務器是受到限制的,因為會 要求 它們為每個打開的 socket 連接分配一個線程。NIO 服務器完全不用擔心這一點,因此它們可以擴展到大量用戶。最后的結果是 NIO 服務器仍然需要線程,只是不需要那么多。
![]() ![]() |
![]()
|
Client
類有兩個用途。首先,通過把傳入的非阻塞 I/O 轉換成可由 Servlet API 消費的阻塞 InputStream
,它解決了阻塞/非阻塞問題。其次,它管理特定客戶機的請求狀態。因為當全部讀取消息時,非阻塞通道沒有給出任何提示,所以強制我們在協議層處理這一情況。 Client
類在任意指定的時刻都指出了它是否正在參與進行中的請求。如果它準備處理新請求, write()
方法就會為請求處理而將該客戶機排到隊列中。如果它已經參與了請求,它就只是使用 PipedInputStream
和 PipedOutputStream
類把傳入的字節轉換成一個 InputStream
。
圖 1 展示了兩個線程圍繞管道進行交互。主線程把從通道讀取的數據寫到管道中。管道把相同的數據作為 InputStream
提供給消費者。管道的另一個重要特性是:它是進行緩沖處理的。如果沒有進行緩沖處理,主線程在嘗試寫到管道時就會阻塞。因為主線程單獨負責所有客戶機間的多路復用,因此我們不能讓它阻塞。
圖 1. PipedInput/OutputStream

在 Client
自己排隊后,工作線程就可以消費它了。 RequestHandlerThread
類承擔了這個角色。至此,我們已經看到主線程是如何連續地循環的,它要么接受新客戶機,要么讀取新的 I/O。工作線程循環等待新請求。當客戶機在請求隊列上變為可用時,它就馬上被 remove()
方法中阻塞的第一個等待線程所消費。
清單 3. RequestHandlerThread.java
public void run() { while (true) { Client client = (Client) myQueue.remove(); try { for (; ; ) { HttpRequest req = new HttpRequest(client.clientInputStream, myServletContext); HttpResponse res = new HttpResponse(client.key); defaultServlet.service(req, res); if (client.notifyRequestDone()) break; } } catch (Exception e) { client.key.cancel(); client.key.selector().wakeup(); } } } |
然后該線程創建新的 HttpRequest
和 HttpResponse
實例,并調用 defaultServlet
的 service 方法。注意, HttpRequest
是用 Client
對象的 clientInputStream
屬性構造的。 PipedInputStream
就是負責把非阻塞 I/O 轉換成阻塞流。
從現在開始,請求處理就與您在 J2EE Servlet API 中期望的相似。當對 servlet 的調用返回時,工作線程在返回到池中之前,會檢查是否有來自相同客戶機的另一個請求可用。注意,這里用到了單詞 池 (pool)。事實上,線程會對隊列嘗試另一個 remove()
調用,并變成阻塞,直到下一個請求可用。
![]() ![]() |
![]()
|
示例服務器實現了 HTTP 1.1 協議的一個子集。它處理普通的 HTTP get
請求。它帶有兩個命令行參數。第一個指定端口號,第二個指定 HTML 文件所駐留的目錄。在解壓文件后, 切換到項目目錄,然后執行下面的命令,注意要把下面的 webroot 目錄替換為您自己的目錄:
java -cp bin org.sse.server.Start 8080 "C:\mywebroot" |
還請注意,服務器并沒有實現目錄清單,因此必須指定有效的 URL 來指向您的 webroot 目錄下的文件。
![]() ![]() |
![]()
|
示例 NIO 服務器是在重負載下與 Tomcat 5.0 進行比較的。選擇 Tomcat 是因為它是基于標準 Java I/O 的純 Java 解決方案。為了提高可伸縮性,一些高級的應用程序服務器是用 JNI 本機代碼優化的,因此它們沒有提供標準 I/O 和 NIO 之間的很好比較。目標是要確定 NIO 是否給出了大量的性能優勢,以及是在什么條件下給出的。
如下是一些說明:
- Tomcat 是用最大的線程數量 2000 來配置的,而示例服務器只允許用 4 個工作線程運行。
- 每個服務器是針對相同的一組簡單 HTTP
get
測試的,這些 HTTPget
基本上由文本內容組成。
- 把加載工具(Microsoft Web Application Stress Tool)設置為使用“Keep-Alive”會話,導致了大約要為每個用戶分配一個 socket。然后它導致了在 Tomcat 上為每個用戶分配一個線程,而 NIO 服務器用固定數量的線程來處理相同的負載。
圖 2 展示了在不斷增加負載下的“請求/秒”率。在 200 個用戶時,性能是相似的。但當用戶數量超過 600 時,Tomcat 的性能開始急劇下降。這最有可能是由于在這么多的線程間切換上下文的開銷而導致的。相反,基于 NIO 的服務器的性能則以線性方式下降。記住,Tomcat 必須為每個用戶分配一個線程,而 NIO 服務器只配置有 4 個工作線程。
圖 2. 請求/秒

圖 3 進一步顯示了 NIO 的性能。它展示了操作的 Socket 連接錯誤數/分鐘。同樣,在大約 600 個用戶時,Tomcat 的性能急劇下降,而基于 NIO 的服務器的錯誤率保持相對較低。
圖 3. Socket 連接錯誤數/分鐘

![]() ![]() |
![]()
|
在本文中您已經學習了,實際上可以使用 NIO 編寫基于 Servlet 的 Web 服務器,甚至可以啟用它的非阻塞特性。對于企業開發人員來說,這是好消息,因為在企業環境中,NIO 比標準 Java I/O 更能夠進行伸縮。不像標準的 Java I/O,NIO 可以用固定數量的線程處理許多客戶機。當基于 Servlet 的 NIO Web 服務器用來處理保持和擁有 socket 連接的客戶機時,會獲得更好的性能。
- 您可以參閱本文在 developerWorks 全球站點上的 英文原文.
- 下載本文中使用的 源代碼。
- 看看“ Merlin 給 Java 平臺帶來了非阻塞 I/O”( developerWorks,2002 年 3 月),獲得 NIO 語義的進一步知識。
- 綜合的 developerWorks 教程“ NIO 入門”( developerWorks,2003 年 11 月)從高級的概念到底層的編程細節,詳細論及了 NIO 庫。
- Merlin Hughes 的由兩部分組成的文章“ Turning streams inside out”( developerWorks,2002 年 9 月)為 Java I/O(標準版和 NIO 版)的一些普遍的挑戰提供了制作精巧的設計解決方案。
- 為獲取有關 Java I/O 問題的一些背景知識,請參閱 Allen Holub 的“ 關于解決 Java 編程語言線程問題的建議 ”( developerWorks,2000 年 10 月)。
- 訪問 NIO 主頁,從資源中學習非阻塞 I/O。
- JavaNIO.info 是查找有關 NIO 的資源的理想地方。
- 為從書本系統學習 NIO,請參閱該領域的經典著作:Ron Hitchens 的 Java NIO(O'Reilly & Associates,2002 年)。
- 在 developerWorks Java 技術專區可以找到有關 Java 編程各個方面的文章。
- 訪問 Developer Bookstore,獲取技術書籍的完整列表,其中包括數百本 Java 相關的圖書。
- 也請參閱 Java 技術專區教程頁,從 developerWorks 獲取免費的 Java 專門教程的完整列表。
![]() |
||
|
![]() |
Taylor Cowan 是一位軟件工程師,也是一位專攻 J2EE 的自由撰稿人。他從 North Texas 大學的計算機科學專業獲得了碩士學位,另外,他還從 Jazz Arranging 獲得了音樂學士學位。 |
一種是繼承自Thread類.Thread 類是一個具體的類,即不是抽象類,該類封裝了線程的行為。要創建一個線程,程序員必須創建一個從 Thread 類導出的新類。程序員通過覆蓋 Thread 的 run() 函數來完成有用的工作。用戶并不直接調用此函數;而是通過調用 Thread 的 start() 函數,該函數再調用 run()。
例如:
public class Test extends Thread{
public Test(){
}
public static void main(String args[]){
Test t1 = new Test();
Test t2 = new Test();
t1.start();
t2.start();
}
public void run(){
//do thread's things
}
}
另一種是實現Runnable接口,此接口只有一個函數,run(),此函數必須由實現了此接口的類實現。
例如:
public class Test implements Runnable{
Thread thread1;
Thread thread2;
public Test(){
thread1 = new Thread(this,"1");
thread2 = new Thread(this,"2");
}
public static void main(String args[]){
Test t = new Test();
t.startThreads();
}
public void run(){
//do thread's things
}
public void startThreads(){
thread1.start();
thread2.start();
}
}
兩種創建方式看起來差別不大,但是弄不清楚的話,也許會將你的程序弄得一團糟。兩者區別有以下幾點:
1.當你想繼承某一其它類時,你只能用后一種方式.
2.第一種因為繼承自Thread,只創建了自身對象,但是在數量上,需要幾個線程,就得創建幾個自身對象;第二種只創建一個自身對象,卻創建幾個Thread對象.而兩種方法重大的區別就在于此,請你考慮:如果你在第一種里創建數個自身對象并且start()后,你會發現好像synchronized不起作用了,已經加鎖的代碼塊或者方法居然同時可以有幾個線程進去,而且同樣一個變量,居然可以有好幾個線程同時可以去更改它。(例如下面的代碼)這是因為,在這個程序中,雖然你起了數個線程,可是你也創建了數個對象,而且,每個線程對應了每個對象也就是說,每個線程更改和占有的對象都不一樣,所以就出現了同時有幾個線程進入一個方法的現象,其實,那也不是一個方法,而是不同對象的相同的方法。所以,這時候你要加鎖的話,只能將方法或者變量聲明為靜態,將static加上后,你就會發現,線程又能管住方法了,同時不可能有兩個線程進入同樣一個方法,那是因為,現在不是每個對象都擁有一個方法了,而是所有的對象共同擁有一個方法,這個方法就是靜態方法。
而你如果用第二種方法使用線程的話,就不會有上述的情況,因為此時,你只創建了一個自身對象,所以,自身對象的屬性和方法對于線程來說是共有的。
因此,我建議,最好用后一種方法來使用線程。
public class mainThread extends Thread{
int i=0;
public static void main(String args[]){
mainThread m1 = new mainThread();
mainThread m2 = new mainThread();
mainThread m3 = new mainThread();
mainThread m4 = new mainThread();
mainThread m5 = new mainThread();
mainThread m6 = new mainThread();
m1.start();
m2.start();
m3.start();
m4.start();
m5.start();
m6.start();
}
public synchronized void t1(){
i=++i;
try{
Thread.sleep(500);
}
catch(Exception e){}
//每個線程都進入各自的t1()方法,分別打印各自的i
System.out.println(Thread.currentThread().getName()+" "+i);
}
public void run(){
synchronized(this){
while (true) {
t1();
}
}
}
}
下面我們來講synchronized的4種用法吧:
1.方法聲明時使用,放在范圍操作符(public等)之后,返回類型聲明(void等)之前.即一次只能有一個線程進入該方法,其他線程要想在此時調用該方法,只能排隊等候,當前線程(就是在synchronized方法內部的線程)執行完該方法后,別的線程才能進入.
例如:
public synchronized void synMethod() {
//方法體
}
2.對某一代碼塊使用,synchronized后跟括號,括號里是變量,這樣,一次只有一個線程進入該代碼塊.例如:
public int synMethod(int a1){
synchronized(a1) {
//一次只能有一個線程進入
}
}
3.synchronized后面括號里是一對象,此時,線程獲得的是對象鎖.例如:
public class MyThread implements Runnable {
public static void main(String args[]) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt, "t1");
Thread t2 = new Thread(mt, "t2");
Thread t3 = new Thread(mt, "t3");
Thread t4 = new Thread(mt, "t4");
Thread t5 = new Thread(mt, "t5");
Thread t6 = new Thread(mt, "t6");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}public void run() {
synchronized (this) {
System.out.println(Thread.currentThread().getName());
}
}
}
對于3,如果線程進入,則得到對象鎖,那么別的線程在該類所有對象上的任何操作都不能進行.在對象級使用鎖通常是一種比較粗糙的方法。為什么要將整個對象都上鎖,而不允許其他線程短暫地使用對象中其他同步方法來訪問共享資源?如果一個對象擁有多個資源,就不需要只為了讓一個線程使用其中一部分資源,就將所有線程都鎖在外面。由于每個對象都有鎖,可以如下所示使用虛擬對象來上鎖:
class FineGrainLock {
MyMemberClass x, y;
Object xlock = new Object(), ylock = new Object();public void foo() {
synchronized(xlock) {
//access x here
}//do something here - but don't use shared resources
synchronized(ylock) {
//access y here
}
}public void bar() {
synchronized(this) {
//access both x and y here
}
//do something here - but don't use shared resources
}
}
4.synchronized后面括號里是類.例如:
class ArrayWithLockOrder{
private static long num_locks = 0;
private long lock_order;
private int[] arr;public ArrayWithLockOrder(int[] a)
{
arr = a;
synchronized(ArrayWithLockOrder.class) {//-----------------------------------------這里
num_locks++; // 鎖數加 1。
lock_order = num_locks; // 為此對象實例設置唯一的 lock_order。
}
}
public long lockOrder()
{
return lock_order;
}
public int[] array()
{
return arr;
}
}class SomeClass implements Runnable
{
public int sumArrays(ArrayWithLockOrder a1,
ArrayWithLockOrder a2)
{
int value = 0;
ArrayWithLockOrder first = a1; // 保留數組引用的一個
ArrayWithLockOrder last = a2; // 本地副本。
int size = a1.array().length;
if (size == a2.array().length)
{
if (a1.lockOrder() > a2.lockOrder()) // 確定并設置對象的鎖定
{ // 順序。
first = a2;
last = a1;
}
synchronized(first) { // 按正確的順序鎖定對象。
synchronized(last) {
int[] arr1 = a1.array();
int[] arr2 = a2.array();
for (int i=0; i<size; i++)
value += arr1[i] + arr2[i];
}
}
}
return value;
}
public void run() {
//...
}
}
對于4,如果線程進入,則線程在該類中所有操作不能進行,包括靜態變量和靜態方法,實際上,對于含有靜態方法和靜態變量的代碼塊的同步,我們通常用4來加鎖.
以上4種之間的關系:
鎖是和對象相關聯的,每個對象有一把鎖,為了執行synchronized語句,線程必須能夠獲得synchronized語句中表達式指定的對象的鎖,一個對象只有一把鎖,被一個線程獲得之后它就不再擁有這把鎖,線程在執行完synchronized語句后,將獲得鎖交還給對象。
在方法前面加上synchronized修飾符即可以將一個方法聲明為同步化方法。同步化方法在執行之前獲得一個鎖。如果這是一個類方法,那么獲得的鎖是和聲明方法的類相關的Class類對象的鎖。如果這是一個實例方法,那么此鎖是this對象的鎖。
下面談一談一些常用的方法:
wait(),wait(long),notify(),notifyAll()等方法是當前類的實例方法,
wait()是使持有對象鎖的線程釋放鎖;
wait(long)是使持有對象鎖的線程釋放鎖時間為long(毫秒)后,再次獲得鎖,wait()和wait(0)等價;
notify()是喚醒一個正在等待該對象鎖的線程,如果等待的線程不止一個,那么被喚醒的線程由jvm確定;
notifyAll是喚醒所有正在等待該對象鎖的線程.
在這里我也重申一下,我們應該優先使用notifyAll()方法,因為喚醒所有線程比喚醒一個線程更容易讓jvm找到最適合被喚醒的線程.
對于上述方法,只有在當前線程中才能使用,否則報運行時錯誤java.lang.IllegalMonitorStateException: current thread not owner.
下面,我談一下synchronized和wait()、notify()等的關系:
1.有synchronized的地方不一定有wait,notify
2.有wait,notify的地方必有synchronized.這是因為wait和notify不是屬于線程類,而是每一個對象都具有的方法,而且,這兩個方法都和對象鎖有關,有鎖的地方,必有synchronized。
另外,請注意一點:如果要把notify和wait方法放在一起用的話,必須先調用notify后調用wait,因為如果調用完wait,該線程就已經不是current thread了。如下例:
/**
* Title: Jdeveloper's Java Projdect
* Description: n/a
* Copyright: Copyright (c) 2001
* Company: soho http://www.ChinaJavaWorld.com
* @author jdeveloper@21cn.com
* @version 1.0
*/
import java.lang.Runnable;
import java.lang.Thread;
public class DemoThread
implements Runnable {
public DemoThread() {
TestThread testthread1 = new TestThread(this, "1");
TestThread testthread2 = new TestThread(this, "2");
testthread2.start();
testthread1.start();
}
public static void main(String[] args) {
DemoThread demoThread1 = new DemoThread();
}
public void run() {
TestThread t = (TestThread) Thread.currentThread();
try {
if (!t.getName().equalsIgnoreCase("1")) {
synchronized (this) {
wait();
}
}
while (true) {
System.out.println("@time in thread" + t.getName() + "=" +
t.increaseTime());
if (t.getTime() % 10 == 0) {
synchronized (this) {
System.out.println("****************************************");
notify();
if (t.getTime() == 100)
break;
wait();
}
}
}
}
catch (Exception e) {
e.printStackTrace();
}
}
}
class TestThread
extends Thread {
private int time = 0;
public TestThread(Runnable r, String name) {
super(r, name);
}
public int getTime() {
return time;
}
public int increaseTime() {
return++time;
}
}
下面我們用生產者/消費者這個例子來說明他們之間的關系:
public class test {
public static void main(String args[]) {
Semaphore s = new Semaphore(1);
Thread t1 = new Thread(s, "producer1");
Thread t2 = new Thread(s, "producer2");
Thread t3 = new Thread(s, "producer3");
Thread t4 = new Thread(s, "consumer1");
Thread t5 = new Thread(s, "consumer2");
Thread t6 = new Thread(s, "consumer3");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
}class Semaphore
implements Runnable {
private int count;
public Semaphore(int n) {
this.count = n;
}public synchronized void acquire() {
while (count == 0) {
try {
wait();
}
catch (InterruptedException e) {
//keep trying
}
}
count--;
}public synchronized void release() {
while (count == 10) {
try {
wait();
}
catch (InterruptedException e) {
//keep trying
}
}
count++;
notifyAll(); //alert a thread that's blocking on this semaphore
}public void run() {
while (true) {
if (Thread.currentThread().getName().substring(0,8).equalsIgnoreCase("consumer")) {
acquire();
}
else if (Thread.currentThread().getName().substring(0,8).equalsIgnoreCase("producer")) {
release();
}
System.out.println(Thread.currentThread().getName() + " " + count);
}
}
}
生產者生產,消費者消費,一般沒有沖突,但當庫存為0時,消費者要消費是不行的,但當庫存為上限(這里是10)時,生產者也不能生產.請好好研讀上面的程序,你一定會比以前進步很多.
上面的代碼說明了synchronized和wait,notify沒有絕對的關系,在synchronized聲明的方法、代碼塊中,你完全可以不用wait,notify等方法,但是,如果當線程對某一資源存在某種爭用的情況下,你必須適時得將線程放入等待或者喚醒.
2003 年 11 月 24 日
盡管 SSL 阻塞操作――當讀寫數據的時候套接字的訪問被阻塞――與對應的非阻塞方式相比提供了更好的 I/O 錯誤通知,但是非阻塞操作允許調用的線程繼續運行。本文中,作者同時就客戶端和服務器端描述了如何使用Java Secure Socket Extensions (JSSE) 和 Java NIO (新 I/O)庫創建非阻塞的安全連接,并且介紹了創建非阻塞套接字的傳統方法,以及使用JSSE 和 NIO 的一種可選的(必需的)方法。
阻塞,還是非阻塞?這就是問題所在。無論在程序員的頭腦中多么高貴……當然這不是莎士比亞,本文提出了任何程序員在編寫 Internet 客戶程序時都應該考慮的一個重要問題。通信操作應該是阻塞的還是非阻塞的?
許多程序員在使用 Java 語言編寫 Internet 客戶程序時并沒有考慮這個問題,主要是因為在以前只有一種選擇――阻塞通信。但是現在 Java 程序員有了新的選擇,因此我們編寫的每個客戶程序也許都應該考慮一下。
非阻塞通信在 Java 2 SDK 的 1.4 版被引入 Java 語言。如果您曾經使用該版本編過程序,可能會對新的 I/O 庫(NIO)留下了印象。在引入它之前,非阻塞通信只有在實現第三方庫的時候才能使用,而第三方庫常常會給應用程序引入缺陷。
NIO 庫包含了文件、管道以及客戶機和服務器套接字的非阻塞功能。庫中缺少的一個特性是安全的非阻塞套接字連接。在 NIO 或者 JSSE 庫中沒有建立安全的非阻塞通道類,但這并不意味著不能使用安全的非阻塞通信。只不過稍微麻煩一點。
要完全領會本文,您需要熟悉:
- Java 套接字通信的概念。您也應該實際編寫過應用程序。而且不只是打開連接、讀取一行然后退出的簡單應用程序,應該是實現 POP3 或 HTTP 之類協議的客戶機或通信庫這樣的程序。
- SSL 基本概念和加密之類的概念。基本上就是知道如何設置一個安全連接(但不必擔心 JSSE ――這就是關于它的一個“緊急教程”)。
- NIO 庫。
- 在您選擇的平臺上安裝 Java 2 SDK 1.4 或以后的版本。(我是在 Windows 98 上使用 1.4.1_01 版。)
如果需要關于這些技術的介紹,請參閱 參考資料部分。
那么到底什么是阻塞和非阻塞通信呢?
阻塞通信意味著通信方法在嘗試訪問套接字或者讀寫數據時阻塞了對套接字的訪問。在 JDK 1.4 之前,繞過阻塞限制的方法是無限制地使用線程,但這樣常常會造成大量的線程開銷,對系統的性能和可伸縮性產生影響。java.nio 包改變了這種狀況,允許服務器有效地使用 I/O 流,在合理的時間內處理所服務的客戶請求。
沒有非阻塞通信,這個過程就像我所喜歡說的“為所欲為”那樣。基本上,這個過程就是發送和讀取任何能夠發送/讀取的東西。如果沒有可以讀取的東西,它就中止讀操作,做其他的事情直到能夠讀取為止。當發送數據時,該過程將試圖發送所有的數據,但返回實際發送出的內容。可能是全部數據、部分數據或者根本沒有發送數據。
阻塞與非阻塞相比確實有一些優點,特別是遇到錯誤控制問題的時候。在阻塞套接字通信中,如果出現錯誤,該訪問會自動返回標志錯誤的代碼。錯誤可能是由于網絡超時、套接字關閉或者任何類型的 I/O 錯誤造成的。在非阻塞套接字通信中,該方法能夠處理的唯一錯誤是網絡超時。為了檢測使用非阻塞通信的網絡超時,需要編寫稍微多一點的代碼,以確定自從上一次收到數據以來已經多長時間了。
哪種方式更好取決于應用程序。如果使用的是同步通信,如果數據不必在讀取任何數據之前處理的話,阻塞通信更好一些,而非阻塞通信則提供了處理任何已經讀取的數據的機會。而異步通信,如 IRC 和聊天客戶機則要求非阻塞通信以避免凍結套接字。
![]() ![]() |
![]()
|
Java NIO 庫使用通道而非流。通道可同時用于阻塞和非阻塞通信,但創建時默認為非阻塞版本。但是所有的非阻塞通信都要通過一個名字中包含 Channel
的類完成。在套接字通信中使用的類是 SocketChannel,
而創建該類的對象的過程不同于典型的套接字所用的過程,如清單 1 所示。
清單 1. 創建并連接 SocketChannel 對象
SocketChannel sc = SocketChannel.open(); sc.connect("www.ibm.com",80); sc.finishConnect(); |
必須聲明一個 SocketChannel
類型的指針,但是不能使用 new
操作符創建對象。相反,必須調用 SocketChannel
類的一個靜態方法打開通道。打開通道后,可以通過調用 connect()
方法與它連接。但是當該方法返回時,套接字不一定是連接的。為了確保套接字已經連接,必須接著調用 finishConnect()
。
當套接字連接之后,非阻塞通信就可以開始使用 SocketChannel
類的 read()
和 write()
方法了。也可以把該對象強制轉換成單獨的 ReadableByteChannel
和 WritableByteChannel
對象。無論哪種方式,都要對數據使用 Buffer
對象。因為 NIO 庫的使用超出了本文的范圍,我們不再對此進一步討論。
當不再需要套接字時,可以使用 close()
方法將其關閉:
sc.close(); |
這樣就會同時關閉套接字連接和底層的通信通道。
![]() ![]() |
![]()
|
上述方法比傳統的創建套接字連接的例程稍微麻煩一點。不過,傳統的例程也能用于創建非阻塞套接字,不過需要增加幾個步驟以支持非阻塞通信。
SocketChannel
對象中的底層通信包括兩個 Channel
類: ReadableByteChannel
和 WritableByteChannel。
這兩個類可以分別從現有的 InputStream
和 OutputStream
阻塞流中使用
Channels
類的 newChannel()
方法創建,如清單 2 所示:
清單 2. 從流中派生通道
ReadableByteChannel rbc = Channels.newChannel(s.getInputStream()); WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream()); |
Channels
類也用于把通道轉換成流或者 reader 和 writer。這似乎是把通信切換到阻塞模式,但并非如此。如果試圖讀取從通道派生的流,讀方法將拋出 IllegalBlockingModeException
異常。
相反方向的轉換也是如此。不能使用 Channels
類把流轉換成通道而指望進行非阻塞通信。如果試圖讀從流派生的通道,讀仍然是阻塞的。但是像編程中的許多事情一樣,這一規則也有例外。
這種例外適合于實現 SelectableChannel
抽象類的類。 SelectableChannel
和它的派生類能夠選擇使用阻塞或者非阻塞模式。 SocketChannel
就是這樣的一個派生類。
但是,為了能夠在兩者之間來回切換,接口必須作為 SelectableChannel
實現。對于套接字而言,為了實現這種能力必須使用 SocketChannel
而不是 Socket
。
回顧一下,要創建套接字,首先必須像通常使用 Socket
類那樣創建一個套接字。套接字連接之后,使用 清單 2中的兩行代碼把流轉換成通道。
清單 3. 創建套接字的另一種方法
Socket s = new Socket("www.ibm.com", 80); ReadableByteChannel rbc = Channels.newChannel(s.getInputStream()); WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream()); |
如前所述,這樣并不能實現非阻塞套接字通信――所有的通信仍然在阻塞模式下。在這種情況下,非阻塞通信必須模擬實現。模擬層不需要多少代碼。讓我們來看一看。
模擬層在嘗試讀操作之前首先檢查數據的可用性。如果數據可讀則開始讀。如果沒有數據可用,可能是因為套接字被關閉,則返回表示這種情況的代碼。在清單 4 中要注意仍然使用了 ReadableByteChannel
讀,盡管 InputStream
完全可以執行這個動作。為什么這樣做呢?為了造成是 NIO 而不是模擬層執行通信的假象。此外,還可以使模擬層與其他通道更容易結合,比如向文件通道內寫入數據。
清單 4. 模擬非阻塞的讀操作
/* The checkConnection method returns the character read when determining if a connection is open. */ y = checkConnection(); if(y <= 0) return y; buffer.putChar((char ) y); return rbc.read(buffer); |
對于非阻塞通信,寫操作只寫入能夠寫的數據。發送緩沖區的大小和一次可以寫入的數據多少有很大關系。緩沖區的大小可以通過調用 Socket
對象的 getSendBufferSize()
方法確定。在嘗試非阻塞寫操作時必須考慮到這個大小。如果嘗試寫入比緩沖塊更大的數據,必須拆開放到多個非阻塞寫操作中。太大的單個寫操作可能被阻塞。
清單 5. 模擬非阻塞的寫操作
int x, y = s.getSendBufferSize(), z = 0; int expectedWrite; byte [] p = buffer.array(); ByteBuffer buf = ByteBuffer.allocateDirect(y); /* If there isn't any data to write, return, otherwise flush the stream */ if(buffer.remaining() == 0) return 0; os.flush() for(x = 0; x < p.length; x += y) { if(p.length - x < y) { buf.put(p, x, p.length - x); expectedWrite = p.length - x; } else { buf.put(p, x, y); expectedWrite = y; } /* Check the status of the socket to make sure it's still open */ if(!s.isConnected()) break; /* Write the data to the stream, flushing immediately afterward */ buf.flip(); z = wbc.write(buf); os.flush(); if(z < expectedWrite) break; buf.clear(); } if(x > p.length) return p.length; else if(x == 0) return -1; else return x + z; |
與讀操作類似,首先要檢查套接字是否仍然連接。但是如果把數據寫入 WritableByteBuffer
對象,就像清單 5 那樣,該對象將自動進行檢查并在沒有連接時拋出必要的異常。在這個動作之后開始寫數據之前,流必須立即被清空,以保證發送緩沖區中有發送數據的空間。任何寫操作都要這樣做。發送到塊中的數據與發送緩沖區的大小相同。執行清除操作可以保證發送緩沖不會溢出而導致寫操作被阻塞。
因為假定寫操作只能寫入能夠寫的內容,這個過程還必須檢查套接字保證它在每個數據塊寫入后仍然是打開的。如果在寫入數據時套接字被關閉,則必須中止寫操作并返回套接字關閉之前能夠發送的數據量。
BufferedOutputReader
可用于模擬非阻塞寫操作。如果試圖寫入超過緩沖區兩倍長度的數據,則直接寫入緩沖區整倍數長度的數據(緩沖余下的數據)。比如說,如果緩沖區的長度是 256 字節而需要寫入 529 字節的數據,則該對象將清除當前緩沖區、發送 512 字節然后保存剩下的 17 字節。
對于非阻塞寫而言,這并非我們所期望的。我們希望分次把數據寫入同樣大小的緩沖區中,并最終把全部數據都寫完。如果發送的大塊數據留下一些數據被緩沖,那么在所有數據被發送的時候,寫操作就會被阻塞。
整個模擬層可以放到一個類中,以便更容易和應用程序集成。如果要這樣做,我建議從 ByteChannel
派生這個類。這個類可以強制轉換成單獨的 ReadableByteChannel
和 WritableByteChannel
類。
清單 6 給出了從 ByteChannel
派生的模擬層類模板的一個例子。本文后面將一直使用這個類表示通過阻塞連接執行的非阻塞操作。
清單 6. 模擬層的類模板
public class nbChannel implements ByteChannel { Socket s; InputStream is; OutputStream os; ReadableByteChannel rbc; WritableByteChannel wbc; public nbChannel(Socket socket); public int read(ByteBuffer dest); public int write(ByteBuffer src); public void close(); protected int checkConnection(); } |
使用新建的模擬層創建套接字非常簡單。只要像通常那樣創建 Socket
對象,然后創建 nbChannel
對象就可以了,如清單 7 所示:
清單 7. 使用模擬層
Socket s = new Socket("www.ibm.com", 80); nbChannel socketChannel = new nbChannel(s); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel; |
![]() ![]() |
![]()
|
服務器端的非阻塞套接字和客戶端上的沒有很大差別。稍微麻煩一點的只是建立接受輸入連接的套接字。套接字必須通過從服務器套接字通道派生一個阻塞的服務器套接字綁定到阻塞模式。清單 8 列出了需要做的步驟。
清單 8. 創建非阻塞的服務器套接字(SocketChannel)
ServerSocketChannel ssc = ServerSocketChannel.open(); ServerSocket ss = ssc.socket(); ss.bind(new InetSocketAddress(port)); SocketChannel sc = ssc.accept(); |
與客戶機套接字通道相似,服務器套接字通道也必須打開而不是使用 new
操作符或者構造函數。在打開之后,必須派生服務器套接字對象以便把套接字通道綁定到一個端口。一旦套接字被綁定,服務器套接字對象就可以丟棄了。
通道使用 accept()
方法接收到來的連接并把它們轉給套接字通道。一旦接收了到來的連接并轉給套接字通道對象,通信就可以通過 read()
和 write()
方法開始進行了。
![]() ![]() |
![]()
|
實際上,并非真正的替代。因為服務器套接字通道必須使用服務器套接字對象綁定,為何不完全繞開服務器套接字通道而僅使用服務器套接字對象呢?不過這里的通信不使用 SocketChannel
,而要使用模擬層 nbChannel。
清單 9. 建立服務器套接字的另一種方法
ServerSocket ss = new ServerSocket(port); Socket s = ss.accept(); nbChannel socketChannel = new nbChannel(s); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel; |
![]() ![]() |
![]()
|
創建SSL連接,我們要分別從客戶端和服務器端考察。
創建 SS L連接的傳統方法涉及到使用套接字工廠和其他一些東西。我將不會詳細討論如何創建SSL連接,不過有一本很好的教程,“Secure your sockets with JSSE”(請參閱 參考資料),從中您可以了解到更多的信息。
創建 SSL 套接字的默認方法非常簡單,只包括幾個很短的步驟:
- 創建套接字工廠。
- 創建連接的套接字。
- 開始握手。
- 派生流。
- 通信。
清單 10 說明了這些步驟:
清單 10. 創建安全的客戶機套接字
SSLSocketFactory sslFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port); ssl.startHandshake(); InputStream is = ssl.getInputStream(); OutputStream os = ssl.getOutputStream(); |
默認方法不包括客戶驗證、用戶證書和其他特定連接可能需要的東西。
建立SSL服務器連接的傳統方法稍微麻煩一點,需要加上一些類型轉換。因為這些超出了本文的范圍,我將不再進一步介紹,而是說說支持SSL服務器連接的默認方法。
創建默認的 SSL 服務器套接字也包括幾個很短的步驟:
- 創建服務器套接字工廠。
- 創建并綁定服務器套接字。
- 接受傳入的連接。
- 開始握手。
- 派生流。
- 通信。
盡管看起來似乎與客戶端的步驟相似,要注意這里去掉了很多安全選項,比如客戶驗證。
清單 11 說明這些步驟:
清單 11. 創建安全的服務器套接字
SSLServerSocketFactory sslssf = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port); SSLSocket ssls = (SSLSocket)sslss.accept(); ssls.startHandshake(); InputStream is = ssls.getInputStream(); OutputStream os = ssls.getOutputStream(); |
![]() ![]() |
![]()
|
要精心實現安全的非阻塞連接,也需要分別從客戶端和服務器端來看。
在客戶端建立安全的非阻塞連接非常簡單:
- 創建并連接
Socket
對象。 - 把
Socket
對象添加到模擬層上。 - 通過模擬層通信。
清單 12 說明了這些步驟:
清單 12. 創建安全的客戶機連接
/* Create the factory, then the secure socket */ SSLSocketFactory sslFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port); /* Start the handshake. Should be done before deriving channels */ ssl.startHandshake(); /* Put it into the emulation layer and create separate channels */ nbChannel socketChannel = new nbChannel(ssl); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel; |
利用前面給出的 模擬層類 就可以實現非阻塞的安全連接。因為安全套接字通道不能使用 SocketChannel
類打開,而 Java API 中又沒有完成這項工作的類,所以創建了一個模擬類。模擬類可以實現非阻塞通信,無論使用安全套接字連接還是非安全套接字連接。
列出的步驟包括默認的安全設置。對于更高級的安全性,比如用戶證書和客戶驗證, 參考資料 部分提供了說明如何實現的文章。
在服務器端建立套接字需要對默認安全稍加設置。但是一旦套接字被接收和路由,設置必須與客戶端的設置完全相同,如清單 13 所示:
清單 13. 創建安全的非阻塞服務器套接字
/* Create the factory, then the socket, and put it into listening mode */ SSLServerSocketFactory sslssf = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port); SSLSocket ssls = (SSLSocket)sslss.accept(); /* Start the handshake on the new socket */ ssls.startHandshake(); /* Put it into the emulation layer and create separate channels */ nbChannel socketChannel = new nbChannel(ssls); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel; |
同樣,要記住這些步驟使用的是默認安全設置。
![]() ![]() |
![]()
|
多數 Internet 客戶機應用程序,無論使用 Java 語言還是其他語言編寫,都需要提供安全和非安全連接。Java Secure Socket Extensions 庫使得這項工作非常容易,我最近在編寫一個 HTTP 客戶庫時就使用了這種方法。
SSLSocket
類派生自 Socket。
您可能已經猜到我要怎么做了。所需要的只是該對象的一個 Socket
指針。如果套接字連接不使用SSL,則可以像通常那樣創建套接字。如果要使用 SSL,就稍微麻煩一點,但此后的代碼就很簡單了。清單 14 給出了一個例子:
清單 14. 集成安全的和非安全的客戶機連接
Socket s; ReadableByteChannel rbc; WritableByteChannel wbc; nbChannel socketChannel; if(!useSSL) s = new Socket(host, port); else { SSLSocketFactory sslsf = SSLSocketFactory.getDefault(); SSLSocket ssls = (SSLSocket)SSLSocketFactory.createSocket(host, port); ssls.startHandshake(); s = ssls; } socketChannel = new nbChannel(s); rbc = (ReadableByteChannel)socketChannel; wbc = (WritableByteChannel)socketChannel; ... s.close(); |
創建通道之后,如果套接字使用了SSL,那么就是安全通信,否則就是普通通信。如果使用了 SSL,關閉套接字將導致握手中止。
這種設置的一種可能是使用兩個單獨的類。一個類負責處理通過套接字沿著與非安全套接字的連接進行的所有通信。一個單獨的類應該負責創建安全的連接,包括安全連接的所有必要設置,無論是否是默認的。安全類應該直接插入通信類,只有在使用安全連接時被調用。
使用J2SEAPI讀取Properties文件的六種方法
1。使用java.util.Properties類的load()方法示例:InputStreamin=lnewBufferedInputStream(newFileInputStream(name));Propertiesp=newProperties();p.load(in);
2。使用java.util.ResourceBundle類的getBundle()方法示例:ResourceBundlerb=ResourceBundle.getBundle(name,Locale.getDefault());
3。使用java.util.PropertyResourceBundle類的構造函數示例:InputStreamin=newBufferedInputStream(newFileInputStream(name));ResourceBundlerb=newPropertyResourceBundle(in);
4。使用class變量的getResourceAsStream()方法示例:InputStreamin=JProperties.class.getResourceAsStream(name);Propertiesp=newProperties();p.load(in);
5。使用class.getClassLoader()所得到的java.lang.ClassLoader的getResourceAsStream()方法示例:InputStreamin=JProperties.class.getClassLoader().getResourceAsStream(name);Propertiesp=newProperties();p.load(in);
6。使用java.lang.ClassLoader類的getSystemResourceAsStream()靜態方法示例:InputStreamin=ClassLoader.getSystemResourceAsStream(name);Propertiesp=newProperties();p.load(in);
補充
Servlet中可以使用javax.servlet.ServletContext的getResourceAsStream()方法示例:InputStreamin=context.getResourceAsStream(path);Propertiesp=newProperties();p.load(in);
Windows和瀏覽器快捷鍵
使用計算機和軟件的最大理由是可以提高工作效率。提高效率的關鍵,一是佳軟,二是善用。熟練掌握熱鍵,乃是高效工作之道的基礎和不二法門。下文針對最經典的軟件,列舉了最實用的快捷鍵,涉及:操作系統、瀏覽器、播放器、交流工具、文件管理工具、文本編輯等。要注意的是,相同的全局熱鍵,只能讓一個程序生效。
Windows 及相應軟件
1 最小化所有窗口(顯示桌面)/恢復原狀 Win + D
2 打開運行對話框 Win + R
3 打開系統屬性 Win + Break/Pause
4 復制某一對象 按住CTRL拖動
5 選中/高亮文本塊 CTRL+SHIFT+方向鍵
6 按打開的順序在窗口間切換 Alt + Esc
7 復制文件 CTRL + C
8 粘貼文件 CTRL + V
9 剪切文件 CTRL + X
10 還原 CTRL + Z
11 撤消還原操作(如果可能的話) CTRL + Y
12 打開輔助工具 Win + U
13 打開資源管理器 Win + E
14 打開上下文菜單 Shift + F10
15 多頁簽時,在頁簽間切換 Ctrl + Tab
更多Windows全局熱鍵: Windows keyboard shortcuts
更多Mac OS全局熱鍵: Mac OS X keyboard shortcuts
更多Linux全局熱鍵: Linux Keyboard Shortcuts You Should Know About
瀏覽器: Firefox
16 去除csS樣式 Alt + V + Y + N (或 CTRL + Shift + S + Web Developer’s 工具欄)
17 恢復CSS樣式 Alt + V + Y + B
18 查看源代碼 Ctrl + U
19 查看選中內容的源代碼 選取內容,Shift + F10,點“Show source code”
20 智能 DOM 偵測 Ctrl + Shift + I
21 啟動Firebug F12
22 添加書簽 Ctrl + D
23 書簽 Ctrl + B
24 歷鳴 Ctrl + H
25 找開剛關閉的標簽 CTRL+SHIFT+T
26 全部標簽加入書簽 CTRL+SHIFT+D
27 返回 Alt + Left Arrow
28 前進 Alt + Right Arrow
29 訪問歷史中后退一步 Backspace
30 為書簽添加關鍵詞 這樣做可以更快的訪問書簽。右擊書簽,選擇屬性,輸入合適的關鍵詞。保存后,你只要在地址欄輸入關鍵詞并回車,就可以訪問書簽了。
31 跳轉到地址欄 Ctrl + L 或 F6
32 回到主頁 Alt + Home
33 減小文本字號 Ctrl + -
34 增大文本字號 Ctrl + +
35 回到主頁(與32重復) Alt + Home
36 快速搜索 /
37 跳轉到搜索條 Ctrl + K
38 在標簽歷史上跳轉 ALT + ← (后退), ALT + → (前進)
39 新建標簽 Ctrl + T (鍵盤), 雙擊標簽條 (鼠標)
40 關閉當前書簽 Ctrl + W (鍵盤), 中鍵標簽 (鼠標)
41 到下一標簽 Ctrl + Page up 或 CTRL + Tab
42 到前一標簽 Ctrl + Page Dn 或 Ctrl + Shift + Tab
43 新標簽中打開鏈接 Ctrl + 左擊
43 選擇標簽 Ctrl + [1 - 9]
45 到下一鏈接 Tab
46 到前一鏈接 Shift + Tab
47 輸入框中顯示輸入過的文字,或下拉菜單中顯示可選項 Alt + ↓
瀏覽器: Internet Explorer 7
48 在新的后臺標簽打開鏈接 CTRL+鼠標左鍵或中鍵
49 在新的前臺標簽打開鏈接 CTRL+SHIFT+鼠標左鍵或中鍵
50 打開快速標簽視圖 CTRL+Q
51 顯示已打開標簽列表 CTRL+SHIFT+Q
52 轉到地址欄 Alt + D
53 在新標簽中打開輸入的地址 Address Bar in new tab Alt + Enter
54 轉到搜索條 Ctrl + E
55 新建標簽 Ctrl + T (keyboard), Double Click on Tab Bar (mouse)
56 關閉當前標簽 Ctrl + W (keyboard), Middle Click on Tab (mouse)
57 到下一標簽 Ctrl + Tab
58 到前一標簽 Ctrl + Shift + Tab
59 到某一標簽 Ctrl + [1 - 9]
60 打開feeds CTRL+J
61 到下一標簽 Tab
62 到前一標簽 Shift + Tab
Safari 熱鍵
更多 MSIE 7熱鍵
播放器快捷鍵
音樂播放: Winamp
要使用全局熱鍵,須設置:Main Windows -> Options > Preferences > Global Hotkeys。其熱鍵可自定義。
63 增大音量 CTRL + ALT + ↑
64 減小音量 CTRL + ALT + ↓
65 播放、重新開始或恢復播放 Winamp 窗口: x
66 暫停 CTRL + ALT + Home (Winamp 窗口: c)
67 播放 CTRL + ALT + Insert (Winamp 窗口: x)
68 停止 CTRL + ALT + End (Winamp 窗口: v)
69 前一首 CTRL + ALT + PgUp (Winamp 窗口: z)
70 下一首 CTRL + ALT + PgDn (Winamp 窗口: b)
71 返回5秒 CTRL + ALT + ← (Winamp 窗口: ←)
72 快進5秒 CTRL + ALT + → (Winamp 窗口: →)
73 切換重復 r
74 切換隨機 s
75 添加文件 l
76 添加目錄 Shift + l
77 隨機播放列表 CTRL + Shift + r
更多熱鍵: Winamp Shortcuts
Winamp Global Hotkeys
iTunes 熱鍵: Hotkeys for iTunes
Last.FM 熱鍵: Autohotkey Script for Last.FM
通訊: Thunderbird
在Skype中可自定義熱鍵: Main Window -> Tools -> Options -> Hotkeys.
在Thunderbird中使用熱鍵: Thunderbird Help: Keyboard Shortcuts 和 All hotkeys: Mozilla Thunderbird Hotkeys
xbeta提醒:Thunderbird和Firefox很多熱鍵是相通的,并且可用keyconfig擴展更改和自定義更多熱鍵。
78 轉到下一封信 F
79 轉到下一封未讀信件 N
80 轉到上一封未讀信件 B
81 增大字號 Ctrl + +
82 減小字號 CTRL + -
83 標記信件為已讀/未讀 M
84 標記為垃圾郵件 J
85 標記為非垃圾郵件 SHIFT + J
86 查看信件源文件 CTRL + U
87 新建信件 CTRL + M, CTRL + N, Cmd + Shift + M (Mac)
88 回復信件 Ctrl + R
89 收取當前賬戶的信件 CTRL+T
90 收取所有賬戶的信件 CTRL+SHIFT+T
91 打開已收的信件 CTRL + O
92 發送信件 CTRL + Enter/Return
通訊: Google Mail
93 寫郵件 c, + c 在新窗口中寫信
94 回信 r, + r 在新窗口中回信
95 轉發信件 f, + f 在新窗口中轉發信件
96 轉到收件箱 g 然后按 i
97 定位到搜索框 /
98 轉到下一封信 n
99 轉到上一封信 p
100 報告垃圾郵件 !
Total Commander快捷鍵
文件管理: Total CoMMander
xbeta注:Total Commander是Windows 下鍵盤操作的典范和極致。用TC不會熱鍵,等于不懂TC。喜歡熱鍵操作而未用TC——xbeta想不出會有這種情況。TC的熱鍵是高度可以自定義的。如下舉例僅是管中窺豹,還有更多熱鍵需要你的發現,比如很重要的 alt+F5, alt+F9, alt+enter, →,space, alt+shift+enter, backspace,+, alt+, alt+F7, 及中國用戶廣泛自定義的ctrl+1,ctrl+2, ctrl+3等。
101 激活或取消激活左側菜單 F10
102 比較和同步文件夾 SHIFT+F2
103 新建文本文件,并用編輯器打開 SHIFT + F4
104 在同一目錄復制文件并改名 SHIFT + F5
105 重命名文件 SHIFT + F6
106 縮小到系統托盤圖標 SHIFT + Esc
107 轉到剛訪問的前/后一個目錄 ALT+left/right
108 顯示已訪問目錄歷史清單 ALT + Arrow Down
109 全選 CTRL+NUM +, CTRL + A
110 全不選 CTRL+NUM -
111 返回上級目錄 (cd ..) CTRL+PgUp or Backspace
112 返回根目錄 (多數歐洲鍵盤)(xbeta:適用于中國用戶) CTRL+
113 返回根目錄 (美國鍵盤布局)(xbeta注:對中國用戶是新建目錄) F7
114 按文件名排序 CTRL+F3
115 按后綴排序 CTRL+F4
116 按時間/日期排序 CTRL+F5
117 按大小排序 CTRL+F6
118 顯示所有文件 CTRL+F10
119 僅顯示可執行文件 CTRL + F11
120 顯示用戶自定義的文件 CTRL + F12
121 顯示文件屬性 ALT+ENTER
122 并列顯示目錄及所有子目錄下的全部文件(xbeta:這是一個非常實用的功能,不用TC的人體會不到) CTRL + B
123 進入目錄收藏列表(書簽)(xbeta:TC用戶就稱之為ctrl+d) CTRL + D
124 連接FTP CTRL + F (斷開: CTRL + SHIFT + F)
125 批量改名(xbeta:極其強大) CTRL + M
126 進入命令行對話框并帶入當前路徑 CTRL + P
127 快速瀏覽(xbeta:這是TC最常用、最實用的熱鍵。相似功能F3) CTRL + Q
128 新建目錄標簽并激活它 CTRL + T (CTRL + SHIFT + T是新建標簽但不激活)
129 左右窗口交換目錄 CTRL + U
130 左右窗口交換目錄和標簽 CTRL + SHIFT + U
131 把光標處目錄在新標簽打開 CTRL + Arrow Up
132 轉到下一標簽 CTRL + TAB
133 轉到上一標簽 CTRL+SHIFT+TAB
134 定位到某一目錄/文件(xbeta強烈建議定位框模式) CTRL+ALT+字母(s)
Wordpress
135 加粗 Alt + Shift + B
136 斜體 Alt + Shift + I
137 引用 Alt + Shift + Q
138 列表 (ul) Alt + Shift + U
139 序號列表 (ol) Alt + Shift + O
140 列表項 (li) Alt + Shift + L
141 代碼 Alt + Shift + C
142 插入 Alt + Shift + S
143 刪除 Alt + Shift + D
144 鏈接 Alt + Shift + A
145 more (Read More tag) Alt + Shift + T
146 發表文章 Alt + Shift + P
通訊: microsoft Office Outlook
147 切換到郵件 CTRL + 1
148 切換到日歷 CTRL + 2
149 切換到通訊錄 Ctrl + 3
150 切換到任務 Ctrl + 4
151 切換到筆記 Ctrl + 5
152 新建約會 CTRL+SHIFT+A
153 新建聯系人 CTRL+SHIFT+C
154 新建日志項 CTRL+SHIFT+J
155 新建會議 CTRL+SHIFT+Q
156 新建信件 CTRL+SHIFT+M
157 新建筆記 CTRL+SHIFT+N
158 新建任務 CTRL + SHIFT + K
159 拼寫檢查 F7
160 轉發信件 CTRL + F
161 搜索 F4
162 切換到收件箱 CTRL+SHIFT+I
163 切換到發件箱 CTRL+SHIFT+O
164 發送 Alt + S
165 回信 Ctrl + SHIFT + R
166 收信 CTRL+M or F9
167 寫信 CTRL + N
168 打開信件 CTRL + O
169 標為已讀 CTRL + Q
Outlook的全局熱鍵: Keyboard shortcuts for Outlook
ICQ、ACDSee
通訊: ICQ
170 模仿在系統托盤中雙擊 CTRL + SHIFT + I
171 激活/取消激活用戶窗口 CTRL + SHIFT + A
172 關閉 ICQ Control + Shift + I and then Alt + F4
173 發送url給聯系人 Control + Shift + F6
174 改變狀態 Alt + S +
175 接收信息 Control + Shift + I
176 選擇好友 Insert +
圖像管理: ACDSee Viewer
177 縮小顯示比例(xbeta:推薦用免費軟件IrfanView或XnView代替ACDSee) - (就是減號)
178 增加顯示比例 +
179 復制當前項到某目錄 ALT + C
180 移動當前項到某目錄 ALT + M
181 改名當前項到某目錄 ALT + R
182 打開屬性面板 Alt + Enter
183 顯/隱狀態欄 b
184 顯/隱菜單欄 Ctrl + Shift + M
185 用默認程序打開當前圖像 CTRL + E
186 進入轉換對話框 CTRL + F
187 復制選中部分到剪貼板 Ctrl + Insert
188 打開旋轉/鏡像對話框(xbeta:還是IrfanView方便) CTRL + J
189 打開當前圖像,進入編輯模式,激活調節曝光工具 Ctrl + L
190 打開當前圖像,進入編輯模式,激活調節大小工具 CTRL+R
191 清空選中內容 CTRL+Q
192 保存圖像 CTRL + S
193 關閉查看窗口 CTRL + W
194 90度順時針旋轉當前圖片(xbeta注:IrfanView只要按一下r即可) Ctrl + Alt +
195 改變圖像為黑白兩色 Ctrl + Shift + 1
196 進入批量處理菜單(xbeta注:IrfanView按B即可) CTRL + Alt + B
197 以默認程序打開當前文件 Shift + E
在小巧但極其強大的IrfanView中使用熱鍵: Irfanview
編輯: Ultraedit
說到編輯器的快捷鍵,VIM是無與倫比的。要反對,也得是帶腳踏板的Emacs。UE還是有差距的,很大差距。注意:VIM是開源、免費的,而UE則需要注冊。UE是Windows下最好的編輯器——如果沒有GVIM和Emacs的話。而VIM和Emacs則是任何操作系統下最好的編輯器。
198 自動換行 CTRL + W
199 插入當前日期/時間 F7
200 找到匹配的括號 (,[,{ or },],) CTRL + B
201 段落重新格式化 CTRL + T
202 Tag 列表 CTRL + F8
203 轉換所選文字為小寫 CTRL + F5
204 轉換所選文字為大寫 Alt + F5
205 激活拼寫檢查 CTRL + K
206 切換列/塊模式 ALT + C
207 設定書簽 CTRL + F2
208 轉到下一書簽 F2
209 插入用戶定義的模板 Alt+0-9 or Shift+Alt+0-9
210 上滾一行,光標不變 CTRL + Up
211 下滾一行,光標不變 CTRL + Down
212 顯示函數列表 F8
213 到下一段 Alt + Right
214 到上一段 Alt + Left
雪絨花(Edelweiss),又叫高山火絨草(德語中意為:高貴的白色),只生長在阿爾卑斯山脈的林線以上。雪絨花約有40個種類,分布在亞洲和歐洲的阿爾卑斯山脈一帶。這種花在阿爾卑斯山脈中通常生長在海拔1700米以上的地方,由于它只生長在非常少有的巖石地表上,因而極為稀少。
在奧地利,雪絨花象征著勇敢,因為野生的雪絨花生長在條件艱苦的山上,常人難以得見其美麗容顏,所以見過雪絨花的人都是英雄。從前,奧地利許多年輕人冒著生命危險,攀上陡峭的山崖,只為摘下一朵雪絨花獻給自己的心上人,只有雪絨花才能代表為愛犧牲一切的決心。關于雪絨花,奧地利有許多傳說。人們相信雪絨花王可以指引那些采摘者們找到他們尋覓已久的花,但如果誰將花兒連根拔去,這個人將會墜入萬丈深淵。
浪漫的傳說固然令人神往,當雪絨花與軍人聯系起來的時候,它才被賦予新的意義,當佩帶雪絨花的第三帝國軍人憑借非凡的勇氣征服一座座雪山的時候,渺小者的力量得到了充分的體現,德軍用它作為自己山地部隊的標志。傳說每個志在加入山地部隊的年輕人,都必須只攜帶最少的裝備爬上阿爾卑斯山,采下一朵雪絨花,以證明自己適合做一個真正的山地軍人。
