之前面试baba系时遇到一个相对简单的多线程编程题,即"3个线程循环输出ADC",自己答的并不是很好,深感内疚,决定更加仔细的学习《并发编程的艺术》一书,到达掌握的强度。(之前两月休息时间都花在了lol和吃鸡上,算是劳逸结合了,推荐大家代码写累了可以玩下吃鸡,是个不错的调剂)

流程分析

Java的线程池是最常用的并发框架,合理的使用线程池可以降低系统消耗、提高响应速度、提高线程的可管理性。线程池的基础处理流程如下图所示。

上图中标红的4处正好是构建线程池的核心,核心线程池大小,队列,线程池大小,拒绝策略4个部分。通常来说,会根据当前系统的CPU数量来设置线程大小,并且一个应用中共用一个线程池,便于管理,过多的线程因为调度切换的原因,反而不能提高系统资源的利用率,如下是个人常用线程池管理类,不过现在对于AtomicReference的用法不是特别理解。

/**
 * 单个应用共用一个线程池,便于管理
 */
public class ApplicationExecutorFactory {
    private static AtomicReference<ExecutorService> serviceRef = new AtomicReference<>(
            Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2));

    public static ExecutorService getExecutor() {
        return serviceRef.get();
    }

    public static void shutdown() {
        if (serviceRef.get() == null)
            return;

        synchronized (ApplicationExecutorFactory.class) {
            if (serviceRef.get() == null)
                return;

            serviceRef.get().shutdown();
            serviceRef.set(null);
        }
    }
}

接下来来看看最常用的固定大小线程newFixedThreadPool()的构造方法,可以看到下面代码中,核心线程池大小和线程池最大大小是一致的,队列使用的链表形式的阻塞队列(默认大小为Int最大值),线程工厂DefaultThreadFactory和拒绝处理器AbortPolicy也是使用的默认的。

//按照层次剖析,涉及类Executors, ThreadPoolExecutor, LinkedBlockingQueue
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
public static ThreadFactory defaultThreadFactory() {
    return new DefaultThreadFactory();
}
private static final RejectedExecutionHandler defaultHandler =
    new AbortPolicy();
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

Tip:
这个引入几个问题来加强学习。
a.阻塞队列(也叫任务队列)的类型有哪些?队列大小为Integer.MAX_VALUE合适么?什么是有界队列,什么是无界队列?无界队列是不是会造成maximuxPoolSize失效?
ArrayBlockingQueue: 基于数组结构的有界阻塞队列,遵循FIFO
LinkedBlockingQueue:基于链表的阻塞队列,吞吐量一般高于ArrayBlockingQueue,固定大小的线程池就是使用的该队列。
SynchronousQueue: 一个不存储元素的阻塞队列(有意思),每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直阻塞。吐吞量Ibanez高于LinkedBlockingQueue,CachedThreadPool使用了该队列,目前 还没有很好的理解?
PriorityBlockingQueue: 一个具有优先级的无限阻塞队列。
b.如果希望给线程池中线程取名怎么办?
使用guava的ThreadFactoryBuilder可以快速的给线程池里线程设置有意义名字。
c.线程池除了固定大小的,还有哪些类型,根据场景如何取舍?(该问题将在Executor一节回答)
d.拒绝策略(也叫饱和策略)有哪些?
AbortPolicy: 直接抛出异常
CallerRunsPolicy: 只用调用者所在线程来运行任务,如何理解?
DiscardOldestPolicy:丢弃队列最老任务,并执行当前任务
DiscardPolicy:直接抛弃不处理
此外,还可以根据场景需要自己来实现RejectedExecutionHandler接口,来记录日志或持久化存储不能处理的任务。
e.keepAliveTime, TimeUnit参数有什么用?该参数决定工作线程空闲后存活的时间,对于任务很多但单个任务执行时间短的场景,可以调大时间提高利用率,看到这可以发现很多中间件都可以做出来。

运行机制

接下来通过下图更进一步了解线程池的运行机制。

1.如果当前运行的线程少于corePoolSize直接创建新线程来执行任务,需要获取全局锁
2.如果运行的线程等于或多余corePoolSize则将任务加入BlockingQueue。
3.如果由于队列已满,无法将任务加到BlockingQueue,则创建新的线程来处任务,需要获取全局锁。
4.如果创建新线程将操作maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
ThreadPoolExecutor采用上述步骤,保证了执行execute()时,尽可能的避免了获取全局锁,大部分的可能都会执行步骤2,而无需获取全局锁。上图就是通过下面的源代码形象化出来的。

//老版本JDK,便于理解
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
//由于是或条件运算符,所以先计算前半部分的值,如果线程池中当前线程数不小于核心池大小
//,那么就会直接进入下面的if语句块了。
    //如果线程池中当前线程数小于核心池大小,则接着执行后半部分,也就是执行
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
    //如果当前线程池处于RUNNING状态,则将任务放入任务缓存队列;
        if (runState == RUNNING && workQueue.offer(command)) {
        //这句判断是为了防止在将此任务添加进任务缓存队列的同时其他线程突然调用shutdown
        //或者shutdownNow方法关闭了线程池的一种应急措施
            if (runState != RUNNING || poolSize == 0)
                ensureQueuedTaskHandled(command);
        }
    //如果当前线程池不处于RUNNING状态或者任务放入缓存队列失败,则执行
        else if (!addIfUnderMaximumPoolSize(command))
            reject(command); // is shutdown or saturated
    }
}

private boolean addIfUnderCorePoolSize(Runnable firstTask) {
    Thread t = null;
    //首先获取到锁,因为这地方涉及到线程池状态的变化,先通过if语句判断当前线程池中的线程数目是否小于核心池大小,
    //有朋友也许会有疑问:前面在execute()方法中不是已经判断过了吗,只有线程池当前线程数目小于核心池大小才会执行addIfUnderCorePoolSize方法的,为何这地方还要继续判断?
    //原因很简单,前面的判断过程中并没有加锁,因此可能在execute方法判断的时候poolSize小于corePoolSize,而判断完之后,
    //在其他线程中又向线程池提交了任务,就可能导致poolSize不小于corePoolSize了,所以需要在这个地方继续判断。
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (poolSize < corePoolSize && runState == RUNNING)
            t = addThread(firstTask);        //创建线程去执行firstTask任务
        } finally {
        mainLock.unlock();
    }
    if (t == null)
        return false;
    t.start();
    return true;
}

//在addThread方法中,首先用提交的任务创建了一个Worker对象,然后调用线程工厂threadFactory创建了一个新的线程t,
//然后将线程t的引用赋值给了Worker对象的成员变量thread,接着通过workers.add(w)将Worker对象添加到工作集当中。
private Thread addThread(Runnable firstTask) {
    Worker w = new Worker(firstTask);
    Thread t = threadFactory.newThread(w);  //创建一个线程,执行任务
    if (t != null) {
        w.thread = t;            //将创建的线程的引用赋值为w的成员变量
        workers.add(w);
        int nt = ++poolSize;     //当前线程数加1
        if (nt > largestPoolSize)
            largestPoolSize = nt;
    }
    return t;
}

private final class Worker implements Runnable {
    private final ReentrantLock runLock = new ReentrantLock();
    private Runnable firstTask;
    volatile long completedTasks;
    Thread thread;
    Worker(Runnable firstTask) {
        this.firstTask = firstTask;
    }
    boolean isActive() {
        return runLock.isLocked();
    }
    void interruptIfIdle() {
        final ReentrantLock runLock = this.runLock;
        if (runLock.tryLock()) {
            try {
        if (thread != Thread.currentThread())
        thread.interrupt();
            } finally {
                runLock.unlock();
            }
        }
    }
    void interruptNow() {
        thread.interrupt();
    }

    private void runTask(Runnable task) {
        final ReentrantLock runLock = this.runLock;
        runLock.lock();
        try {
            if (runState < STOP &&
                Thread.interrupted() &&
                runState >= STOP)
            boolean ran = false;
            beforeExecute(thread, task);   //beforeExecute方法是ThreadPoolExecutor类的一个方法,没有具体实现,用户可以根据
            //自己需要重载这个方法和后面的afterExecute方法来进行一些统计信息,比如某个任务的执行时间等
            try {
                task.run();
                ran = true;
                afterExecute(task, null);
                ++completedTasks;
            } catch (RuntimeException ex) {
                if (!ran)
                    afterExecute(task, ex);
                throw ex;
            }
        } finally {
            runLock.unlock();
        }
    }

    public void run() {
        try {
            Runnable task = firstTask;
        // 从run方法的实现可以看出,它首先执行的是通过构造器传进来的任务firstTask,在调用runTask()执行完firstTask之后,
        //在while循环里面不断通过getTask()去取新的任务来执行,那么去哪里取呢?自然是从任务缓存队列里面去取,
        //getTask是ThreadPoolExecutor类中的方法,并不是Worker类中的方法,下面是getTask方法的实现:
            firstTask = null;
            while (task != null || (task = getTask()) != null) {
                runTask(task);
                task = null;
            }
        } finally {
            workerDone(this);   //当任务队列中没有任务时,进行清理工作
        }
    }
}

Runnable getTask() {
    for (;;) {
        try {
            int state = runState;
            if (state > SHUTDOWN)
                return null;
            Runnable r;
            if (state == SHUTDOWN)  // Help drain queue
                r = workQueue.poll();
            else if (poolSize > corePoolSize || allowCoreThreadTimeOut) //如果线程数大于核心池大小或者允许为核心池线程设置空闲时间,
                //则通过poll取任务,若等待一定的时间取不到任务,则返回null
                r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
            else
                r = workQueue.take();
            if (r != null)
                return r;
            if (workerCanExit()) {    //如果没取到任务,即r为null,则判断当前的worker是否可以退出
                if (runState >= SHUTDOWN) // Wake up others
                    interruptIdleWorkers();   //中断处于空闲状态的worker
                return null;
            }
            // Else retry
        } catch (InterruptedException ie) {
            // On interruption, re-check runState
        }
    }
}

//JDK1.8
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

Tip:
这个引入几个问题来加强学习。
a.这儿的全局锁如何理解?
b.线程池有哪些状态
ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态(JDK1.5,1.8做的更加复杂不适合表述)。

volatile int runState;
static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;
static final int STOP       = 2;
static final int TERMINATED = 3;

runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性,下面的几个static final变量表示runState可能的几个取值。
当创建线程池后,初始时,线程池处于RUNNING状态;
如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

补充知识与自制线程池

提交任务execute(), submit()提交有返回值的任务
关闭线程池shutdown, shutdownNow它们均是遍历线程池中的工作线程,逐个调用interrupt方法来中断线程,所有无法响应中断的任务永远无法停止。前者将线程池状态设置为SHUTDOWN,然后中断没有执行任务的线程,而后者会将状态设置为STOP,然后尝试停止所有正在执行或者暂停执行的线程,并返回等待执行任务的列表。当所有任务都关闭后,才表示线程关闭成功,这是isTerminated返回成功。
线程池的选择:通常来说,性质不同的任务可用不同规模的线程池分开处理,CPU密集型的任务应配置尽可能小的线程,而IO密集型可以配置尽可能多的,混合型的任务最好将任务拆分后再做权衡。
对于数据库请求和服务请求这类IO密集型的操作,可以将线程池设置的相对较大,具体根据线上的情况做权衡(平均耗时)。
队列的选择:建议使用有界队列,避免线程池耗尽整个系统的资源,当然这时就需要很好的提供饱和策略了。

接下来,分享一个个人通过对线程池核心原理的分析,自制的简化线程池。

public class CustomThreadPool implements ExecutorService {
    /**
     * 线程池状态部分
     */
    volatile int runState;
    static final int RUNNING = 0;
    static final int SHUTDOWN = 1;
    static final int STOP = 2;
    static final int TERMINATED = 3;

    /**
     * 线程池基础参数部分
     */
    int coreMaxSize;// 最大线程池大小和最大核心线程大小一样,简化
    final BlockingQueue<Runnable> queue;
    RejectedExecutionHandler handler;
    // 线程池不是简单的使用一个List<Thread>管理,而是借助一个Worker的概念,Worker需要线程和Task两个因素才能工作
    // 想想生产线的工人Worker,task比如是生产一个手套,线程比如就是一个机械臂,可以选择一个空闲的机械臂进行生产
    final HashSet<CustomWorker> workers = new HashSet<CustomWorker>();
    final ReentrantLock mainLock = new ReentrantLock();// 线程池的主要状态锁,对线程池状态(大小,runstate)
    private volatile ThreadFactory threadFactory; // 线程工厂,用来创建线程

    public CustomThreadPool() {
        runState = RUNNING;
        coreMaxSize = 4;
        queue = new CustomBlockingQueue(100);
        handler = new CustomRejectedExecutionHandler();
        threadFactory = new ThreadFactoryBuilder().setNameFormat("").build();
    }

    @Override
    public void execute(Runnable command) {
        // 1.判断线程池是否已满
        if (getCurSize() <= coreMaxSize && runState == RUNNING) {
            createWorker(command);
            return;
        }
        // 2.判断队列是否满了
        if (getCurSize() <= coreMaxSize && runState == RUNNING) {
            enqueue(command);
            return;
        }
        // 3.判断线程池子是否满了,省略
        // 4.按照饱和策略做处理
        rejectHandle(command);
    }

    private void createWorker(Runnable command) {
        CustomWorker worker = new CustomWorker();
        Thread thd = threadFactory.newThread(command);
        worker.setThread(thd);

        mainLock.lock();
        try {
            workers.add(worker);
        } finally {
            mainLock.unlock();
        }
        worker.run();
    }

    private void rejectHandle(Runnable command) {
        // handler.rejectedExecution(command, this);
    }

    private boolean enqueue(Runnable command) {
        return queue.add(command);
    }

    private int getCurSize() {
        mainLock.lock();
        try {
            return workers.size();
        } finally {
            mainLock.unlock();
        }
    }

    @Override
    public void shutdown() {
        for (CustomWorker worker : workers) {
            worker.interrupt();
        }
    }
}

面试题

1."3个线程循环输出ADC"?
a.通过Object对象实现

public class ABCDemo {
    public static void test() {
        Object lockA = new Object();
        Object lockB = new Object();
        Object lockC = new Object();

        Thread thA = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        System.out.println("A线程获取C锁");
                        synchronized (lockC) {
                            System.out.println("A线程开始等待C锁信号");
                            lockC.wait();
                            System.out.println("A线程结束等待C锁信号");
                        }
                        System.out.println("A线程释放C锁");

                        System.out.println("A");

                        System.out.println("A线程获取A锁");
                        synchronized (lockA) {
                            System.out.println("A线程开始发送A锁信号");
                            lockA.notify();
                            System.out.println("A线程结束发送A锁信号");
                        }
                        System.out.println("A线程释放A锁");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }
        });

        Thread thB = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        System.out.println("B线程获取A锁");
                        synchronized (lockA) {
                            System.out.println("B线程开始等待A锁信号");
                            lockA.wait();
                            System.out.println("B线程结束等待A锁信号");
                        }
                        System.out.println("B线程释放A锁");

                        System.out.println("B");

                        System.out.println("B线程获取B锁");
                        synchronized (lockB) {
                            System.out.println("B线程开始发送B锁信号");
                            lockB.notify();
                            System.out.println("B线程结束发送B锁信号");
                        }
                        System.out.println("B线程释放B锁");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }
        });

        Thread thC = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        System.out.println("C线程获取B锁");
                        synchronized (lockB) {
                            System.out.println("C线程开始等待B锁信号");
                            lockB.wait();
                            System.out.println("C线程结束等待B锁信号");
                        }
                        System.out.println("C线程释放B锁");

                        System.out.println("C");

                        System.out.println("C线程获取C锁");
                        synchronized (lockC) {
                            System.out.println("C线程开始发送C锁信号");
                            lockC.notify();
                            System.out.println("C线程结束发送C锁信号");
                        }
                        System.out.println("C线程释放C锁");

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }
        });

        System.out.println("主线程开启A线程");
        thA.start();
        System.out.println("主线程开启B线程");
        thB.start();
        System.out.println("主线程开启C线程");
        thC.start();

        System.out.println("主线程获取C锁");
        synchronized (lockC) {
            System.out.println("主线程开始发送C锁信号");
            lockC.notify();
            System.out.println("主线程结束发送C锁信号");
        }
        System.out.println("主线程释放C锁");
    }
}

b.通过ReentrantLock实现(在ReentrantLock一章实现)

参考资料

  1. 方腾飞. Java并发编程的艺术[M]. 上海:机械工业出版社, 2017.
  2. 推荐博文--Java并发编程:线程池的使用

07深入理解Java线程池的更多相关文章

  1. 深入理解Java线程池:ScheduledThreadPoolExecutor

    介绍 自JDK1.5开始,JDK提供了ScheduledThreadPoolExecutor类来支持周期性任务的调度.在这之前的实现需要依靠Timer和TimerTask或者其它第三方工具来完成.但T ...

  2. 深入理解 Java 线程池

    一.简介 什么是线程池 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务. 为什么要用线程池 如果并发请求数量很多,但每个线程执行的时间很短,就会出现频繁的创建 ...

  3. 由浅入深理解Java线程池及线程池的如何使用

    前言 多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担.线程本身也要占用内存空间,大量的线程会占用内存资源并且可能会导致Out of Memory ...

  4. 深入理解Java线程池

    我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁 ...

  5. 深入理解java线程池—ThreadPoolExecutor

    几句闲扯:首先,我想说java的线程池真的是很绕,以前一直都感觉新建几个线程一直不退出到底是怎么实现的,也就有了后来学习ThreadPoolExecutor源码.学习源码的过程中,最恶心的其实就是几种 ...

  6. 深入理解Java线程池:ThreadPoolExecutor

    线程池介绍 在web开发中,服务器需要接受并处理请求,所以会为一个请求来分配一个线程来进行处理.如果每次请求都新创建一个线程的话实现起来非常简便,但是存在一个问题: 如果并发的请求数量非常多,但每个线 ...

  7. Java线程池ThreadPoolExecutor使用和分析(三) - 终止线程池原理

    相关文章目录: Java线程池ThreadPoolExecutor使用和分析(一) Java线程池ThreadPoolExecutor使用和分析(二) - execute()原理 Java线程池Thr ...

  8. 【转载】深度解读 java 线程池设计思想及源码实现

    总览 开篇来一些废话.下图是 java 线程池几个相关类的继承结构: 先简单说说这个继承结构,Executor 位于最顶层,也是最简单的,就一个 execute(Runnable runnable) ...

  9. Java并发指南12:深度解读 java 线程池设计思想及源码实现

    ​深度解读 java 线程池设计思想及源码实现 转自 https://javadoop.com/2017/09/05/java-thread-pool/hmsr=toutiao.io&utm_ ...

随机推荐

  1. 第五章 MySQL事务,视图,索引,备份和恢复

    第五章 MySQL事务,视图,索引,备份和恢复 一.事务 1.什么是事务 事务是一种机制,一个操作序列,它包含了一组数据库操作命令,并且把所有的命令作为一个整体一起向系统提交或撤销操作请求.要么都执行 ...

  2. UWP 手绘视频创作工具技术分享系列 - 位图的绘制

    前面我们针对 SVG 的解析和绘制做了介绍,SVG 是图片的一种形式,而另一种很重要的图片是:位图,包括 png.jpeg.bmp 等格式.位图的基本规则是,组成的基本元素是像素点,由宽度 * 高度个 ...

  3. OC实现同步访问属性

    有时候,我们在开发过程中需要对属性的访问进行同步操作,这种属性需要做成原子的,用atomic来修饰属性,即可实现这一点. 如果我们想自己实现,可以按照下面方式写代码: SFPerson.h #impo ...

  4. win10 uwp 隐藏实时可视化

    新的vs有个功能,实时可视化 但是他会挡我们界面,想要隐藏 点击转到实时可视化,就是点击横线看到,接着就可以看到下面的选项 点击在应用程序中显示运行时,就是不选中 很简单就看到,没有那个 本作品采用知 ...

  5. 张高兴的 Xamarin.Android 学习笔记:(三)活动生命周期

    本文将直接解释我写的一个示例.示例目的在于展示 Android 活动在 Xamarin 中的用法.如果有朋友对基础知识不太了解建议先学 Android . 新建一个 Xamarin.Android 项 ...

  6. .NET下发送邮件遇到问题及解决方案

    .NET后台代码利用QQ邮箱服务器发送邮件遇到的问题: "mail from address must be same as authorization user" 首先,看下我的 ...

  7. UVa 10954,Add All

    Huffman编码简化版,优先队列搞定. 1A 调试的时候发现一个问题..木有想明白...问题代码里给出,哪位大神给解释下. #include <iostream> #include &l ...

  8. Jvm加载jar包的顺序

    使用-XX:+TraceClassPaths或者在服务器上执行jinfo时,都能得到classpath包含的jar包,例如: java.class.path = local/aaa/lib/sprin ...

  9. 【微信小程序开发】秒懂,架构及框架

    今天1024程序员节,写文章庆祝!!! 今天的文章是讲微信小程序开发的,按理解把架构与框架说说.有不对之处请大神指点…… 微信小程序与web应用很像,但是原理不同,微信小程序是运行在微信应用内的,不是 ...

  10. 域名系统DNS

    一.域名系统是什么 域名系统其实就是一个把主机名解析为IP地址的名字系统. 因特网使用层次树状结构的命名方法,并使用分布式的域名系统DNS.因特网的域名系统DNS被设计成一个联机分布式数据库系统,并采 ...