Java并发编程技术知识点梳理(第五篇)多线程与网络编程

Java IO与多线程

多线程在Socket编程中的作用

IO可以分为文件IO网络IO等等。本篇我们主要使用网络IO来介绍。
对于标准的网络IO来说,我们一般使用Socket进行网络的读写,为了让服务器可以支持更多的客户端连接,通常为每一个客户端连接开启一个线程。
在这里插入图片描述
服务器会为每一个客户端连接启用一个线程,这个线程将只为这个客户端服务。同时,为了接受客户端连接,服务器还会额外使用一个派发线程。

Socket多线程网络编程案例

服务端代码如下:

package test30;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * 服务端处理消息线程
 * 读取客户端消息之后
 * 计算每个客户端消息的耗时
 */
public class HandleMsg implements Runnable {
    private Socket clientSocket;

    public HandleMsg(Socket clientSocket) {
        this.clientSocket = clientSocket;
    }

    @Override
    public void run() {
        BufferedReader is = null;
        PrintWriter os = null;
        try{
            is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            os = new PrintWriter(clientSocket.getOutputStream(),true);
            String inputLine = null;
            long b = System.currentTimeMillis();
            while ((inputLine = is.readLine())!=null){
                os.println(inputLine);
            }
            long e = System.currentTimeMillis();
            System.out.println("spend:"+(e-b)+"ms");
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            try{
                if(is!=null) is.close();
                if(os!=null) os.close();
                clientSocket.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }
}
package test30;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 服务端启动类
 * 监听客户端的连接
 * 连接成功之后,
 * 使用newCachedThreadPool()类型的线程池提交客户端任务,相当于为每个客户端开启一个线程
 */
public class ServerTest {
    public static void main(String[] args) {
        ExecutorService es = Executors.newCachedThreadPool();
        ServerSocket echoServer = null;
        Socket clientSocket = null;
        try{
            echoServer = new ServerSocket(8000);
        }catch (IOException e){
            System.out.println(e);
        }
        while(true){
            try{
                clientSocket = echoServer.accept();
                System.out.println(clientSocket.getRemoteSocketAddress()+" connect!");
                es.execute(new HandleMsg(clientSocket));
            }catch (IOException e){
                System.out.println(e);
            }
        }
    }
}

客户端代码如下:

package test30;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.locks.LockSupport;

/**
 * 客户端线程,负责向服务端发送消息
 * 并接受服务端返回的消息
 */
public class EchoClient implements Runnable {
    private final int sleep_time= 1000*1000*1000;
    @Override
    public void run() {
        Socket client = null;
        PrintWriter writer = null;
        BufferedReader reader = null;
        try{
            client = new Socket();
            client.connect(new InetSocketAddress("localhost",8000));
            writer = new PrintWriter(client.getOutputStream(),true);
            writer.print("H");
            LockSupport.parkNanos(sleep_time);
            writer.print("e");
            LockSupport.parkNanos(sleep_time);
            writer.print("l");
            LockSupport.parkNanos(sleep_time);
            writer.print("l");
            LockSupport.parkNanos(sleep_time);
            writer.print("o");
            LockSupport.parkNanos(sleep_time);
            writer.print("!");
            LockSupport.parkNanos(sleep_time);
            writer.println();
            writer.flush();
            reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
            System.out.println("from server: "+reader.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try{
                if(writer!=null) writer.close();
                if(reader!=null) reader.close();
                if(client!= null) client.close();
            }catch (IOException e){
                e.printStackTrace();
            }

        }
    }
}
package test30;

import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 使用newCachedThreadPool类型的线程池提交10次任务
 * 相当于开启10个线程
 */
public class ClientTest {
    public static void main(String[] args) throws IOException {
        ExecutorService es = Executors.newCachedThreadPool();
        EchoClient ec = new EchoClient();
        for (int i = 0; i < 10; i++) {
            es.execute(ec);
        }
    }

}

程序的运行结果如下所示
服务端运行结果
在这里插入图片描述
客户端运行结果在这里插入图片描述
在这个案例中,服务器之所以处理慢,并不是因为在服务端有多少繁重的任务,而是应为服务线程在等待IO,让高速运转的CPU去等待及其低效的网络IO是非常不合算的行为,那么,我们需要想一个办法,将网络IO的等待时间从线程中分离出来

Java NIO与多线程

使用Java nio就可以将网络io等待时间从业务处理线程中抽取出来。

NIO中关键组件

Channel(通道):类似于普通Java IO编程中的流。这个Channel可以和文件或者网络Socket对应。如果Channel对应着一个Socket,那么往Channel中写数据,就等于向Socket中写入数据。
Buffer(缓冲区):负责数据存储。数据需要包装成Buffer形式才能和Channel交互(写入或者读取)。
Selector(选择器):在Channel的众多实现中,有一个SelectableChannel实现,表示“可被选择的通道”,任何一个SelectableChannel都可以将自己注册到一个Selector中,因此这个Channel就可以被Selector管理。一个Selector可以管理多个SelectableChannel。当SelectableChannel的数据准备好时,Selector就会接到通知,得到那些已经准备好的数据,而SocketChannel就是SelectableChannel的一种。
它们的关系如下图所示。
在这里插入图片描述

NIO案例

服务端代码:

package test31;

/**
 * 服务端启动类
 */
public class ServerTest {
    public static void main(String[] args) {
        MultiThreadNIOEchoServer echoServer = new MultiThreadNIOEchoServer();
        try {
            echoServer.startServer();
        } catch (Exception e) {
            System.out.println("Exception caught, program exiting...");
            e.printStackTrace();
        }
    }
}
package test31;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 服务端资源操作类
 */
public class MultiThreadNIOEchoServer {
    //定义一个选择器
    private Selector selector;
    private ExecutorService es = Executors.newCachedThreadPool();
    //统计服务器线程在一个客户端花费的时间,定义一个与时间统计的变量
    Map<Socket,Long> time_stat = new HashMap<>(10240);

    /**
     * 启动服务器方法
     * @throws Exception
     */
    public void startServer() throws Exception{
        //得到selector对象实例
        selector = SelectorProvider.provider().openSelector();
        //得到服务器的ServerSocketChannel实例
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //将其设置为非阻塞方式。
        ssc.configureBlocking(false);

        //进行端口绑定,将通道绑定至8000端口
        //InetSocketAddress isa = new InetSocketAddress(InetAddress.getLocalHost(),8000);
        InetSocketAddress isa = new InetSocketAddress(8000);
        ssc.socket().bind(isa);
        //将ServerSocketChannel绑定到Selector上,并注册它的事件为Accept
        SelectionKey acceptKey = ssc.register(selector,SelectionKey.OP_ACCEPT);
        while (true){
            //阻塞方法,没有数据准备好,等待;有数据可读,返回
            selector.select();
            //selector可同时为多个channel服务,所以已经准备就绪的channel可能是多个。
            Set readyKeys = selector.selectedKeys();
            Iterator i = readyKeys.iterator();
            long e = 0;
            while (i.hasNext()){
                SelectionKey sk = (SelectionKey)i.next();
                //获取到selectionKey后,就将其从集合中移除,以免重复处理
                //注:正确删除集合元素的方法,是使用迭代器
                i.remove();
                //判断当前selectionKey是否在Acceptable状态,如果是,就进行接收客户端内容
                if(sk.isAcceptable()){
                    doAccept(sk);
                }else if(sk.isValid()&& sk.isReadable()){
                    //记录一下此客户端连接的时间,在读取前的时间戳。
                    if(!time_stat.containsKey(((SocketChannel)sk.channel()).socket())){
                        time_stat.put(((SocketChannel)sk.channel()).socket(),System.currentTimeMillis());
                    }
                    doRead(sk);
                }else if(sk.isValid()&&sk.isWritable()){
                    doWrite(sk);
                    e = System.currentTimeMillis();
                    //写出完成后,移除该socket,并返回读入前的时间戳。
                    long b = time_stat.remove(((SocketChannel)sk.channel()).socket());
                    System.out.println("spend:"+(e - b)+"ms");
                }
            }

        }
    }

    /**
     * 与客户端建立连接的方法
     * @param sk
     */
    private void doAccept(SelectionKey sk) {
        ServerSocketChannel server = (ServerSocketChannel)sk.channel();
        SocketChannel clientChannel;
        try{
            clientChannel = server.accept();
            //设置为非阻塞模式,要求系统在准备好IO之后,就通知线程来读取或者写入
            clientChannel.configureBlocking(false);
            //注册此通道为读
            //当selector发现这个channel已经准备好读时,就能给线程一个通知
            SelectionKey clientKey = clientChannel.register(selector,SelectionKey.OP_READ);
            //分配一个客户端Bean实例,并绑定在clientKey,这样在整个连接的处理过程中,我们可以共享此实例
            Echo echo = new Echo();
            clientKey.attach(echo);

            InetAddress clientAddress = clientChannel.socket().getInetAddress();
            System.out.println("Accepted connection from: "+clientAddress.getHostAddress()+".");
        }catch (Exception e){
            System.out.println("Failed to accept new client.");
            e.printStackTrace();
        }
    }

    /**
     * 当channel可以读取时,doRead()方法就可以被调用
     * @param sk
     */
    private void doRead(SelectionKey sk) {
        SocketChannel channel = (SocketChannel)sk.channel();
        ByteBuffer byteBuffer=ByteBuffer.allocate(8192);
        int len;
        try{
            len = channel.read(byteBuffer);
            if(len<0){
                disconnect(sk);
                return;
            }
        }catch (Exception e){
            System.out.println("Failed to read from client.");
            e.printStackTrace();
            disconnect(sk);
            return;
        }
        //读取完成后,重置缓冲区
        byteBuffer.flip();
        es.execute(new HandleMsg(sk,byteBuffer,selector));
    }

    /**
     * 回写至客户端
     * @param sk
     */
    private void doWrite(SelectionKey sk) {
        SocketChannel channel = (SocketChannel)sk.channel();
        Echo echo = (Echo)sk.attachment();
        LinkedList<ByteBuffer> outputQueueContent = echo.getOutqueue();
        //获得列表顶部元素
        ByteBuffer byteBuffer = outputQueueContent.getLast();
        try{
            int len = channel.write(byteBuffer);
            if(len == -1){
                disconnect(sk);
                return;
            }
            if(byteBuffer.remaining() == 0){
                outputQueueContent.removeLast();
            }
        }catch (Exception e){
            System.out.println("Failed to write to client.");
            e.printStackTrace();
            disconnect(sk);
        }
        //全部数据发送完成后,需要将写事件从感兴趣的操作中移除。
        if(outputQueueContent.size()==0){
            sk.interestOps(SelectionKey.OP_READ);
        }
    }

    private void disconnect(SelectionKey sk) {
        SocketChannel channel = (SocketChannel) sk.channel();
        InetAddress clientAddress = channel.socket().getInetAddress();
        System.out.println(clientAddress.getHostAddress() + " disconnected.");
        try {
            channel.close();
        } catch (Exception e) {
            System.out.println("Failed to close client socket channel.");
            e.printStackTrace();
        }
    }
}
package test31;


import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;

/**
 * 服务端处理消息线程
 * 读取客户端消息
 */
public class HandleMsg implements Runnable {
    private SelectionKey sk;
    private ByteBuffer bb;
    private Selector selector;

    public HandleMsg(SelectionKey sk, ByteBuffer bb, Selector selector) {
        this.sk = sk;
        this.bb = bb;
        this.selector = selector;
    }

    @Override
    public void run() {
        Echo echo = (Echo)sk.attachment();
        echo.enterQueue(bb);
        //我们将数据处理完成后,还会回写客户端,所以将读和写都提交,这样在
        //通道准备好写入时,就能通知线程。
        sk.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);
        //强迫selector立即返回
        selector.wakeup();
    }
}

package test31;

import java.nio.ByteBuffer;
import java.util.LinkedList;

/**
 * 客户端实例类,封装队列,保存在需要回复给此客户端
 * 的所有信息
 */
public class Echo {
    private LinkedList<ByteBuffer> outqueue;

    public Echo() {
        this.outqueue = new LinkedList<>();
    }

    /**
     * 返回队列
     * @return
     */
    public LinkedList<ByteBuffer> getOutqueue() {
        return outqueue;
    }

    /**
     * 进入队列
     * @param byteBuffer
     */
    public void enterQueue(ByteBuffer byteBuffer) {
        this.outqueue.addFirst(byteBuffer);
    }
}

客户端代码如下:

package test31;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.Iterator;

public class NIOClient {
    private Selector selector;
    public void init(String ip, int port) throws IOException {
        SocketChannel channel = SocketChannel.open();
        channel.configureBlocking(false);
        this.selector = SelectorProvider.provider().openSelector();
        channel.connect(new InetSocketAddress(ip, port));
        channel.register(selector, SelectionKey.OP_CONNECT);
    }

    public void working() throws IOException {
        while (true) {
            if (!selector.isOpen())
                break;
            selector.select();
            Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator();
            while (ite.hasNext()) {
                SelectionKey key = ite.next();
                ite.remove();
                // 连接事件发生
                if (key.isConnectable()) {
                    connect(key);
                } else if (key.isReadable()) {
                    read(key);
                }
            }
        }
    }

    /**
     * 处理读取服务端发来的信息 的事件
     * 
     * @param key
     * @throws IOException
     */
    public void read(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        // 创建读取的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(100);
        channel.read(buffer);
        byte[] data = buffer.array();
        String msg = new String(data).trim();
        System.out.println("客户端收到信息:" + msg);
        channel.close();
        key.selector().close();
    }

    public void connect(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        // 如果正在连接,则完成连接
        if (channel.isConnectionPending()) {
            channel.finishConnect();
        }
        channel.configureBlocking(false);
        channel.write(ByteBuffer.wrap(new String("hello server!\r\n")
                .getBytes()));
        channel.register(this.selector, SelectionKey.OP_READ);
    }

    /**
     * 启动客户端测试
     * 
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    NIOClient client = new NIOClient();
                    try {
                        client.init("localhost", 8000);
                        client.working();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

    }
}

案例运行效果如下:
在这里插入图片描述
在这里插入图片描述

AIO与多线程

AIO概述

AIO是异步IO的缩写,虽然NIO在网络操作中提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身还是同步的。
对于AIO来说,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此,AIO是完全不会阻塞的。此时,我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。
异步类和方法介绍
异步服务端Socket通道(AsynchronousServerSocketChannel):accept()方法会立即返回,它并不会真的等待客户端的到来

/**
     * Accepts a connection.
     *
     * <p> This method initiates an asynchronous operation to accept a
     * connection made to this channel's socket. The {@code handler} parameter is
     * a completion handler that is invoked when a connection is accepted (or
     * the operation fails). The result passed to the completion handler is
     * the {@link AsynchronousSocketChannel} to the new connection.
     *
     * <p> When a new connection is accepted then the resulting {@code
     * AsynchronousSocketChannel} will be bound to the same {@link
     * AsynchronousChannelGroup} as this channel. If the group is {@link
     * AsynchronousChannelGroup#isShutdown shutdown} and a connection is accepted,
     * then the connection is closed, and the operation completes with an {@code
     * IOException} and cause {@link ShutdownChannelGroupException}.
     *
     * <p> To allow for concurrent handling of new connections, the completion
     * handler is not invoked directly by the initiating thread when a new
     * connection is accepted immediately (see <a
     * href="AsynchronousChannelGroup.html#threading">Threading</a>).
     *
     * <p> If a security manager has been installed then it verifies that the
     * address and port number of the connection's remote endpoint are permitted
     * by the security manager's {@link SecurityManager#checkAccept checkAccept}
     * method. The permission check is performed with privileges that are restricted
     * by the calling context of this method. If the permission check fails then
     * the connection is closed and the operation completes with a {@link
     * SecurityException}.
     *
     * @param   <A>
     *          The type of the attachment
     * @param   attachment
     *          The object to attach to the I/O operation; can be {@code null}
     * @param   handler
     *          The handler for consuming the result
     *
     * @throws  AcceptPendingException
     *          If an accept operation is already in progress on this channel
     * @throws  NotYetBoundException
     *          If this channel's socket has not yet been bound
     * @throws  ShutdownChannelGroupException
     *          If the channel group has terminated
     */
    public abstract <A> void accept(A attachment,
                                    CompletionHandler<AsynchronousSocketChannel,? super A> handler);

此方法第一个参数是一个附件,可以是任意类型,作用是让当前线程和后续的回调方法可以共享信息,它会在后续调用中传递给handler。
第二个参数是CompletionHandler接口,此接口有两个方法

/**
 * A handler for consuming the result of an asynchronous I/O operation.
 *
 * <p> The asynchronous channels defined in this package allow a completion
 * handler to be specified to consume the result of an asynchronous operation.
 * The {@link #completed completed} method is invoked when the I/O operation
 * completes successfully. The {@link #failed failed} method is invoked if the
 * I/O operations fails. The implementations of these methods should complete
 * in a timely manner so as to avoid keeping the invoking thread from dispatching
 * to other completion handlers.
 *
 * @param   <V>     The result type of the I/O operation
 * @param   <A>     The type of the object attached to the I/O operation
 *
 * @since 1.7
 */
public interface CompletionHandler<V,A> {

    /**
     * Invoked when an operation has completed.
     *
     * @param   result
     *          The result of the I/O operation.
     * @param   attachment
     *          The object attached to the I/O operation when it was initiated.
     */
    void completed(V result, A attachment);

    /**
     * Invoked when an operation fails.
     *
     * @param   exc
     *          The exception to indicate why the I/O operation failed
     * @param   attachment
     *          The object attached to the I/O operation when it was initiated.
     */
    void failed(Throwable exc, A attachment);
}

accept方法实际上做了两件事。第一,发起accept请求,告诉系统可以开始监听端口。第二,注册CompletionHandler实例,告诉系统一旦有客户端前来连接,如果连接成功,就去执行completed()方法,如果连接失败,就去执行failed()方法。
所以,server.accept()方法不会阻塞,它会立即返回。

AIO案例

服务端代码:

package test32;

public class ServerTest {
    public static void main(String[] args) throws Exception {
        new AIOEchoServer().start();
        //主线程可以继续自己的行为
        //由于start()方法使用的都是异步方法,因此它会马上返回,它并不像阻塞方法那样会进行的等待
        //因此如果想让驻守执行,下列的语句是必须的。否则start()方法结束后,不等客户端到来
        //程序已经完成,主线程退出
        while (true){
            Thread.sleep(1000);
        }
    }
}
package test32;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class AIOEchoServer {
    public final static int PORT = 8000;
    //异步IO需要使用异步通道
    private AsynchronousServerSocketChannel server;

    public AIOEchoServer() throws IOException {
        //通道打开并绑定服务端端口
        this.server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));
    }
    public void start(){
        System.out.println("Server listen on "+PORT);
        //注册事件和事件完成后的处理器
        //accept()方法会立即返回,它不会真的等待客户端的到来
        server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            final ByteBuffer buffer = ByteBuffer.allocate(1024);
            @Override
            public void completed(AsynchronousSocketChannel result, Object attachment) {
                System.out.println(Thread.currentThread().getName());
                Future<Integer> writeResult = null;
                try{
                    buffer.clear();
                    //读取客户端传来的数据,read方法是异步的
                    //它不会等待读取完成了再返回,而是立即返回
                    result.read(buffer).get(100, TimeUnit.SECONDS);
                    buffer.flip();
                    //将数据回写客户端
                    writeResult = result.write(buffer);
                }catch (InterruptedException | ExecutionException | TimeoutException e){
                    e.printStackTrace();
                }finally {
                    try {
                        //进行下一个客户端连接的准备
                        server.accept(null,this);
                        //同时关闭当前正在处理的客户端连接
                        //在关闭之前,确保write方法已经完成,因此,使用Future.get()方法进行等待。
                        writeResult.get();
                        result.close();
                    }catch (Exception e){
                        System.out.println(e.toString());
                    }
                }
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                System.out.println("failed: "+exc);
            }
        });
    }
}

客户端代码:

package test32;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

public class AIOClient {
    public static void main(String[] args) throws Exception {
        //打开通道
        final AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
        client.connect(new InetSocketAddress("localhost", 8000), null, new CompletionHandler<Void, Object>() {
            @Override
            public void completed(Void result, Object attachment) {
                //连接成功,进行数据写入,向服务端发送数据
                client.write(ByteBuffer.wrap("Hello!".getBytes()), null, new CompletionHandler<Integer, Object>() {
                    @Override
                    public void completed(Integer result, Object attachment) {
                        //进行数据读取,从服务端读取回写的数据
                        try {
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            //立即返回
                            client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                                @Override
                                public void completed(Integer result, ByteBuffer attachment) {
                                    //打印输出
                                    buffer.flip();
                                    System.out.println(new String(buffer.array()));
                                    try {
                                        client.close();
                                    } catch (IOException e) {
                                        e.printStackTrace();
                                    }
                                }

                                @Override
                                public void failed(Throwable exc, ByteBuffer attachment) {
                                }
                            });
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }

                    @Override
                    public void failed(Throwable exc, Object attachment) {
                    }
                });
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
            }
        });
        //由于主线程马上结束,这里等待上述处理全部完成
        Thread.sleep(1000);
    }
}

运行结果:
在这里插入图片描述
在这里插入图片描述

发布了20 篇原创文章 · 获赞 14 · 访问量 8788

猜你喜欢

转载自blog.csdn.net/yemuxiaweiliang/article/details/104662570