<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    qileilove

    blog已經轉移至github,大家請訪問 http://qaseven.github.io/

    Java NIO與IO的差別和比較

    導讀
      J2SE1.4以上版本號中公布了全新的I/O類庫。本文將通過一些實例來簡介NIO庫提供的一些新特性:非堵塞I/O,字符轉換,緩沖以及通道。
      一. 介紹NIO
      NIO包(java.nio.*)引入了四個關鍵的抽象數據類型,它們共同解決傳統的I/O類中的一些問題。
      1. Buffer:它是包括數據且用于讀寫的線形表結構。當中還提供了一個特殊類用于內存映射文件的I/O操作。
      2. Charset:它提供Unicode字符串影射到字節序列以及逆影射的操作。
      3. Channels:包括socket,file和pipe三種管道,它實際上是雙向交流的通道。
      4. Selector:它將多元異步I/O操作集中到一個或多個線程中(它能夠被看成是Unix中select()函數或Win32中WaitForSingleEvent()函數的面向對象版本號)。
      二. 回想傳統
      在介紹NIO之前,有必要了解傳統的I/O操作的方式。以網絡應用為例,傳統方式須要監聽一個ServerSocket,接受請求的連接為其提供服務(服務通常包含了處理請求并發送響應)圖一是server的生命周期圖,當中標有粗黑線條的部分表明會發生I/O堵塞。
      
    圖一
      能夠分析創建server的每一個詳細步驟。首先創建ServerSocket
      ServerSocket server=new ServerSocket(10000);
      然后接受新的連接請求
      Socket newConnection=server.accept();
      對于accept方法的調用將造成堵塞,直到ServerSocket接受到一個連接請求為止。一旦連接請求被接受,server能夠讀客戶socket中的請求。
      InputStream in = newConnection.getInputStream();
      InputStreamReader reader = new InputStreamReader(in);
      BufferedReader buffer = new BufferedReader(reader);
      Request request = new Request();
      while(!request.isComplete()) {
      String line = buffer.readLine();
      request.addLine(line);
      }
      這種操作有兩個問題,首先BufferedReader類的readLine()方法在其緩沖區未滿時會造成線程堵塞,僅僅有一定數據填滿了緩沖區或者客戶關閉了套接字,方法才會返回。其次,它回產生大量的垃圾,BufferedReader創建了緩沖區來從客戶套接字讀入數據,可是相同創建了一些字符串存儲這些數據。盡管BufferedReader內部提供了StringBuffer處理這一問題,可是全部的String非常快變成了垃圾須要回收。
      相同的問題在發送響應代碼中也存在
      Response response = request.generateResponse();
      OutputStream out = newConnection.getOutputStream();
      InputStream in = response.getInputStream();
      int ch;
      while(-1 != (ch = in.read())) {
      out.write(ch);
      }
      newConnection.close();
      類似的,讀寫操作被堵塞并且向流中一次寫入一個字符會造成效率低下,所以應該使用緩沖區,可是一旦使用緩沖,流又會產生很多其它的垃圾。
    傳統的解決方法
      通常在Java中處理堵塞I/O要用到線程(大量的線程)。通常是實現一個線程池用來處理請求,如圖二
      
    圖二
      線程使得server能夠處理多個連接,可是它們也相同引發了很多問題。每一個線程擁有自己的棧空間并且占用一些CPU時間,耗費非常大,并且非常多時間是浪費在堵塞的I/O操作上,沒有有效的利用CPU。
      三. 新I/O
      1. Buffer
      傳統的I/O不斷的浪費對象資源(一般是String)。新I/O通過使用Buffer讀寫數據避免了資源浪費。Buffer對象是線性的,有序的數據集合,它依據其類別僅僅包括唯一的數據類型。
      java.nio.Buffer 類描寫敘述
      java.nio.ByteBuffer 包括字節類型。 能夠從ReadableByteChannel中讀在 WritableByteChannel中寫
      java.nio.MappedByteBuffer 包括字節類型,直接在內存某一區域映射
      java.nio.CharBuffer 包括字符類型,不能寫入通道
      java.nio.DoubleBuffer 包括double類型,不能寫入通道
      java.nio.FloatBuffer 包括float類型
      java.nio.IntBuffer 包括int類型
      java.nio.LongBuffer 包括long類型
      java.nio.ShortBuffer 包括short類型
      能夠通過調用allocate(int capacity)方法或者allocateDirect(int capacity)方法分配一個Buffer。特別的,你能夠創建MappedBytesBuffer通過調用FileChannel.map(int mode,long position,int size)。直接(direct)buffer在內存中分配一段連續的塊并使用本地訪問方法讀寫數據。非直接(nondirect)buffer通過使用Java中的數組訪問代碼讀寫數據。有時候必須使用非直接緩沖比如使用不論什么的wrap方法(如ByteBuffer.wrap(byte[]))在Java數組基礎上創建buffer。
      2. 字符編碼
      向ByteBuffer中存放數據涉及到兩個問題:字節的順序和字符轉換。ByteBuffer內部通過ByteOrder類處理了字節順序問題,可是并沒有處理字符轉換。其實,ByteBuffer沒有提供方法讀寫String。
      Java.nio.charset.Charset處理了字符轉換問題。它通過構造CharsetEncoder和CharsetDecoder將字符序列轉換成字節和逆轉換。
      3. 通道(Channel)
      你可能注意到現有的java.io類中沒有一個能夠讀寫Buffer類型,所以NIO中提供了Channel類來讀寫Buffer。通道能夠覺得是一種連接,能夠是到特定設備,程序或者是網絡的連接。通道的類等級結構圖例如以下
      
    圖三
      圖中ReadableByteChannel和WritableByteChannel分別用于讀寫。
      GatheringByteChannel能夠從使用一次將多個Buffer中的數據寫入通道,相反的,ScatteringByteChannel則能夠一次將數據從通道讀入多個Buffer中。你還能夠設置通道使其為堵塞或非堵塞I/O操作服務。
      為了使通道可以同傳統I/O類相容,Channel類提供了靜態方法創建Stream或Reader
      4. Selector
      在過去的堵塞I/O中,我們一般知道什么時候能夠向stream中讀或寫,由于方法調用直到stream準備好時返回。可是使用非堵塞通道,我們須要一些方法來知道什么時候通道準備好了。在NIO包中,設計Selector就是為了這個目的。SelectableChannel能夠注冊特定的事件,而不是在事件發生時通知應用,通道跟蹤事件。然后,當應用調用Selector上的隨意一個selection方法時,它查看注冊了的通道看是否有不論什么感興趣的事件發生。圖四是selector和兩個已注冊的通道的樣例
      
    圖四
      并非全部的通道都支持全部的操作。SelectionKey類定義了全部可能的操作位,將要用兩次。首先,當應用調用SelectableChannel.register(Selector sel,int op)方法注冊通道時,它將所需操作作為第二個參數傳遞到方法中。然后,一旦SelectionKey被選中了,SelectionKey的readyOps()方法返回全部通道支持操作的數位的和。SelectableChannel的validOps方法返回每一個通道同意的操作。注冊通道不支持的操作將引發IllegalArgumentException異常。下表列出了SelectableChannel子類所支持的操作。
      ServerSocketChannel OP_ACCEPT
      SocketChannel OP_CONNECT, OP_READ, OP_WRITE
      DatagramChannel OP_READ, OP_WRITE
      Pipe.SourceChannel OP_READ
      Pipe.SinkChannel OP_WRITE
      四. 舉例說明
      1. 簡單網頁內容下載
      這個樣例很easy,類SocketChannelReader使用SocketChannel來下載特定網頁的HTML內容。
    package examples.nio;
    import java.nio.ByteBuffer;
    import java.nio.channels.SocketChannel;
    import java.nio.charset.Charset;
    import java.net.InetSocketAddress;
    import java.io.IOException;
    public class SocketChannelReader{
    private Charset charset=Charset.forName("UTF-8");//創建UTF-8字符集
    private SocketChannel channel;
    public void getHTMLContent(){
    try{
    connect();
    sendRequest();
    readResponse();
    }catch(IOException e){
    System.err.println(e.toString());
    }finally{
    if(channel!=null){
    try{
    channel.close();
    }catch(IOException e){}
    }
    }
    }
    private void connect()throws IOException{//連接到CSDN
    InetSocketAddress socketAddress=
    new InetSocketAddress("http://www.csdn.net",80/);
    channel=SocketChannel.open(socketAddress);
    //使用工廠方法open創建一個channel并將它連接到指定地址上
    //相當與SocketChannel.open().connect(socketAddress);調用
    }
    private void sendRequest()throws IOException{
    channel.write(charset.encode("GET "
    +"/document"
    +"\r\n\r\n"));//發送GET請求到CSDN的文檔中心
    //使用channel.write方法,它須要CharByte類型的參數,使用
    //Charset.encode(String)方法轉換字符串。
    }
    private void readResponse()throws IOException{//讀取應答
    ByteBuffer buffer=ByteBuffer.allocate(1024);//創建1024字節的緩沖
    while(channel.read(buffer)!=-1){
    buffer.flip();//flip方法在讀緩沖區字節操作之前調用。
    System.out.println(charset.decode(buffer));
    //使用Charset.decode方法將字節轉換為字符串
    buffer.clear();//清空緩沖
    }
    }
    public static void main(String [] args){
    new SocketChannelReader().getHTMLContent();
    }
     2. 簡單的加法server和客戶機
      server代碼
    package examples.nio;
    import java.nio.ByteBuffer;
    import java.nio.IntBuffer;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.net.InetSocketAddress;
    import java.io.IOException;
    /**
    * SumServer.java
    *
    *
    * Created: Thu Nov 06 11:41:52 2003
    *
    * @author starchu1981
    * @version 1.0
    */
    public class SumServer {
    private ByteBuffer _buffer=ByteBuffer.allocate(8);
    private IntBuffer _intBuffer=_buffer.asIntBuffer();
    private SocketChannel _clientChannel=null;
    private ServerSocketChannel _serverChannel=null;
    public void start(){
    try{
    openChannel();
    waitForConnection();
    }catch(IOException e){
    System.err.println(e.toString());
    }
    }
    private void openChannel()throws IOException{
    _serverChannel=ServerSocketChannel.open();
    _serverChannel.socket().bind(new InetSocketAddress(10000));
    System.out.println("server通道已經打開");
    }
    private void waitForConnection()throws IOException{
    while(true){
    _clientChannel=_serverChannel.accept();
    if(_clientChannel!=null){
    System.out.println("新的連接增加");
    processRequest();
    _clientChannel.close();
    }
    }
    }
    private void processRequest()throws IOException{
    _buffer.clear();
    _clientChannel.read(_buffer);
    int result=_intBuffer.get(0)+_intBuffer.get(1);
    _buffer.flip();
    _buffer.clear();
    _intBuffer.put(0,result);
    _clientChannel.write(_buffer);
    }
    public static void main(String [] args){
    new SumServer().start();
    }
    } // SumServer
      客戶代碼
    package examples.nio;
    import java.nio.ByteBuffer;
    import java.nio.IntBuffer;
    import java.nio.channels.SocketChannel;
    import java.net.InetSocketAddress;
    import java.io.IOException;
    /**
    * SumClient.java
    *
    *
    * Created: Thu Nov 06 11:26:06 2003
    *
    * @author starchu1981
    * @version 1.0
    */
    public class SumClient {
    private ByteBuffer _buffer=ByteBuffer.allocate(8);
    private IntBuffer _intBuffer;
    private SocketChannel _channel;
    public SumClient() {
    _intBuffer=_buffer.asIntBuffer();
    } // SumClient constructor
    public int getSum(int first,int second){
    int result=0;
    try{
    _channel=connect();
    sendSumRequest(first,second);
    result=receiveResponse();
    }catch(IOException e){System.err.println(e.toString());
    }finally{
    if(_channel!=null){
    try{
    _channel.close();
    }catch(IOException e){}
    }
    }
    return result;
    }
    private SocketChannel connect()throws IOException{
    InetSocketAddress socketAddress=
    new InetSocketAddress("localhost",10000);
    return SocketChannel.open(socketAddress);
    }
    private void sendSumRequest(int first,int second)throws IOException{
    _buffer.clear();
    _intBuffer.put(0,first);
    _intBuffer.put(1,second);
    _channel.write(_buffer);
    System.out.println("發送加法請求 "+first+"+"+second);
    }
    private int receiveResponse()throws IOException{
    _buffer.clear();
    _channel.read(_buffer);
    return _intBuffer.get(0);
    }
    public static void main(String [] args){
    SumClient sumClient=new SumClient();
    System.out.println("加法結果為 :"+sumClient.getSum(100,324));
    }
    } // SumClient
      3. 非堵塞的加法server
      首先在openChannel方法中增加語句
      _serverChannel.configureBlocking(false);//設置成為非堵塞模式
      重寫WaitForConnection方法的代碼例如以下,使用非堵塞方式
    private void waitForConnection()throws IOException{
    Selector acceptSelector = SelectorProvider.provider().openSelector();
    /*在server套接字上注冊selector并設置為接受accept方法的通知。
    這就告訴Selector,套接字想要在accept操作發生時被放在ready表
    上,因此,同意多元非堵塞I/O發生。*/
    SelectionKey acceptKey = ssc.register(acceptSelector,
    SelectionKey.OP_ACCEPT);
    int keysAdded = 0;
    /*select方法在不論什么上面注冊了的操作發生時返回*/
    while ((keysAdded = acceptSelector.select()) > 0) {
    // 某客戶已經準備好能夠進行I/O操作了,獲取其ready鍵集合
    Set readyKeys = acceptSelector.selectedKeys();
    Iterator i = readyKeys.iterator();
    // 遍歷ready鍵集合,并處理加法請求
    while (i.hasNext()) {
    SelectionKey sk = (SelectionKey)i.next();
    i.remove();
    ServerSocketChannel nextReady =
    (ServerSocketChannel)sk.channel();
    // 接受加法請求并處理它
    _clientSocket = nextReady.accept().socket();
    processRequest();
    _clientSocket.close();
    }
    }
    }

    posted @ 2014-10-30 11:54 順其自然EVO 閱讀(645) | 評論 (0)編輯 收藏

    Java內存區域與內存溢出

    內存區域
      Java虛擬機在執行Java程序的過程中會把他所管理的內存劃分為若干個不同的數據區域。Java虛擬機規范將JVM所管理的內存分為以下幾個運行時數據區:程序計數器、Java虛擬機棧、本地方法棧、Java堆、方法區。下面詳細闡述各數據區所存儲的數據類型。
      程序計數器(Program Counter Register)
      一塊較小的內存空間,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時通過改變該計數器的值來選擇下一條需要執行的字節碼指令,分支、跳轉、循環等基礎功能都要依賴它來實現。每條線程都有一個獨立的的程序計數器,各線程間的計數器互不影響,因此該區域是線程私有的。
      當線程在執行一個Java方法時,該計數器記錄的是正在執行的虛擬機字節碼指令的地址,當線程在執行的是Native方法(調用本地操作系統方法)時,該計數器的值為空。另外,該內存區域是唯一一個在Java虛擬機規范中么有規定任何OOM(內存溢出:OutOfMemoryError)情況的區域。
      Java虛擬機棧(Java Virtual Machine Stacks)
      該區域也是線程私有的,它的生命周期也與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀,棧它是用于支持續虛擬機進行方法調用和方法執行的數據結構。對于執行引擎來講,活動線程中,只有棧頂的棧幀是有效的,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法,執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作。棧幀用于存儲局部變量表、操作數棧、動態鏈接、方法返回地址和一些額外的附加信息。在編譯程序代碼時,棧幀中需要多大的局部變量表、多深的操作數棧都已經完全確定了,并且寫入了方法表的Code屬性之中。因此,一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決于具體的虛擬機實現。
      在Java虛擬機規范中,對這個區域規定了兩種異常情況:
      1、如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常。
      2、如果虛擬機在動態擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。
      這里有一點要重點說明,在多線程情況下,給每個線程的棧分配的內存越大,越容易產生內存溢出異常。操作系統為每個進程分配的內存是有限制的,虛擬機提供了參數來控制Java堆和方法區這兩部分內存的最大值,忽略掉程序計數器消耗的內存(很小),以及進程本身消耗的內存,剩下的內存便給了虛擬機棧和本地方法棧,每個線程分配到的棧容量越大,可以建立的線程數量自然就越少。因此,如果是建立過多的線程導致的內存溢出,在不能減少線程數的情況下,就只能通過減少最大堆和每個線程的棧容量來換取更多的線程。當由于創建過量線程發生OOM時,會報錯:java.lang.OutOfMemoryError, unable to create new native thread。
      本地方法棧(Native Method Stacks)
      該區域與虛擬機棧所發揮的作用非常相似,只是虛擬機棧為虛擬機執行Java方法服務,而本地方法棧則為使用到的本地操作系統(Native)方法服務。與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError與OutOfMemoryError異常。
      Java堆(Java Heap)
      Java Heap是Java虛擬機所管理的內存中最大的一塊,它是所有線程共享的一塊內存區域,幾乎所有的對象實例和數組都在這類分配內存。Java Heap是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”。
      根據Java虛擬機規范的規定,Java堆可以處在物理上不連續的內存空間中,只要邏輯上是連續的即可。如果在堆中沒有內存可分配時,并且堆也無法擴展時,將會拋出OutOfMemoryError異常。
      注意:隨著JIT編譯器的發展與逃逸技術逐漸成熟,所有對象都分配在堆上也逐漸變得不是那么絕對了,線程共享的Java堆中也可能劃分出線程私有的分配緩沖區(TLAB)。
      方法區(Method Area)
      方法區也是各個線程共享的內存區域,它用于存儲已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。Java虛擬機規范把方法區描述為Java堆的一個邏輯部分,而且它和Java Heap一樣不需要連續的內存,可以選擇固定大小或可擴展,另外,虛擬機規范允許該區域可以選擇不實現垃圾回收。相對而言,垃圾收集行為在這個區域比較少出現。不過,這部分區域的回收是有必要的,如果這部分區域永遠不回收,那么類型就無法卸載,我們就無法加載更多的類,HotSpot的該區域有實現垃圾回收。
      根據Java虛擬機規范的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。
    直接內存(Direct Memory)
      直接內存并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域,它直接從操作系統中分配,因此不受Java堆大小的限制,但是會受到本機總內存的大小及處理器尋址空間的限制,因此它也可能導致OutOfMemoryError異常出現。在JDK1.4中新引入了NIO機制,它是一種基于通道與緩沖區的新I/O方式,可以直接從操作系統中分配直接內存,即在堆外分配內存,這樣能在一些場景中提高性能,因為避免了在Java堆和Native堆中來回復制數據。
      當使用超過虛擬機允許的直接內存時,虛擬機會拋出OutOfMemoryError異常,由DirectMemory導致的內存溢出,一個明顯的特征是在Heap Dump文件中不會看見明顯的異常。一般來說,如果發現OOM后Dump文件很小,那就應該考慮一下,是不是這塊內存發生了溢出。
      內存溢出
      Java堆內存溢出
    public class HeapOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
    List<OOMObject> list = new ArrayList<OOMObject>();
    while (true) {
    list.add(new OOMObject());
    }
    }
    }
      運行以上代碼時,可以增加運行參數-Xms20m -Xmx20m,該參數限制Java堆大小為20M,不可擴展。運行結果如下:
    <span style="color: #ff0000;">Exception in thread "main" java.lang.OutOfMemoryError: Java heap space</span>
    at java.util.Arrays.copyOf(Arrays.java:2245)
    at java.util.Arrays.copyOf(Arrays.java:2219)
    at java.util.ArrayList.grow(ArrayList.java:242)
    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
    at java.util.ArrayList.add(ArrayList.java:440)
    at HeapOOM.main(HeapOOM.java:17)
      可以看到,在堆內存溢出時,除了會報錯java.lang.OutOfMemoryError外,還會跟著進一步提示Java heap space。
      虛擬機棧和本地方法棧溢出
      要讓虛擬機棧內存溢出,我們可以使用遞歸調用:因為每次方法調用都需要向棧中壓入調用信息,當棧的大小固定時,過深的遞歸將向棧中壓入過量信息,導致
    StackOverflowError:
    public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
    stackLength++;
    stackLeak();
    }
    public static void main(String[] args) throws Throwable {
    JavaVMStackSOF oom = new JavaVMStackSOF();
    try {
    oom.stackLeak();
    } catch (Throwable e) {
    System.out.println("stack length:" + oom.stackLength);
    throw e;
    }
    }
    }
      運行以上代碼,輸出如下:
      stack length:10828
      <span style="color: #ff0000;">Exception in thread "main" java.lang.StackOverflowError</span>
      at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
      at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
      at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
      可以看到,在我的電腦上運行以上代碼,最多支持的棧深度是10828層,當發生棧溢出時,會報錯java.lang.StackOverflowError。
      方法區溢出
      方法區用于存放Class的相關信息,如類名、訪問修飾符、字段描述等,對于這個區域的測試,基本思路是運行時使用CGLib產生大量的類去填充方法區,直到溢出:
    public class JavaMethodAreaOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
    while( true) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(OOMObject. class);
    enhancer.setUseCache( false);
    enhancer.setCallback( new MethodInterceptor() {
    @Override
    public Object intercept(Object obj, Method method, Object[] args,
    MethodProxy proxy) throws Throwable {
    return proxy.invokeSuper(obj, args);
    }
    });
    enhancer.create();
    }
    }
    }
      運行時增加虛擬機參數:-XX:PermSize=10M -XX:MaxPermSize=10M,限制永久代大小為10M,最后報錯為java.lang.OutOfMemoryError: PermGen space。報錯信息明確說明,溢出區域為永久代。
      總結
      本文主要說明Java虛擬機一共分為哪幾塊內存區域,以及這幾塊內存區域是否會內存溢出,如果這些區域發生內存溢出報錯如何。了解這些知識后,以后遇到內存溢出報錯,我們就可以定位到具體內存區域,然后具體問題,具體分析。

    posted @ 2014-10-30 11:50 順其自然EVO 閱讀(206) | 評論 (0)編輯 收藏

    Java傳參方式-值傳遞還是引用傳遞

    參數是按值而不是按引用傳遞的說明 Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞。寫它是為了揭穿普遍存在的一種神話,即認為 Java 應用程序按引用傳遞參數,以避免因依賴“按引用傳遞”這一行為而導致的常見編程錯誤。
      對此節選的某些反饋意見認為,我把這一問題搞糊涂了,或者將它完全搞錯了。許多不同意我的讀者用 C++ 語言作為例子。因此,在此欄目中我將使用 C++ 和 Java 應用程序進一步闡明一些事實。
      要點
      讀完所有的評論以后,問題終于明白了,考試吧提示: 至少在一個主要問題上產生了混淆。因為對象是按引用傳遞的。對象確實是按引用傳遞的;節選與這沒有沖突。節選中說所有參數都是按值 -- 另一個參數 -- 傳遞的。下面的說法是正確的:在 Java 應用程序中永遠不會傳遞對象,而只傳遞對象引用。因此是按引用傳遞對象。但重要的是要區分參數是如何傳遞的,這才是該節選的意圖。Java 應用程序按引用傳遞對象這一事實并不意味著 Java 應用程序按引用傳遞參數。參數可以是對象引用,而 Java 應用程序是按值傳遞對象引用的。
      C++ 和 Java 應用程序中的參數傳遞
      Java 應用程序中的變量可以為以下兩種類型之一:引用類型或基本類型。當作為參數傳遞給一個方法時,處理這兩種類型的方式是相同的。兩種類型都是按值傳遞的;沒有一種按引用傳遞。這是一個重要特性,正如隨后的代碼示例所示的那樣。
      在繼續討論之前,定義按值傳遞和按引用傳遞這兩個術語是重要的。按值傳遞意味著當將一個參數傳遞給一個函數時,函數接收的是原始值的一個副本。因此,如果函數修改了該參數,僅改變副本,而原始值保持不變。按引用傳遞意味著當將一個參數傳遞給一個函數時,函數接收的是原始值的內存地址,而不是值的副本。因此,如果函數修改了該參數,調用代碼中的原始值也隨之改變。
      上面的這些是很重要的,請大家注意以下幾點結論,這些都是我認為的上面的文章中的精華和最終的結論:
      1、對象是按引用傳遞的
      2、Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞
      3、按值傳遞意味著當將一個參數傳遞給一個函數時,函數接收的是原始值的一個副本
      4、按引用傳遞意味著當將一個參數傳遞給一個函數時,函數接收的是原始值的內存地址,而不是值的副本
      首先考試吧來看看第一點:對象是按引用傳遞的
      確實,這一點我想大家沒有任何疑問,例如:
      class Test01
      {
      public static void main(String[] args)
      {
      StringBuffer s= new StringBuffer("good");
      StringBuffer s2=s;
      s2.append(" afternoon.");
      System.out.println(s);
      }
      }
      對象s和s2指向的是內存中的同一個地址因此指向的也是同一個對象。
      如何解釋“對象是按引用傳遞的”的呢?
      這里的意思是進行對象賦值操作是傳遞的是對象的引用,因此對象是按引用傳遞的,有問題嗎?
      程序運行的輸出是:
      good afternoon.
      這說明s2和s是同一個對象。
      這里有一點要澄清的是,這里的傳對象其實也是傳值,因為對象就是一個指針,這個賦值是指針之間的賦值,因此在java中就將它說成了傳引用。(引用是什么?不就是地址嗎?地址是什么,不過就是一個整數值)
      再看看下面的例子:
      class Test02
      {
      public static void main(String[] args)
      {
      int i=5;
      int i2=i;
      i2=6;
      System.out.println(i);
      }
      }
      程序的結果是什么?5!!!
      這說明什么,原始數據類型是按值傳遞的,這個按值傳遞也是指的是進行賦值時的行為。
    下一個問題:Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞
    class Test03
    {
    public static void main(String[] args)
    {
    StringBuffer s= new StringBuffer("good");
    StringBuffer s2=new StringBuffer("bad");
    test(s,s2);
    System.out.println(s);//9
    System.out.println(s2);//10
    }
    static void test(StringBuffer s,StringBuffer s2) {
    System.out.println(s);//1
    System.out.println(s2);//2
    s2=s;//3
    s=new StringBuffer("new");//4
    System.out.println(s);//5
    System.out.println(s2);//6
    s.append("hah");//7
    s2.append("hah");//8
    }
    }
      程序的輸出是:
      good
      bad
      new
      good
      goodhah
      bad
      考試吧提示: 為什么輸出是這樣的?
      這里需要強調的是“參數傳遞機制”,它是與賦值語句時的傳遞機制的不同。
      我們看到1,2處的輸出與我們的預計是完全匹配的
      3將s2指向s,4將s指向一個新的對象
      因此5的輸出打印的是新創建的對象的內容,而6打印的原來的s的內容
      7和8兩個地方修改對象內容,但是9和10的輸出為什么是那樣的呢?
      Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞。
      至此,我想總結一下我對這個問題的最后的看法和我認為可以幫助大家理解的一種方法:
      我們可以將java中的對象理解為c/c++中的指針
      例如在c/c++中:
      int *p;
      print(p);//1
      *p=5;
      print(*p);//2
      1打印的結果是什么,一個16進制的地址,2打印的結果是什么?5,也就是指針指向的內容。
      即使在c/c++中,這個指針其實也是一個32位的整數,我們可以理解我一個long型的值。
      而在java中一個對象s是什么,同樣也是一個指針,也是一個int型的整數(對于JVM而言),我們在直接使用(即s2=s這樣的情況,但是對于System.out.print(s)這種情況例外,因為它實際上被晃猄ystem.out.print(s.toString()))對象時它是一個int的整數,這個可以同時解釋賦值的傳引用和傳參數時的傳值(在這兩種情況下都是直接使用),而我們在s.XXX這樣的情況下時s其實就是c/c++中的*s這樣的使用了。這種在不同的使用情況下出現不同的結果是java為我們做的一種簡化,但是對于c/c++程序員可能是一種誤導。java中有很多中這種根據上下文進行自動識別和處理的情況,下面是一個有點極端的情況:
      class t
      {
      public static String t="t";
      public static void main(String[] args)
      {
      t t =new t();
      t.t();
      }
      static void t() {
      System.out.println(t);
      }
      }
      (關于根據上下文自動識別的內容,有興趣的人以后可以看看我們翻譯的《java規則》)
      1、對象是按引用傳遞的
      2、Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞
      3、按值傳遞意味著當將一個參數傳遞給一個函數時,函數接收的是原始值的一個副本
      4、按引用傳遞意味著當將一個參數傳遞給一個函數時,函數接收的是原始值的內存地址,而不是值的副本
      三句話總結一下:
      1.對象就是傳引用
      2.原始類型就是傳值
      3.String類型因為沒有提供自身修改的函數,每次操作都是新生成一個String對象,所以要特殊對待。可以認為是傳值。
      ==========================================================================
    public class Test03 {
    public static void stringUpd(String str) {
    str = str.replace("j", "l");
    System.out.println(str);
    }
    public static void stringBufferUpd(StringBuffer bf) {
    bf.append("c");
    System.out.println(bf);
    }
    public static void main(String[] args) {
    /**
    * 對於基本類型和字符串(特殊)是傳值
    *
    * 輸出lava,java
    */
    String s1 = new String("java");
    stringUpd(s1);
    System.out.println(s1);
    /**
    * 對於對象而言,傳的是引用,而引用指向的是同一個對象
    *
    * 輸出javac,javac
    */
    StringBuffer bb = new StringBuffer("java");
    stringBufferUpd(bb);
    System.out.println(bb);
    }
    }
      解析:就像光到底是波還是粒子的問題一樣眾說紛紜,對于Java參數是傳值還是傳引用的問題,也有很多錯誤的理解和認識。我們首先要搞清楚一點就是:不管Java參數的類型是什么,一律傳遞參數的副本。對此,thinking in Java一書給出的經典解釋是When you’re passing primitives into a method, you get a distinct copy of the primitive. When you’re passing a reference into a method, you get a copy of the reference.(如果Java是傳值,那么傳遞的是值的副本;如果Java是傳引用,那么傳遞的是引用的副本。)
      在Java中,變量分為以下兩類:
      ① 對于基本類型變量(int、long、double、float、byte、boolean、char),Java是傳值的副本。(這里Java和C++相同)
      ② 對于一切對象型變量,Java都是傳引用的副本。其實傳引用副本的實質就是復制指向地址的指針,只不過Java不像C++中有顯著的*和&符號。(這里Java和C++不同,在C++中,當參數是引用類型時,傳遞的是真實引用而不是引用副本)
      需要注意的是:String類型也是對象型變量,所以它必然是傳引用副本。不要因為String在Java里面非常易于使用,而且不需要new,就被蒙蔽而把String當做基本變量類型。只不過String是一個非可變類,使得其傳值還是傳引用顯得沒什么區別。
      對基本類型而言,傳值就是把自己復制一份傳遞,即使自己的副本變了,自己也不變。而對于對象類型而言,它傳的引用副本(類似于C++中的指針)指向自己的地址,而不是自己實際值的副本。為什么要這么做呢?因為對象類型是放在堆里的,一方面,速度相對于基本類型比較慢,另一方面,對象類型本身比較大,如果采用重新復制對象值的辦法,浪費內存且速度又慢。就像你要張三(張三相當于函數)打開倉庫并檢查庫里面的貨物(倉庫相當于地址),有必要新建一座倉庫(并放入相同貨物)給張三么? 沒有必要,你只需要把鑰匙(引用)復制一把寄給張三就可以了,張三會拿備用鑰匙(引用副本,但是有時效性,函數結束,鑰匙銷毀)打開倉庫。
      在這里提一下,很多經典書籍包括thinking in Java都是這樣解釋的:“不管是基本類型還是對象類型,都是傳值。”這種說法也不能算錯,因為它們把引用副本也當做是一種“值”。但是筆者認為:傳值和傳引用本來就是兩個不同的內容,沒必要把兩者弄在一起,弄在一起反而更不易理解。

    posted @ 2014-10-30 11:46 順其自然EVO 閱讀(199) | 評論 (0)編輯 收藏

    什么原因成就了一位優秀的程序員?

    這些年我曾和很多程序員一起工作,他們之中的一些人非常厲害,而另一些人顯得平庸。不久前因為和一些技術非常熟練的程序員工作感覺很愉快,我花了一些時間在考慮我佩服他們什么呢?什么原因讓優秀的程序員那么優秀,糟糕的程序員那么糟糕?簡而言之,什么原因成就了一位優秀的程序員呢?
      根據我的經驗,成為一個優秀程序員同年齡,教育程度,還有和你賺多少錢沒有任何關系。關鍵在于你的做法,更深入地說,就是你的想法。我注意到我所欽佩的程序員都有一些相似習慣。不是他們所選語言的知識,也不是對數據結構和算法的深入理解,甚至不是多年的工作經驗。而是他們的溝通方式,他們管理自己的方式,以及以他們精湛技術水平編程演講的方式。
      當然成為一個優秀的程序員還要具備更多特質,我也不能單單依靠是否存在(或者缺少)這些特質來評判一個程序員。但是我知道當我看見它,當我看見一個程序員具備這些特質的時候,我認為,“這個人真的知道他們正在做什么”。
      他們做調查研究
      或者叫“三思而后行”,或者叫“谷歌一下”
      不論你怎么稱呼它,大多數可能會遇到的編程問題已經以某種形式解決,傳道書早就記載著世界上本來就沒有什么新鮮事。優秀的程序員在解決問題之前知道通過GitHub圖書館、網絡博客,或者通過與經驗豐富的程序員交流等形式來做調查研究。
      我見過甚至是優秀的程序員可以快速找出解決方案,但是和我一起工作過的糟糕的程序員從來不求助于他人,結果做了大量的重復工作或者錯誤地解決問題,不幸的是,后來他們終將為自己犯下的錯誤付出了代價。
      他們閱讀錯誤信息(并按照它們行事)
      這包括解析堆棧路徑信息。是的,這是一件非常不幸的事情。但是如果你不愿意這么做的話,怎么才能知道哪里錯了呢?我知道的高效程序員是不會害怕深究問題的。低效的程序員看見有錯誤,但就是不愿意甚至是去讀這些錯誤信息。(這聽起來很可笑,但你會驚訝我遇到它的頻率)
      更進一步地說,優秀的程序員發現問題馬上就解決它。讀錯誤信息對他們來說僅僅是個開始,他們渴望深究問題并查出問題的根源。他們不喜歡推卸責任,而是愿意查找解決問題的方案,問題在他們這里止步。
      他們去看源代碼
      文檔、測試、團隊,這些都會說謊。盡管不是故意的,但是如果你想確切地知道事情是怎么回事,你必須自己親自看源代碼。
      如果它不是你最擅長的語言,你也不要害怕。如果你是一個Ruby的程序員,你懷疑在Ruby的C語言庫中有個錯誤,破解打開看看。是的,你可能拿不到源代碼,但是誰知道呢?你只是可能而已,你有更好的機會,總比你根本不去嘗試好吧。
      不幸的是,如果你處在一個封閉源代碼的環境中,這會變得非常難,但道理是不變的。糟糕的程序員對于查看源代碼沒有絲毫的興趣,結果問題困擾他們時間,要比愿意看源代碼的時間長得多。
      They just do it
      優秀的程序員趨向于主動去做。他們的內心有著難以控制的沖動,當他們確定問題或者發現新的需求時他們立刻會實現解決方案,有時過早有時太過激進。但是他們對問題本能的反應是正面解決問題。
      有時這會令人很煩惱,但是他們的熱情是他們做好事情的一個重要部分。一些人可能拖延時間回避問題或者等待問題自己能夠消失,然而優秀的程序員一開始就解決它。簡而言之(或者顯而易見),如果你看見有人興致勃勃地查找問題并在解決,很可能你的手下有位優秀的程序員。
      他們避免危機
      這通常是糟糕程序員的特點:他們輕易地從一個人為危機跳到另一個人為危機,在沒有真正理解一個問題之前就進入到下一個問題。他們會把責任歸咎于程序的錯誤,然后花費大把的時間調試已經運行良好的代碼。他們讓情感占據主動,相信直覺,而不是仔細嚴謹的分析。
      如果你匆匆忙忙地解決一個問題,甚至視每一個問題為震驚世界的災難。你很可能犯錯誤或者沒有解決潛在的問題。優秀的程序員花時間去了解發生了什么錯誤,哪怕災難來臨的時候;但更重要的是,他們對待平常的問題像是要解決的重要問題,因此他們更準確地解決更多的問題,并且這樣做沒有提高團隊的緊張程度。
    他們善于溝通交流
      說到底,編程是一種形式的溝通交流。寫代碼和寫散文創作一樣,能夠簡潔地表達你的想法很重要。我發現那些可以寫簡潔郵件,優雅的狀態報告,或者甚至只是一個有效的備忘錄的程序員也將會是優秀的程序員。
      這能應用在寫代碼還有英語上。用圓括號、括號和單個字母的函數寫出一行代碼當然是有可能的,但是如果沒有人理解它,有什么意義呢。優秀的程序員會花時間以各種渠道交流他們的想法。
      他們激情四射
      我認為這可能是優秀的程序員最重要的方面(也許這點也適用于除計算機科學領域的其它領域)
      如果你真的在乎你所做的事情,如果不把它當成工作,當作一個業余愛好、興趣或一件很有吸引力的事情,那么在該領域你比其他人更有優勢。優秀的程序員一直不斷編程。普通程序員一天工作八小時,并且沒有業余項目,也沒興趣回饋社區。他們不會不斷地嘗試新方法,而只是為了看看它們是如何運行而執著于編程語言。
      當我看見一個程序員利用周末的時間做自己喜歡的項目時,參與創作他們每天能用到的工具時,執著于新的有意義的事情時:那個時候我確信我眼前的是一個令人驚奇的人。最后,優秀的程序員視他們的職業不僅僅是賺錢的途徑,更是讓生活變得有些不同的方法。我認為那就是成就最優秀程序員的真正原因。對于他們來說,編寫代碼是改變世界的一種方法,也是我非常尊敬崇拜他們的原因。

    posted @ 2014-10-30 11:42 順其自然EVO 閱讀(139) | 評論 (0)編輯 收藏

    幾個軟件研發團隊管理的小問題

    最近在與一位總經理交流的時候,他談到他們公司的軟件研發管理,說:“我們公司最大的問題是項目不能按時完成,總要一拖再拖。”他問我有什么辦法能改變這個境況。從這樣一個問題開始,在隨后的交談中,又引出他一連串在軟件研發管理中的遇到的問題,包括:
      . 現有代碼質量不高,新來的開發人員接手時寧愿重寫,也不愿意看別人留下的“爛”代碼,怎么辦?
      . 重構會造成回退,怎樣避免?
      . 有些開發人員水平相對不高,如何保證他們的代碼質量?
      . 軟件研發到底需不需要文檔?
      . 要求提交代碼前做Code Review,而開發人員不做,或敷衍了事,怎么辦?
      . 當有開發人員在開發過程中遇到難題,工作無法繼續,因而拖延進度,怎么解決?
      . 如何提高開發人員的主觀能動性?
      其實,每個軟件研發團隊的管理者都面臨著或曾經面臨過這些問題,也都有著自己的管理“套路”來應對這些問題。我把我的“套路”再此絮叨絮叨。
      1. 項目不能按時完成,總要一拖再拖,怎么改變?
      找解決辦法前,當然要先知道問題為什么會出現。這位總經理說:“總會不斷地有需求要改變和新需求提出來,使原來的開發計劃不得不延長。”原來如此。知道根源,當然解決辦法也就有了,那就是“敏捷”。敏捷開發因其迭代(Iterative)和增量(Incremental)的思想與實踐,正好適合“需求經常變化和增加”的項目和產品。在我講述了敏捷的一些概念,特別是Scrum的框架后,總經理也表示了對“敏捷”的認同。
      其實仔細想想,這里面還有一個非常普遍的問題。對于產品的交付時間或項目的完成時間,往往由高級管理層根據市場情況決策和確定。在很多軟件企業中,這些決策者在決策時往往忽略了一個重要的參數,那就是團隊的生產率(Velocity)。生產率需要量化,而不是“拍腦門子”感覺出來的。敏捷開發中有關于如何估算生產率的方法。所以使用敏捷,在估算產品交付時間或項目完成時間時,是相對較準確的。Scrum創始人之一的Jeff Sutherland說,他在一個風險投資團隊做敏捷教練時,團隊中的資深合伙人會向所有的待投資企業問同一個問題:“你們是否清楚團隊的生產率?”而這些企業都很難做出明確的答復。軟件企業要想給產品定一個較實際的交付日期,就首先要弄清楚自己的軟件生產率。
      2. 現有代碼質量不高,新來的開發人員接手時寧愿重寫,也不愿意看別人留下的“爛”代碼,怎么辦?
      這可能是很多軟件開發工程師都有過的體驗,在接手別人的代碼時,看不懂、無法加新功能,讀代碼讀的頭疼。這說明什么?排除接手人個人水平的因素,這說明舊代碼可讀性、可擴展性比較差。怎么辦?這時,也許重構是一種兩全其美的辦法。接手人重構代碼,既能改善舊代碼的可讀性和可擴展性,又不至于因重寫代碼帶來的時間上的風險。
      從接手人心理的角度看,重構還有一個好的副作用,就是代碼重構之后,接手人覺得那些原來的“爛”代碼被修改成為自己引以自豪的新成就。《Scrum敏捷軟件開發》的作者Mike Cohn寫到過:“我的女兒們畫了一幅特別令人贊嘆的杰作后,她們會將它從學校帶回家,并想把它展示在一個明顯的位置,也就是冰箱上面。有一天,在工作中,我用C++代碼實現了某個特別有用的策略模式的程序。因為我認定冰箱門適合展示我們引以為豪的任何東西,所以我就將它放上去了。如果我們一直對自己工作的質量特別自豪,可以驕傲地將它和孩子的藝術品一樣展示在冰箱上,那不是很好嗎?”所以這個積極的促進作用,將使得接手人感覺修改的代碼是自己的了,而且期望能夠找到更多的可以重構的東西。
      3. 重構會造成回退,怎樣避免?
      重構確實很容易造成回退(Regression)。這時,重構會起到與其初衷相反的作用。所以我們應該盡可能多地增加單元測試。有些老產品,舊代碼,可能沒有或者沒有那么多的單元測試。但我們至少要在重構前,增加對要重構部分代碼的單元測試。基于重構目的的單元測試,應該遵循以下的原則(見《重構》第4章:構筑測試體系):
      - 編寫未臻完善的測試并實際運行,好過對完美測試的無盡等待。測試應該是一種風險驅動行為,所以不要去測試那些僅僅讀寫一個值域的訪問函數,應為它們太簡單了,不大可能出錯。
      - 考慮可能出錯的邊界條件,把測試火力集中在哪兒。扮演“程序公敵”,縱容你心智中比較促狹的那一部分,積極思考如何破壞代碼。
      - 當事情被公認應該會出錯時,別忘了檢查是否有異常如期被拋出。
      - 不要因為“測試無法捕捉所有Bug”,就不撰寫測試代碼,因為測試的確可以捕捉到大多數Bug。
      - “花合理時間抓出大多數Bug”要好過“窮盡一生抓出所有Bug”。因為當測試數量達到一定程度之后,測試效益就會呈現遞減態勢,而非持續遞增。
      說到《重構》這本書,其實在每個重構方法中都有“作法(Mechanics)”一段,在重構的實踐中按照上面所述的步驟進行是比較穩妥的,同時也能避免很多不經意間制造的回退出現。4. 要求提交代碼前做Code Review,而開發人員不做,或敷衍了事,怎么辦?
      如果每個開發人員都是積極主動的,Code Review的作用能落到實處。但如果不是呢?團隊管理者需要一些手段促使其有效地進行Code Review。首先,我們采用的Code Review有2種形式,一是Over-the-shoulder,也就是2個人座在一起,一個人講,另一個人審查。二是用工具Code Collaborator來進行。無論哪種形式,在提交代碼時,必須注明關于審查的信息,比如:審查者(Reviewer)的名字或審查號(Review ID,Code Collaborator自動生成),每天由一名專職人員來檢查Checklist中的每一條,看是否有人漏寫這些信息,如果發現會提醒提交的人補上。另外,某段提交的代碼出問題,提交者和審查者都要一起來解決出現的問題,以最大限度避免審查過程敷衍了事。
      博主Inovy在某個評論說的很形象:“木(沒)有賞罰的制度,就是帶到廁所的報紙,看完就可以用來擦屁股了。”沒有獎懲制度作保證,當然上面的要求沒有什么效力。所以,當有人經常不審查就提交,或審查時不負責任,它的績效評定就會因此低一點,而績效的評分是跟每年工資漲落掛鉤的。說白了,可能某個人會因為多次被查出沒有做Code Review就提交代碼,而到年底加薪時比別人少漲500塊錢。
      5. 軟件研發到底需不需要文檔?
      軟件研發需要文檔的起原可能有2種,一是比較原始的,需要文檔是為了當開發人員離職后,企業需要接手的人能根據文檔了解他所接手的代碼或模塊的設計。二是較高層次的,企業遵從ISO9001質量管理體系或CMMI。
      對于第一種,根源可能來自于兩個方面:
      - 原開發人員設計編碼水平不高,其代碼可讀性較差。
      - 設計思想和代碼只有一個人了解,此人一旦離職,無人知道其細節。
      在編碼前寫一些簡單的設計文檔,有助于理清思路,尤其是輔以一些UML圖,在交流時也是有好處的。但同時,我們也應該提高開發人員的編碼水平增加其代碼的可讀性,比如增強其變量命名的可讀性、用一些被大家所了解的設計模式來替代按自己某些獨特思路編寫的代碼、增加和改進注釋等等,以減少不必要的文檔。另外推行代碼的集體所有權(Collective Ownership),避免某些代碼只被一個人了解,這樣可以減少以此為目的而編寫的文檔。
      對于第二種,情況有些復雜。接觸過敏捷開發的人都知道《敏捷宣言》中的“可以工作的軟件勝于面面俱到的文檔”。接觸過CMMI開發或者ISO9001質量管理體系的人知道它們對文檔的要求是多么的高。它們看起來水火不相容。但是,它們的宗旨是一致的,即:構建高質量的產品。
      對于敏捷,使用手寫用戶故事來記錄需求和優先級的方法,以及在白板上寫畫的非正式設計,是不能通過ISO9001的審核的,但當把它們復印、拍照、增加序號、保存后,可以通過審核。每次都是成功的Daily Build和Auto Test報告無法證明它們是否真正被執行并真正成功,所以不能通過ISO9001的審核。但添加一個斷言失敗(類似assert(false)的斷言)的測試后,則可以通過審核。
      CMMI與敏捷也是互補的,前者告訴組織在總體條款上做什么,但是沒有說如何去做,后者是一套最佳實踐。SCRUM之類的敏捷方法也被引入過那些已通過CMMI5級評估的組織。很多企業忘記了最終目標是改進他們構建軟件及遞交產品的方式,相反,它們關注于填寫按照CMMI文檔描述的假想的缺陷,卻不關心這些變化是否能改進過程或產品。
      所以敏捷開發在過程中只編寫夠用的文檔,和以“信息的溝通、符合性的證據以及知識共享”作為主要目標的質量體系文檔要求并不矛盾。在實踐中,我們可以按以下方法做,在實現SCRUM的同時,符合審核和評估的要求:
      - 制作格式良好的、被細化的、被保存的和能跟蹤的Backlog。復印和照片同樣有效。
      - 將監管需要的文檔工作也放入Backlog。除了可以確保它們不被忘記,還能使監管要求的成本是可見的。
      - 使用檢查列表,以向審核員或評估員證明活動已執行。團隊對“完成”的定義(Definition of “Done”)可以很容易轉變為一份檢查列表。
      - 使用敏捷項目管理工具。它其實就是開發程序和記錄的電子呈現方式。
      總而言之,軟件研發需要文檔(但文檔的形式可以是多種多樣的,用Word寫的文字式的文件是文檔,用Visio畫的UML圖也是文檔,保存在Quality Center中的測試用例也是文檔),同時我們只需寫夠用的文檔。
      6. 當有開發人員在開發過程中遇到難題,工作無法繼續,因而拖延進度,怎么解決?
      這也是個常遇到的問題。如果管理者對于某個工程師的具體問題進行指導,就會陷入過度微觀管理的境地。我們需要找到宏觀解決辦法。一,我們基于Scrum的“團隊有共同的目標”這一規則,利用前面提到的集體所有權,當出現這些問題時,用團隊中所有可以使用的力量來幫助其擺脫困境,而不是任其他人袖手旁觀。當然這里會牽扯到績效評定的問題,比如:提供幫助的人會覺得,他的幫助無助于自己績效評定的提高,為什么要提供幫助。這需要人力資源部門在使用Scrum開發的團隊的績效評估中,盡量消除那些傾向個人的因素,還要包含團隊協作的因素,廣泛聽取個方面的意見,更頻繁地評估績效等等。
      二,即使動用所有可以使用的力量,如果某個難題真的無法逾越,為了減少不能按時交付的風險,產品負責人應當站出來,并有所作為。要么重新評估Backlog的優先級,使無法繼續的Backlog遲一點交付,先做一些相對較低優先級的Backlog,以保證整體交付時間不至于延長;要么減少部分功能,給出更多的時間去攻克難題。總之逾越技術上難關會使團隊的生產率下降,產品負責人必須作出取舍。
      7. 有些開發人員水平相對不高,如何保證他們的代碼質量?
      當然首先讓較有經驗的人Review其要提交的代碼,這幾乎是所有管理者會做的事。除此之外,管理者有責任幫助這些人(也包括水平較高的人)提高水平,他們可以看一些書,上網看資料,讀別人的代碼等等,途經還是很多的。但問題是你如何去衡量其是否真正有所收獲。我們的經驗是,在每年大約3月份為每個工程師制定整個年度的目標,每個人的目標包括產品上的,技術上的,個人能力上的等4到5項。半年后和一年后,要做兩次Performance Review,目標是否實現,也會跟績效評定掛鉤。我們在制定目標時,遵循SMART原則,即:
      Specific(明確的):目標應該按照明確的結果和成效表述。
      Measurable(可衡量的):目標的完成情況應該可以衡量和驗證。
      Aligned(結盟的):目標應該與公司的商業策略保持一致。
      Realistic(現實的):目標雖然應具挑戰性,但更應該能在給定的條件和環境下實現。
      Time-Bound(有時限的):目標應該包括一個實現的具體時間。
      比如:某個人制定了“初步掌握本地化技術”的目標,他要確定實現時間,要描述學習的途經和步驟,要通過將技術施加到公司現有的產品中,為公司產品的本地化/國際化/全球化作一些探索,并制作Presentation給團隊演示他的成果,并準備回答其他人提出的問題。團隊還為了配合其實現目標,組織Tech Talk的活動,供大家分享每個人的學習成果。通過這些手段,提高開發人員的自學興趣,并逐步提高開發人員的技術水平。
      8. 如何提高開發人員的主觀能動性?
      提高開發人員的主觀能動性,少不了激勵機制。不能讓開發人員感到,5年以后的他和現在比不會有什么進步。你要讓他感到他所從事的是一個職業(Career),而不只是一份工作(Job)。否則,他們是不會主動投入到工作中的。我們的經驗是提供一套職業發展的框架。框架制定了2類發展道路,管理類(Managerial Path)和技術類(Technical Path),6個職業級別(1-3級是Entry/Associate,Intermediate,Senior。4級管理類是Manager/Senior Manager,技術類是Principal/Senior Principal。5級管理類是Director/Senior Director,技術類是Fellow/Architect。6級是Executive Management)。每個級別都有13個方面的具體要求,包括:范圍(Scope)、跨職能(Cross Functional)、層次(Level)、知識(Knowledge)、指導(Guidance)、問題解決(Problem Solving)、遞交成果(Delivering Result)、責任感(Responsbility)、導師(Mentoring)、交流(Communication)、自學(Self-Learning),運作監督(Operational Oversight),客戶響應(Customer Responsiveness)。每年有2次提高級別的機會,開發人員一旦具備了升級的條件,他的Supervisor將會提出申請,一旦批準,他的頭銜隨之提高,薪水也會有相對較大提高。從而使每個開發人員覺得“有奔頭”,自然他們的主觀能動性也就提高了。
      上面的“套路”涉及了軟件研發團隊管理中的研發過程、技術實踐、文檔管理、激勵機制等一些方面。但只是九牛一毛,研發團隊管理涵蓋的內容還有很多很多,還需要管理者在不斷探索和實踐的道路上學習和掌握。

    posted @ 2014-10-30 11:42 順其自然EVO 閱讀(202) | 評論 (0)編輯 收藏

    從工程師到管理者的飛躍

      曾有人問過我,“管理者什么的,跟開發人員到底有什么區別?”這兩個角色都是我經歷過的,但我仍花了一點時間來考慮。這個問題真的蠻重要的。
      編程是從我六歲就開始的消遣。那時我寫了第一個程序:從我爸爸的書里照抄了一段游戲的源代碼,隨即就著了迷,并且一直未曾放棄,直到編程成了我的事業。多年來,在我解決了各種有趣的或者復雜的編程問題之后,我覺得是時候去迎接新的挑戰了。
      但是轉行就意味著放棄,放棄我多年來磨練出來的專業技能。然而,經過一番掙扎與向專業導師咨詢之后,我毅然決然的跨出了這一步。
      現在,干了三年半的管理,我終于有資格來回答這個問題了。管理者和開發人員最大的區別就在于衡量成功的標準不同。
      具體來說:
      (一) 你的成功會更瑣碎
      當我還是一個程序員的時候,每天來上班腦子里都會有一個工作計劃,通常這天結束時,我都能完成好這個計劃。這種感覺就像是每一天我都在進步。
      而作為一個經理,常常在回家的時候,都不知道我那天到底干了啥。并不是我什么事都沒做,只是實在沒有可供衡量的結果。
      身為管理者,任務之一就是幫助工程師去做改變,但改變不會是一朝一夕可以完成的,需要時間和關注。
      ·你努力去實現的變化,可能模棱兩可并且很難有清晰的定義。
      ·要認識到需要改變,這件事本身也可能很難。
      ·工程師們很難拋棄舊的習慣,需要不斷的提醒他們。要改變他們的思維定勢,是一件有挑戰性的而且不輕松的事。
      在 New Relic,我們每季度都會舉辦一個定期檢查,用于提供一個反饋的渠道,讓工作的重點放在長期的目標上。有些季度的發展可能突飛猛進,但大多數時候,會有的只是還叫不錯的進步。
      當團隊人員真正出現大的變化時,就需要管理者不斷的引導。我們經常使用的工具叫做“regular info-bits(定期的信息交流)”。具體做法是,工程師把他們工作上的進展用簡短的 email 發給管理者。Email 的主題通常與專業發展,團隊合作,項目更新,溝通交流和工作與生活的平衡有關。這個過程有助于他們更加系統的去思考問題,你也可以通過這些信息,獲悉他們的成長。
      這種做法會需要很長的時間才能看到結果,但是最終可以看到你團隊的成員們建立了自信并且成長良好,你會覺得一切辛苦沒有白費。
      (二)你的成功有戰略上的影響
      好的開發者可以對企業造成巨大的長期的影響,好的經理會引導整個團隊的成功。
      工程上的問題常常不是黑就是白。但是人的問題,幾乎總是模棱兩可的。即使你知道你要解決的問題是什么,解決的方法卻并不是總是那么清楚。過去你用這個方法解決了這個人的問題,不代表你就可以用它解決現在的問題。人類行為這個東西實在是有太多的變量了。
      要建立一個合作無間的團隊,就有一系列的挑戰:
      ·團隊的建設并不是把一個個明星成員拼湊起來這么簡單。
      ·一個有凝聚力的團隊需要充分理解每個人的長處和短處。
      ·團隊不會是一成不變的,每當有人加入或者離開,都需要重新磨合。
      作為經理,你的工作是確保你的團隊盡可能的高效的運行。但你不能指望稍有變動,就來一次大刀闊斧的革新,不能指望流程上動輒就做徹底的改變。有效的管理者需要:
      ·不斷的評估你的團隊需要什么幫助。
      ·注意,這個需求對于不同的工作和不同的團隊可能是不一樣的。
      ·使用和發明正確的工具來支持你的團隊。
      這個關鍵是逐漸的變化,不斷的觀測,而后不斷的做出改進。例如,如果你發現你的團隊沒能得到足夠力度的支持,首先找到辦法使局面不要那么混亂,而后再尋求工具來優化你的團隊。和你的小伙伴們一起努力克服困難,先選擇重要的問題來處理,然后回頭檢查這個變動是否得當。
    (三)你的成功往往就是幫助別人獲得成功
      在 New Relic,我們相信 Invisible Manager(隱形的經理)這個理念。這意味著我們在幕后工作,讓工程師站在聚光燈下,突出他們的成功。我們確信工程師應該獲得榮譽,這是他們應得的。所以 New Relic 的新功能推介會,我們鼓勵讓團隊成員站在公眾面前去介紹產品,而不是產品經理或工程經理。
      大多數人管理者之前都是成功的工程師。而作為一個經理,會產生更大的影響,不僅在生意上,也在員工的生活里。許多管理者會發現,幫助別人也是幫助自己成長。
      先管理好你自己
      對于有工程背景的我來說,專注于手頭上的問題比什么都重要。在我心里總是有一個完整的計劃,涵蓋了團隊所需要的一切:成長,動態,質量,支持,產品交付,會議,博客發表等等。我會有一個清晰的愿景:我的團隊在未來一年里要成為什么樣子。這有助于我做出日常的每一個決定,而最終走向我們長期的目標。
      要管理好一個團隊需要大量的工作。但它也帶來了大量的喜悅和自豪。作為經理你能做的最好的事情就是思考。一個月一次,找一個安靜的空間,去想想你團隊里的每個人,去看看舊的郵件和項目報告。你會發現你的影響力,不僅加諸于你的產品,還影響了你團隊里的人。這就是成功的真正標準。

    posted @ 2014-10-30 11:41 順其自然EVO 閱讀(163) | 評論 (0)編輯 收藏

    項目管理修煉之道

    1. 你的項目每天都在加快節奏,
      2.你的客戶變得越來越不耐煩,
      3.大家越來越不能容忍無法正常工作的產品。
      4.管理項目的關鍵驅動因素,約束和浮動因素;
      5.確定產品的發布條件;
      6.制定項目風險列表;
      7.確定當前項目最重要的因素;
      8.拒絕鍍金,滿足要求,能夠使用就是最好的項目承諾。
      9.日期等于承諾;
      10.好的項目管理工具,好的項目源代碼管理,好的軟件缺陷跟蹤工具;
      11.提升人際交往技能;
      12.提升功能性技能;
      13.提升專業領域性專業知識技能;
      14.提升工具和技術的專業技能;
      15.首先實現具有最高價值的功能;
      16.多幾只眼睛盯著產品;
      17.確定產品的關鍵流程,并能跑通;
      18.每日站立會議,一對一會議,通過可見的方式管理進度,每周獲取和報告進度情況。
      19.從一開始,就要做一個功能是一個功能,不能形成技術債務;
      20.學會對多任務的情況說不。

    posted @ 2014-10-30 11:41 順其自然EVO 閱讀(184) | 評論 (0)編輯 收藏

    有關管理客戶需求的一點見解

     軟件開發難,恐怕大家都覺得最難的是搞清楚需求;但是其實更難的是管理需求。今天在北京.NET俱樂部上又有人提出了這樣的問題,主要的難點是他的開發團隊是為了自己的領導們服務的,幾個領導都有自己的想法,而且不停的在開發過程中提各種個樣的問題;開發進度無法保證,開發的結果總是滿足不了要求……
      其實這樣的問題大家都遇到過,而對于普通的開發人員來說我們往往不去關心,認為這是項目經理的事情,但是其實不然,這樣的問題涉及軟件開發的各個環節,就算你是出于最底層的開發人員,一樣需要控制項目經理交給你的任務。其實這里最重需要把握的一點就是:把任務控制在你能控制的范圍之內。總結一下,我的經驗如下:
      第一:無論你的客戶是誰,我們永遠需要一個中介來接受需求;你首先需要和客戶有個協議,需要他們制定某一個人來提所有的需求,這個人不需要是很高職位的人,而且往往最好的選擇是中層的技術管理人員;用戶的所有需求必須通過這個人的認可,就算是對方老總提出的要求,如果沒有這個人的認可我們也不執行。這點非常重要,可是替我們減少許多麻煩。
      第二:無論是什么樣的軟件開發過程理論現在都承認一個問題,那就是軟件開發需要迭代。而且我們一定要面對一個現實,就是軟件開發的過程是在不斷的變化中尋找平衡的過程,我們的需求永遠不會結束,我們的軟件永遠都在被修改;修改不是壞事,但是我們必須要保證在一定的時候可以拿出成果。
      所以,控制迭代的增量就是非常重要的。一般我們公司的做法是,以兩周為一個周期最為一個Release,一旦這個Release開始以后,任何用戶的新需求就都需要放到后面的Release;我們不會決絕客戶的需求,但是我們必須管理我們可以承受的進度。這樣做的最大好處在于,在兩周的時間內,我們一定可以為客戶提供一個更好的版本,這可能不是客戶現在心目中的最終結果(因為很多新需求都在后面的Release中),但是我們至少完成了我們在兩周前所承諾的結果,客戶得到他們想要的東西(當然不是全部),我們也可以很明確的告訴客戶,我們完成什么樣的需求。
      而且在這樣一個迭代的過程中,我們會發現很多需求中的不完善之處,每兩周的時間我們都可以針對開發方向作相應調整。最終的結果是保證了客戶的滿意度,同時也保證了產品的按期交付。
      在這里,我們需要明確的區分修改bug的需求和新功能的需求,bug應該是那些對軟件主要功能造成決定性影響的缺陷,這些東西無論是我們開發人員自己發現的還是客戶反饋的,都必須在當前的Release處理完;而新需求則必須放到后面的Release中去。明確區分這兩種不同需求對軟件項目的成功起到決定性作用。
      第三:我們需要學會管理客戶。可能有人覺得我在胡扯,客戶怎么可能被管理,他們是上帝啊??!!其實上帝也是人,而且是通事理的人。我們對客戶永遠不應該是100%的服從,正確的方式是控制用戶對開發進度的期望值,盡量使他們一致。當然有些時候我們需要更強硬一點點,比如我就經常很直接的告訴我的老板,這個需求屬于新功能,必須放到后面的Release中去。

    posted @ 2014-10-30 11:40 順其自然EVO 閱讀(181) | 評論 (0)編輯 收藏

    需求管理是需求開發的基礎

     為什么cmmi建議需求管理在2級實施、而需求開發在3級實施呢?以前看cmmi的時候對這個是有疑問的,但是當時問了其他人也沒有人很清楚,也就睜一眼閉一眼了。這次培訓后,我從“成熟的過程有利于新技術的引入”的思想中得到一些啟發,我覺得是不是cmmi認為,只有把需求管理做好了,做到了對需求管理理念的理解和認同,繼而形成了好的習慣之后,需求開發作為一種新的技術,是相關管理人員在了解了自己的需求現狀(有度量和分析)后,很樸素的和必然的要考慮的問題就是“如何把需求做得更好?”,相應的自然的就回去尋求如何“開發好的需求”。不知道,我這么理解對不對?
      你的思路是對的。規范的項目過程能力,有助技術的提高,需求開發也是一樣。我們需要明白哪方面的規范,可以幫助需求開發的提高。你能夠看得通,可喜可賀!
      但是你“睜一眼閉一眼”的態度就非常不好了。
      問題的答案早就在CMMI的描述里。當然,在二級的時候,我們也有需求開發的,否則項目就不可能有交付產品。但是很多時候,我們的需求做的不夠規范,沒有專員負責,需求的內容,往往是不同的開發人員補充自己的任務部分,需求不能一致、不能滿足客戶,質量不能提高。
      那么,如何才能提高需求質量?CMMI的需求管理要求:1)需求是項目與客戶的了解一致、項目按著需求開展活動,以實現需求為目標。2)一個真心這樣做的項目,它非得到客戶真正的需求不可。開始的時候,我們的技巧未必可以達到這一點,真正明白客戶的需求。但是如果我們接受以客戶為中心,極力爭取客戶滿意,我們就會不斷地找方法把抽取需求的方法加以完善。這就是第三級專心要做的。但是基礎,就是第二級的“項目就是要實現客戶滿意的需求”這個概念上的。你應該留意到,我們的項目還沒有建立這個強烈的意愿,要按需求開展項目活動,所以我們連建立系統工程師團隊都不愿意好好地做。3)要實現需求,就需要需求跟蹤,其意義在于確保所有需求到不多不少地得到實現。我們就需要盯著需求的變更,否則我們的工作就不是真正實現了最終版本的需求了。這一步是保證需求得到忠實實現必要的舉措。
      以上各點,都是CMMI二級要求的。就是說,我們二級的時候,是有需求的,但是不規范,因為我們還不了解需求的意義。這就是我說的:“我們還不尊重需求”。當項目還不尊重需求的時候,需求是提高不了的。這里“需求管理”里面的”管理“,不單單是一般的管理任務而已,它是通過這些任務,表達一個目標,這個 “需求管理”,更像是“需求意識”。就是說,知道需求的意義,重要性,與項目的關系,等等之后,必然采取的舉措。CMMI列出這些舉措,其實是要求項目建立需求意識。
      其實,這里的“管理”可以有兩個含義。字面上,他就是有一些“需求”,管理,就是如何處理它。這個含義,讓人自然地想到,如果我們沒有好需求,需求管理,就自然沒有意義。另一個含義,就是驅動管理活動的思路與方法,而不一定是管理的實際活動。我們需要知道需求的重要性,以及它的關鍵因素,才能最有效地管理它。這里的管理,含義在于創造有利條件,才能提高需求質量。
      讓我再舉一個案例:剛才收看了CCTV4的“尋寶”節目。有些觀眾,拿來評審的文物是假的,有些是非常寶貴的。有些對考古有認識,有些沒有。自己在家里收藏古董當然無所謂。但是如果我們要當一位規范的古董鑒賞家,我們是否需要在家里(CMMI第二級)學習古董的價值與收藏方法(需求管理),才可以放膽投資在真正的古董上面(需求開發)?
      所以需求管理,是需求開發的基礎。這個跟你的說法是非常一致的。

    posted @ 2014-10-30 11:39 順其自然EVO 閱讀(177) | 評論 (0)編輯 收藏

    核心業務需求及邏輯架構分析

     12306的已知信息、數據及問題
      需求分析(一)—— 售票系統領域知識(區間票、訂票、預留票)
      需求分析(二)—— 涉眾、用戶體驗
      核心業務需求及邏輯架構分析
      需求分析(三)—— 票倉
      票倉設計(一)—— 預生成車票方案的優缺點
      票倉設計(二)—— 區間二進制方案的優缺點
      票倉設計(三)—— 平衡方案的優缺點
      票務并發沖突處理原則設計(基于平衡方案)
      緩存邏輯架構設計
      數據庫邏輯設計
      災難備份與恢復
      快要太監了 :-(
      由于各種個人原因, 鐵道部的這個博文系列中止了很久。最近終于連自己都不好意思了。所以還是繼續完成它吧,估計1-2周一篇的節奏。
      感覺不先劃分一下大的系統架構總會讓大家感覺有點頭暈, 不過沒能力對整個12306進行設計,這個貨太大了。只是借這個機會談談自己對系統結構分析的一些感想
      樸素的面向對象分析
      面向對象是一個萬金油,但是據說真正懂的人不多是吧?
      我對面向對象的感覺就是: 他本質上是對現實世界的抽象,其表面現象是不斷細分對象的粒度,提升對象的抽象度。最終形成一種用有限數量的獨立的對象“積木”構造整個需求不斷變化的系統的目標。
      而系統級別的分析也大致如此,我們可以借鑒類分析中的很多概念,不斷劃小系統規模,剝離職責,抽出依賴性。
      一般系統結構
      這里只是一個簡單模型,用以表達我對多數項目的結構分析。
      配置數據服務:系統運行所需要的動態配置信息
      資產數據服務:所有實際或虛擬的“物”的管理(CRUD),甚至可以包括人。
      業務數據服務:該企業實際經營的業務產生的數據。超市的收銀記錄,企業的銷售記錄,鐵道部的售票記錄
      報表數據服務:各類統計報表需要的
      其中業務系統和業務數據服務應該是最核心的部分。
      一般而言,那些配置和資產管理的部分不會造成嚴重的性能問題。只要在實現CRUD的時候多考慮考慮相關的業務需求,努力做到任何資產的屬性變動時,確保相關的業務完整性就好(出租公司管理系統里,一輛出租車今天還在運營,后臺系統絕對不應該可以輕松地把它標記成報廢車輛,連軟刪除都是不合理的做法)。
      12306之所以能招全國人民圍觀,我覺得主要還是花的錢和大家的感受之間有落差。而我陰暗的以為這個問題的核心部分就在票務處理的部分。
      所以我后續的幾篇博文都會圍繞票務處理里面的內容展開。
      另外,我要大家了解的是,我是要設計一個合理的區間票售票系統核心。而不是實現鐵道部的需求。本質上我認為鐵道部不會說清楚他自己的需求,而太極公司的需求分析有可以進一步深挖的可能。
      12306核心需求及模塊分析
      整體架構沒法子設計,太大了。有興趣的可以參考
      中國鐵路客票發售和預訂系統5_0版的研究與實現
      國鐵路客票發售和預訂系統5.0版本(TRSv5.0)售票與經由維護操作說明
      目前我專注的是用于訂票的部分。我感覺這個是最重要的部分。
      12306最大的問題,就是如何在訂票的時候高效率得并且適當優化得找到需要數量的車票。并且要能徹底保證一張票不會被兩個請求同時處理成功。
      只要這個問題無法徹底解決,任何分布式處理,最終都會卡在這個問題上。
      我會涉及到的模塊
    12306票務處理功能模塊分析
      假想完整網絡訂票流程圖
      這里實際的用戶和系統的交互有4種類型。
      1、車次和余額查詢
      2、訂票
      3、取消訂票
      4、確認訂票
      這里希望廣大圍觀群眾都來評估一下這個假設的訂票流程及其參數是不是都合理?如果這個流程本身不合理,則我后續的分析都要重寫了。不熟悉售票流程的,可以看看我之前的分析文章。
      然后我們繼續來細化一下
      車次和余額查詢
      輸入:起始站,終到站,日期,座位類型集合
      輸出:車次,對應座位類型可售余額
      作用:最終用戶根據查詢結果選擇需要出行的車次,并進一步進入訂票操作。但是系統不保證顯示為有票的車次在下一步操作中必然有票。
      這里其實涉及到兩個類型的查詢
      1、哪些車次符合用戶的查詢結果,可以通過一個基本固定不變的數據源來提供。而該數據源可以實現分布式緩存以緩解查詢壓力,甚至可以考慮客戶端部分結果緩存。
      輸入:起始站,終到站,日期
      輸出 :車次列表,
      2、特定車次,特定座位類型的可售票數量。這個數據的來源應該和第一個查詢不同。
      輸入:起始站,終到站,車次,日期
      輸出:數量
      訂票(我喜歡稱它為鎖票)
      輸入:起始站,終到站,日期,座位類型,需要車票數量,用戶ID
      輸出:實際到的獲取車票數量
      作用:最終用戶通過訂票操作,順利鎖定需要數量的車票。系統保證用戶在規定的時間段內對這幾張車票具有優先訂購權利,且其他人不得購買這些車票。
      目前我感覺留給用戶10-15分鐘時間繼續后續操作,以進入支付環節(當然,這必須是在系統本身性能良好條件下。否則點個按鈕就要等10分鐘,那就不對了。)
      同時如果超時,則系統會在后續訂票操作中忽視該鎖定狀態。
      取消訂票
      輸入:起始站,終到站,日期,座位類型,數量,用戶ID
      輸出:成功標志
      作用:用戶放棄已經獲得的被鎖定的售票權利,系統恢復對應的數據為可售。
      確認訂票(確認支付)
      輸入:起始站,終到站,日期,座位類型,數量,用戶ID,支付相關信息
      輸出:成功標志/確認失敗(剛好鎖定超時,且被他人訂走)
      作用:最終確認售票,系統向第三方支付服務提交確認請求。

    posted @ 2014-10-30 11:39 順其自然EVO 閱讀(299) | 評論 (0)編輯 收藏

    僅列出標題
    共394頁: First 上一頁 26 27 28 29 30 31 32 33 34 下一頁 Last 
    <2025年5月>
    27282930123
    45678910
    11121314151617
    18192021222324
    25262728293031
    1234567

    導航

    統計

    常用鏈接

    留言簿(55)

    隨筆分類

    隨筆檔案

    文章分類

    文章檔案

    搜索

    最新評論

    閱讀排行榜

    評論排行榜

    主站蜘蛛池模板: 亚洲AV无码资源在线观看| 69pao强力打造免费高清| 亚洲成aⅴ人片在线观| 亚洲男人的天堂在线va拉文| 国产大片91精品免费观看不卡| 久久久久女教师免费一区| 亚洲AV无码专区国产乱码不卡| 亚洲酒色1314狠狠做| 亚洲情XO亚洲色XO无码| 亚洲国产精品一区二区三区久久| 99久久这里只精品国产免费 | 最近2019中文字幕免费看最新| 免费观看在线禁片| jizz中国免费| 特级毛片爽www免费版| 亚洲精品蜜夜内射| 亚洲乱码在线卡一卡二卡新区| 久久亚洲精品无码aⅴ大香| 亚洲色欲一区二区三区在线观看| 亚洲AV日韩精品一区二区三区| 成全视频免费高清| 无码视频免费一区二三区| 亚洲一区在线免费观看| 老汉精品免费AV在线播放| 中文字幕久精品免费视频| 黄桃AV无码免费一区二区三区| 另类专区另类专区亚洲| 成人婷婷网色偷偷亚洲男人的天堂 | 国产日产亚洲系列最新| 亚洲成av人在片观看| 亚洲Av无码乱码在线znlu| 又黄又爽一线毛片免费观看| 日韩a级毛片免费观看| 无码国模国产在线观看免费| 暖暖在线日本免费中文| 永久免费bbbbbb视频| 午夜毛片不卡高清免费| 永久黄网站色视频免费观看 | 老司机免费午夜精品视频| 香港经典a毛片免费观看看| 美女黄色免费网站|