java中的同步與異步

經常看到介紹 ArrayList 和HashMap是異步,Vector和HashTable是同步,這裏同步是線程安全的,異步不是線程安全的,舉例說明:

當創建一個Vector對象時候,

Vector ve=new Vector();
ve.add("1");

當在多線程程序中,第一個線程調用修改對象ve的時候,就爲其上了鎖,其他線程只有等待。

當創建一個ArrayList對象時候,

ArrayList list=new ArrayList();
list.add("1");

當在多線程程序中,第一個線程調用修改對象list的時候,沒有爲其上鎖,其他線程訪問時就會報錯。

eg:list.remove("1"),然後再由其他線程訪問list對象的1時就會報錯。


經常看到介紹 ArrayList 和HashMap是異步,Vector和HashTable是同步,這裏同步是線程安全的,異步不是線程安全的,舉例說明:

當創建一個Vector對象時候,

Vector ve=new Vector();
ve.add("1");

當在多線程程序中,第一個線程調用修改對象ve的時候,就爲其上了鎖,其他線程只有等待。

當創建一個ArrayList對象時候,

ArrayList list=new ArrayList();
list.add("1");

當在多線程程序中,第一個線程調用修改對象list的時候,沒有爲其上鎖,其他線程訪問時就會報錯。

eg:list.remove("1"),然後再由其他線程訪問list對象的1時就會報錯。


java 異步與同步應用

所謂異步輸入輸出機制,是指在進行輸入輸出處理時,不必等到輸入輸出處理完畢才返回。所以異步的同義語是非阻塞(None Blocking)。

 

網上有很多網友用很通俗的比喻 把同步和異步講解的很透徹 轉過來

 

舉個例子:普通B/S模式(同步)AJAX技術(異步)  
           同步:提交請求->等待服務器處理->處理完畢返回 這個期間客戶端瀏覽器不能幹任何事  
           異步:   請求通過事件觸發->服務器處理(這是瀏覽器仍然可以作其他事情)->處理完畢

 

同步就是你叫我去吃飯,我聽到了就和你去吃飯;如果沒有聽到,你就不停的叫,直到我告訴你聽到了,才一起去吃飯。  
異步就是你叫我,然後自己去吃飯,我得到消息後可能立即走,也可能等到下班纔去吃飯。  
所以,要我請你吃飯就用同步的方法,要請我吃飯就用異步的方法,這樣你可以省錢。

 

以通訊爲例  
           同步:發送一個請求,等待返回,然後再發送下一個請求  
           異步:發送一個請求,不等待返回,隨時可以再發送下一個請求  
           併發:同時發送多個請求

 

下面再轉一段關於java異步應用的文章

         用異步輸入輸出流編寫Socket進程通信程序

    在Merlin中加入了用於實現異步輸入輸出機制的應用程序接口包:java.nio(新的輸入輸出包,定義了很多基本類型緩衝(Buffer)), java.nio.channels(通道及選擇器等,用於異步輸入輸出),java.nio.charset(字符的編碼解碼)。通道 (Channel)首先在選擇器(Selector)中註冊自己感興趣的事件,當相應的事件發生時,選擇器便通過選擇鍵(SelectionKey)通知已註冊的通道。然後通道將需要處理的信息,通過緩衝(Buffer)打包,編碼/解碼,完成輸入輸出控制。

           通道介紹:

    這裏主要介紹ServerSocketChannel和 SocketChannel.它們都是可選擇的(selectable)通道,分別可以工作在同步和異步兩種方式下(注意,這裏的可選擇不是指可以選擇兩種工作方式,而是指可以有選擇的註冊自己感興趣的事件)。可以用channel.configureBlocking(Boolean )來設置其工作方式。與以前版本的API相比較,ServerSocketChannel就相當於ServerSocket (ServerSocketChannel封裝了ServerSocket),而SocketChannel就相當於Socket (SocketChannel封裝了Socket)。當通道工作在同步方式時,編程方法與以前的基本相似,這裏主要介紹異步工作方式。

所謂異步輸入輸出機制,是指在進行輸入輸出處理時,不必等到輸入輸出處理完畢才返回。所以異步的同義語是非阻塞(None Blocking)。在服務器端,ServerSocketChannel通過靜態函數open()返回一個實例serverChl。然後該通道調用 serverChl.socket().bind()綁定到服務器某端口,並調用register(Selector sel, SelectionKey.OP_ACCEPT)註冊OP_ACCEPT事件到一個選擇器中(ServerSocketChannel只可以註冊 OP_ACCEPT事件)。當有客戶請求連接時,選擇器就會通知該通道有客戶連接請求,就可以進行相應的輸入輸出控制了;在客戶端,clientChl實例註冊自己感興趣的事件後(可以是OP_CONNECT,OP_READ,OP_WRITE的組合),調用clientChl.connect (InetSocketAddress )連接服務器然後進行相應處理。注意,這裏的連接是異步的,即會立即返回而繼續執行後面的代碼。

           選擇器和選擇鍵介紹:

    選擇器(Selector)的作用是:將通道感興趣的事件放入隊列中,而不是馬上提交給應用程序,等已註冊的通道自己來請求處理這些事件。換句話說,就是選擇器將會隨時報告已經準備好了的通道,而且是按照先進先出的順序。那麼,選擇器是通過什麼來報告的呢?選擇鍵(SelectionKey)。選擇鍵的作用就是表明哪個通道已經做好了準備,準備幹什麼。你也許馬上會想到,那一定是已註冊的通道感興趣的事件。不錯,例如對於服務器端serverChl來說,可以調用key.isAcceptable()來通知serverChl有客戶端連接請求。相應的函數還有: SelectionKey.isReadable(),SelectionKey.isWritable()。一般的,在一個循環中輪詢感興趣的事件(具體可參照下面的代碼)。如果選擇器中尚無通道已註冊事件發生,調用Selector.select()將阻塞,直到有事件發生爲止。另外,可以調用 selectNow()或者select(long timeout)。前者立即返回,沒有事件時返回0值;後者等待timeout時間後返回。一個選擇器最多可以同時被63個通道一起註冊使用。

            應用實例:

    下面是用異步輸入輸出機制實現的客戶/服務器實例程序�D�D程序清單1(限於篇幅,只給出了服務器端實現,讀者可以參照着實現客戶端代碼):

1.         public class NBlockingServer {

2.             int port = 8000;

3.             int BUFFERSIZE = 1024;

4.             Selector selector = null;

5.             ServerSocketChannel serverChannel = null;

6.             HashMap clientChannelMap = null;//用來存放每一個客戶連接對應的套接字和通道

7.         

8.             public NBlockingServer( int port ) {

9.                 this.clientChannelMap = new HashMap();

10.             this.port = port;

11.         }

12.     

13.         public void initialize() throws IOException {

14.           //初始化,分別實例化一個選擇器,一個服務器端可選擇通道

15.           this.selector = Selector.open();

16.           this.serverChannel = ServerSocketChannel.open();

17.           this.serverChannel.configureBlocking(false);

18.           InetAddress localhost = InetAddress.getLocalHost();

19.           InetSocketAddress isa = new InetSocketAddress(localhost, this.port );

20.           this.serverChannel.socket().bind(isa);//將該套接字綁定到服務器某一可用端口

21.         }

22.         //結束時釋放資源

23.         public void finalize() throws IOException {

24.             this.serverChannel.close();

25.             this.selector.close();

26.         }

27.         //將讀入字節緩衝的信息解碼

28.         public String decode( ByteBuffer byteBuffer ) throws 

29.     CharacterCodingException {

30.             Charset charset = Charset.forName( "ISO-8859-1" );

31.             CharsetDecoder decoder = charset.newDecoder();

32.             CharBuffer charBuffer = decoder.decode( byteBuffer );

33.             String result = charBuffer.toString();

34.             return result;

35.         }

36.         //監聽端口,當通道準備好時進行相應操作

37.         public void portListening() throws IOException, InterruptedException {

38.           //服務器端通道註冊OP_ACCEPT事件

39.           SelectionKey acceptKey =this.serverChannel.register( this.selector,

40.                                                SelectionKey.OP_ACCEPT );

41.             //當有已註冊的事件發生時,select()返回值將大於0

42.             while (acceptKey.selector().select() > 0 ) {

43.                 System.out.println("event happened");

44.                 //取得所有已經準備好的所有選擇鍵

45.                 Set readyKeys = this.selector.selectedKeys();

46.                 //使用迭代器對選擇鍵進行輪詢

47.                 Iterator i = readyKeys.iterator();

48.                 while (i.hasNext()) {

49.                     SelectionKey key = (SelectionKey)i.next();

50.                     i.remove();//刪除當前將要處理的選擇鍵

51.                     if ( key.isAcceptable() ) {//如果是有客戶端連接請求

52.                         System.out.println("more client connect in!");

53.                         ServerSocketChannel nextReady =

54.                             (ServerSocketChannel)key.channel();

55.                         //獲取客戶端套接字

56.                         Socket s = nextReady.accept();

57.                         //設置對應的通道爲異步方式並註冊感興趣事件

58.                         s.getChannel().configureBlocking( false );

59.                         SelectionKey readWriteKey =

60.                             s.getChannel().register( this.selector,

61.                                 SelectionKey.OP_READ|SelectionKey.OP_WRITE  );

62.                         //將註冊的事件與該套接字聯繫起來

63.     readWriteKey.attach( s );

64.     //將當前建立連接的客戶端套接字及對應的通道存放在哈希表//clientChannelMap中

65.                         this.clientChannelMap.put( s, new 

66.     ClientChInstance( s.getChannel() ) );

67.                         }

68.                     else if ( key.isReadable() ) {//如果是通道讀準備好事件

69.                         System.out.println("Readable");

70.                         //取得選擇鍵對應的通道和套接字

71.                         SelectableChannel nextReady =

72.                             (SelectableChannel) key.channel();

73.                         Socket socket = (Socket) key.attachment();

74.                         //處理該事件,處理方法已封裝在類ClientChInstance中

75.                         this.readFromChannel( socket.getChannel(),

76.                         (ClientChInstance)

77.     this.clientChannelMap.get( socket ) );

78.                     }

79.                     else if ( key.isWritable() ) {//如果是通道寫準備好事件

80.                         System.out.println("writeable");

81.                         //取得套接字後處理,方法同上

82.                         Socket socket = (Socket) key.attachment();

83.                         SocketChannel channel = (SocketChannel) 

84.     socket.getChannel();

85.                         this.writeToChannel( channel,"This is from server!");

86.                     }

87.                 }

88.             }

89.         }

90.         //對通道的寫操作

91.         public void writeToChannel( SocketChannel channel, String message ) 

92.     throws IOException {

93.             ByteBuffer buf = ByteBuffer.wrap( message.getBytes()  );

94.             int nbytes = channel.write( buf );

95.         }

96.          //對通道的讀操作

97.         public void readFromChannel( SocketChannel channel, ClientChInstance clientInstance )

98.         throws IOException, InterruptedException {

99.             ByteBuffer byteBuffer = ByteBuffer.allocate( BUFFERSIZE );

100.         int nbytes = channel.read( byteBuffer );

101.         byteBuffer.flip();

102.         String result = this.decode( byteBuffer );

103.         //當客戶端發出”@exit”退出命令時,關閉其通道

104.         if ( result.indexOf( "@exit" ) >= 0 ) {

105.             channel.close();

106.         }

107.         else {

108.                 clientInstance.append( result.toString() );

109.                 //讀入一行完畢,執行相應操作

110.                 if ( result.indexOf( ""n" ) >= 0 ){

111.                 System.out.println("client input"+result);

112.                 clientInstance.execute();

113.                 }

114.         }

115.     }

116.     //該類封裝了怎樣對客戶端的通道進行操作,具體實現可以通過重載execute()方法

117.     public class ClientChInstance {

118.         SocketChannel channel;

119.         StringBuffer buffer=new StringBuffer();

120.         public ClientChInstance( SocketChannel channel ) {

121.             this.channel = channel;

122.         }

123.         public void execute() throws IOException {

124.             String message = "This is response after reading from channel!";

125.             writeToChannel( this.channel, message );

126.             buffer = new StringBuffer();

127.         }

128.         //當一行沒有結束時,將當前字竄置於緩衝尾

129.         public void append( String values ) {

130.             buffer.append( values );

131.         }

132.     }

133. 

134. 

135.     //主程序

136.     public static void main( String[] args ) {

137.         NBlockingServer nbServer = new NBlockingServer(8000);

138.         try {

139.             nbServer.initialize();

140.         } catch ( Exception e ) {

141.             e.printStackTrace();

142.             System.exit( -1 );

143.         }

144.         try {

145.             nbServer.portListening();

146.         }

147.         catch ( Exception e ) {

148.             e.printStackTrace();

149.         }

150.     }

151. }

152. 

 小結:

從以上程序段可以看出,服務器端沒有引入多餘線程就完成了多客戶的客戶/服務器模式。該程序中使用了回調模式(CALLBACK),細心的讀者應該早就看出來了。需要注意的是,請不要將原來的輸入輸出包與新加入的輸入輸出包混用,因爲出於一些原因的考慮,這兩個包並不兼容。即使用通道時請使用緩衝完成輸入輸出控制。該程序在Windows2000,J2SE1.4下,用telnet測試成功

synchronized的一個簡單例子

public class TextThread
{

 /**
  * @param args
  */
 public static void main(String[] args)
 {
  // TODO 自動生成方法存根
        TxtThread tt = new TxtThread();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
 }
}

class TxtThread implements Runnable
{
 int num = 100;
 String str = new String();
 public void run()
 {
  while (true)
  {
   synchronized(str)
   {
   if (num>0)
   {
    try
    {
     Thread.sleep(10);
    }
    catch(Exception e)
    {
     e.getMessage();
    }
    System.out.println(Thread.currentThread().getName()+ "this is "+ num--);
   }
   }
  }
 }
}

上面的例子中爲了製造一個時間差,也就是出錯的機會,使用了Thread.sleep(10)

Java對多線程的支持與同步機制深受大家的喜愛,似乎看起來使用了synchronized關鍵字就可以輕鬆地解決多線程共享數據同步問題。到底如何?――還得對synchronized關鍵字的作用進行深入瞭解纔可定論。

總的說來,synchronized關鍵字可以作爲函數的修飾符,也可作爲函數內的語句,也就是平時說的同步方法和同步語句塊。如果再細的分類,synchronized可作用於instance變量、object reference(對象引用)、static函數和class literals(類名稱字面常量)身上。

在進一步闡述之前,我們需要明確幾點:

A.無論synchronized關鍵字加在方法上還是對象上,它取得的鎖都是對象,而不是把一段代碼或函數當作鎖――而且同步方法很可能還會被其他線程的對象訪問。

B.每個對象只有一個鎖(lock)與之相關聯。

C.實現同步是要很大的系統開銷作爲代價的,甚至可能造成死鎖,所以儘量避免無謂的同步控制。

接着來討論synchronized用到不同地方對代碼產生的影響:

假設P1、P2是同一個類的不同對象,這個類中定義了以下幾種情況的同步塊或同步方法,P1、P2就都可以調用它們。

1.  把synchronized當作函數修飾符時,示例代碼如下:

Public synchronized void methodAAA()

{

//….

}

這也就是同步方法,那這時synchronized鎖定的是哪個對象呢?它鎖定的是調用這個同步方法對象。也就是說,當一個對象P1在不同的線程中執行這個同步方法時,它們之間會形成互斥,達到同步的效果。但是這個對象所屬的Class所產生的另一對象P2卻可以任意調用這個被加了synchronized關鍵字的方法。

上邊的示例代碼等同於如下代碼:

public void methodAAA()

{

synchronized (this)      //  (1)

{

       //…..

}

}

 (1)處的this指的是什麼呢?它指的就是調用這個方法的對象,如P1。可見同步方法實質是將synchronized作用於object reference。――那個拿到了P1對象鎖的線程,纔可以調用P1的同步方法,而對P2而言,P1這個鎖與它毫不相干,程序也可能在這種情形下襬脫同步機制的控制,造成數據混亂:(

2.同步塊,示例代碼如下:

public void method3(SomeObject so)

{

    synchronized(so)

    {
       //…..
    }

}

這時,鎖就是so這個對象,誰拿到這個鎖誰就可以運行它所控制的那段代碼。當有一個明確的對象作爲鎖時,就可以這樣寫程序,但當沒有明確的對象作爲鎖,只是想讓一段代碼同步時,可以創建一個特殊的instance變量(它得是一個對象)來充當鎖:

class Foo implements Runnable

{

        private byte[] lock = new byte[0];  // 特殊的instance變量

        Public void methodA()
        {

           synchronized(lock) { //… }

        }

        //…..

}

注:零長度的byte數組對象創建起來將比任何對象都經濟――查看編譯後的字節碼:生成零長度的byte[]對象只需3條操作碼,而Object lock = new Object()則需要7行操作碼。

3.將synchronized作用於static 函數,示例代碼如下:

Class Foo
{

    public synchronized static void methodAAA()   // 同步的static 函數
    {
        //….
    }

    public void methodBBB()
    {

       synchronized(Foo.class)   //  class literal(類名稱字面常量)

    }
}

   代碼中的methodBBB()方法是把class literal作爲鎖的情況,它和同步的static函數產生的效果是一樣的,取得的鎖很特別,是當前調用這個方法的對象所屬的類(Class,而不再是由這個Class產生的某個具體對象了)。

記得在《Effective Java》一書中看到過將 Foo.class和 P1.getClass()用於作同步鎖還不一樣,不能用P1.getClass()來達到鎖這個Class的目的。P1指的是由Foo類產生的對象。

可以推斷:如果一個類中定義了一個synchronized的static函數A,也定義了一個synchronized 的instance函數B,那麼這個類的同一對象Obj在多線程中分別訪問A和B兩個方法時,不會構成同步,因爲它們的鎖都不一樣。A方法的鎖是Obj這個對象,而B的鎖是Obj所屬的那個Class。

小結如下:

搞清楚synchronized鎖定的是哪個對象,就能幫助我們設計更安全的多線程程序。

還有一些技巧可以讓我們對共享資源的同步訪問更加安全:

1.  定義private 的instance變量+它的 get方法,而不要定義public/protected的instance變量。如果將變量定義爲public,對象在外界可以繞過同步方法的控制而直接取得它,並改動它。這也是JavaBean的標準實現方式之一。

2.  如果instance變量是一個對象,如數組或ArrayList什麼的,那上述方法仍然不安全,因爲當外界對象通過get方法拿到這個instance對象的引用後,又將其指向另一個對象,那麼這個private變量也就變了,豈不是很危險。 這個時候就需要將get方法也加上synchronized同步,並且,只返回這個private對象的clone()――這樣,調用端得到的就是對象副本的引用了。

 

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/teedry/archive/2009/06/19/4281618.aspx

發佈了47 篇原創文章 · 獲贊 21 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章