1. 程式人生 > >java中的同步與非同步(轉)

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時就會報錯。

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()――這樣,呼叫端得到的就是物件副本的引用了。