5_并发工具-线程池

共享模型之工具

1. 自定义线程池

我们在了解JDK更完善的线程池之前,我们自己先写一个线程池。我们首先分析一下一个线程池的基本设计图:

image-20240728152857535

首先模拟阻塞队列的实现:

// 定义一个阻塞队列
class BlockingQueue<T> {
    // 任务队列 -- 使用双向链表实现
    private LinkedList<T> queue = new LinkedList<>();
    // 定义锁保护队列头部和尾部的元素
    private ReentrantLock lock = new ReentrantLock();

    // 生产者条件变量 --> 队列满的时候不能再往队列中添加任务
    private Condition fullWaitSet = lock.newCondition();
    // 消费者条件变量 ---> 队列为空的时候不能再往队列中取任务
    private Condition emptyWaitSet = lock.newCondition();
    // 队列容量
    private volatile int capcity;

    // 从队列中取出任务
    public T take() {

        try {
            lock.lock();
            // 如果队列中没有元素,则阻塞等待
            try {
                while (queue.isEmpty()) {
                    // 进入 emptyWaitSet 条件中等待
                    emptyWaitSet.wait();
                }
                T task = queue.removeFirst();
                // 此时队列不为空了,唤醒fullWaitSet条件下阻塞的线程
                fullWaitSet.signal();
                return task;
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
                // 队列不为空,获取队列头部中的任务并返回
            }
        } finally {
            lock.unlock();
        }
    }

    // 带超时的阻塞获取(为了避免长时间等待)
    public T poll(long timeout, TimeUnit unit){
        // 将时间转为统一的时间(纳秒)
        long nanos = unit.toNanos(timeout);
        try {
            lock.lock();
            // 如果队列中没有元素,则阻塞等待
            try {
                while (queue.isEmpty()) {
                    // 进入 emptyWaitSet 条件中等待
                    if (nanos <= 0){
                        return null;    // 等待一段时间后还没有被唤醒,则直接返回null,避免永久等待
                    }
                    nanos = emptyWaitSet.awaitNanos(nanos); //避免虚假唤醒 --> 返回的是剩余等待时间

                }
                T task = queue.removeFirst();
                // 此时队列不为空了,唤醒fullWaitSet条件下阻塞的线程
                fullWaitSet.signal();
                return task;
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
                // 队列不为空,获取队列头部中的任务并返回
            }
        } finally {
            lock.unlock();
        }
    }



    // 往队列中添加元素
    public void put(T task) {
        try {
            lock.lock();
            while (queue.size() == capcity) {
                try {
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 队列非满,则往队列中添加任务
            queue.addLast(task);
            // 唤醒 emptyWaitSet 条件下阻塞的线程
            emptyWaitSet.signal();
        } finally {
            lock.unlock();
        }
    }

    // 获取阻塞队列大小
    public int size() {
        try {
            lock.lock();
            try {
                return queue.size();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } finally {
            lock.unlock();
        }
    }
}

接着实现线程池:

class ThreadPool{
    // 关联一个阻塞队列
    private BlockingQueue<Runnable> taskQueue;
    // 线程集合
    private HashSet<Worker> workers = new HashSet();

    // 定义核心的线程数
    private int coreSize;

    // 获取任务的超时时间
    private long timeout;

    private TimeUnit timeUnit;

    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapcity)
    }

    // 对线程的包装
    class Worker{

    }
}

编写具体执行任务的代码:

// 对线程的包装
class Worker extends Thread{
    private Runnable task;

    public Worker(Runnable task) {
        this.task = task;
    }

    // 执行任务
    @Override
    public void run() {
        // 当task不为空,执行任务; task为空,从任务队列中获取任务执行
        while(task!=null || (task = taskQueue.take())!=null){
            if (task != null){
                try{
                    task.run();
                } catch (Exception e){

                } finally {
                    // 移除任务,因为已经执行完了任务
                    task = null;
                }
            }
        }

        // 任务完成后,从任务队列中移除该任务
        synchronized (workers){
            workers.remove(this);
        }
    }
}

完整的代码如下:

package org.example.a45;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.stylesheets.LinkStyle;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;


public class TestPool {
    private static final Logger log = LoggerFactory.getLogger(TestPool.class);

    public static void main(String[] args) {
        ThreadPool pool = new ThreadPool(2,1000,TimeUnit.MILLISECONDS, 10);
        for (int i=0; i<5; i++){
            int j = i;
            // 执行任务
            pool.execute(()->{
                log.debug("{}",j);
            });
        }
    }
}


// 定义一个阻塞队列
class BlockingQueue<T> {
    private static final Logger log = LoggerFactory.getLogger(BlockingQueue.class);
    // 任务队列 -- 使用双向链表实现
    private LinkedList<T> queue = new LinkedList<>();
    // 定义锁保护队列头部和尾部的元素
    private ReentrantLock lock = new ReentrantLock();

    // 生产者条件变量 --> 队列满的时候不能再往队列中添加任务
    private Condition fullWaitSet = lock.newCondition();
    // 消费者条件变量 ---> 队列为空的时候不能再往队列中取任务
    private Condition emptyWaitSet = lock.newCondition();
    // 队列容量
    private volatile int capcity;

    public BlockingQueue(int capcity) {
        this.capcity = capcity;
    }

    // 从队列中取出任务
    public T take() {

        try {
            lock.lock();
            // 如果队列中没有元素,则阻塞等待
            try {
                while (queue.isEmpty()) {
                    // 进入 emptyWaitSet 条件中等待
                    emptyWaitSet.await();
                }
                T task = queue.removeFirst();
                // 此时队列不为空了,唤醒fullWaitSet条件下阻塞的线程
                fullWaitSet.signal();
                return task;
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
                // 队列不为空,获取队列头部中的任务并返回
            }
        } finally {
            lock.unlock();
        }
    }

    // 带超时的阻塞获取(为了避免长时间等待)
    public T poll(long timeout, TimeUnit unit) {
        // 将时间转为统一的时间(纳秒)
        long nanos = unit.toNanos(timeout);
        try {
            lock.lock();
            // 如果队列中没有元素,则阻塞等待
            try {
                while (queue.isEmpty()) {
                    // 进入 emptyWaitSet 条件中等待
                    if (nanos <= 0) {
                        return null;    // 等待一段时间后还没有被唤醒,则直接返回null,避免永久等待
                    }
                    nanos = emptyWaitSet.awaitNanos(nanos); //避免虚假唤醒 --> 返回的是剩余等待时间

                }
                T task = queue.removeFirst();
                // 此时队列不为空了,唤醒fullWaitSet条件下阻塞的线程
                fullWaitSet.signal();
                return task;
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
                // 队列不为空,获取队列头部中的任务并返回
            }
        } finally {
            lock.unlock();
        }
    }


    // 往队列中添加元素
    public void put(T task) {
        try {
            lock.lock();
            while (queue.size() == capcity) {
                try {
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 队列非满,则往队列中添加任务
            queue.addLast(task);
            // 唤醒 emptyWaitSet 条件下阻塞的线程
            emptyWaitSet.signal();
        } finally {
            lock.unlock();
        }
    }

    // 获取阻塞队列大小
    public int size() {
        try {
            lock.lock();
            try {
                return queue.size();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } finally {
            lock.unlock();
        }
    }
}

class ThreadPool {
    private static final Logger log = LoggerFactory.getLogger(ThreadPool.class);
    // 关联一个阻塞队列
    private BlockingQueue<Runnable> taskQueue;
    // 线程集合
    private HashSet<Worker> workers = new HashSet();

    // 定义核心的线程数
    private int coreSize;

    // 获取任务的超时时间
    private long timeout;

    private TimeUnit timeUnit;

    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapcity);
    }

    // 执行任务
    public void execute(Runnable task) {
        synchronized (workers) {
            // 判断当前任务数是否超过核心线程数,直接交给worker对象执行
            // 如果任务数超过了coreSize, 就要加入到任务队类中在暂存起来
            if (workers.size() < coreSize) {
                log.debug("新增worker:{}", workers);
                // 创建新的worker对象,并交给它执行
                Worker worker = new Worker(task);
                // 将新创建的worker对象加入到线程池中
                workers.add(worker);
                worker.start();
            } else {
                // 加入到任务队列中
                log.debug("加入任务队列:{}", task);
                taskQueue.put(task);
            }
        }
    }

    // 对线程的包装
    class Worker extends Thread {
        private Runnable task;

        public Worker(Runnable task) {
            this.task = task;
        }

        // 执行任务
        @Override
        public void run() {
            // 当task不为空,执行任务; task为空,从任务队列中获取任务执行
            while (task != null || (task = taskQueue.take()) != null) {
                try {
                    log.debug("正在执行...{}", task);
                        task.run();
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        // 移除任务,因为已经执行完了任务
                        task = null;
                }
            }
            // 任务完成后,从任务队列中移除该任务
            synchronized (workers) {
                log.debug("worker被移除:{}",this);
                workers.remove(this);
            }
        }
    }
}

测试结果如下:

16:45:13.829 [main] DEBUG org.example.a45.ThreadPool - 新增worker:[]
16:45:13.836 [main] DEBUG org.example.a45.ThreadPool - 新增worker:[Thread[Thread-0,5,main]]
16:45:13.837 [main] DEBUG org.example.a45.ThreadPool - 加入任务队列:org.example.a45.TestPool$$Lambda$1/451111351@2a18f23c
16:45:13.837 [Thread-0] DEBUG org.example.a45.ThreadPool - 正在执行...org.example.a45.TestPool$$Lambda$1/451111351@709170b
16:45:13.837 [Thread-0] DEBUG org.example.a45.TestPool - 0
16:45:13.837 [main] DEBUG org.example.a45.ThreadPool - 加入任务队列:org.example.a45.TestPool$$Lambda$1/451111351@d7b1517
16:45:13.837 [Thread-1] DEBUG org.example.a45.ThreadPool - 正在执行...org.example.a45.TestPool$$Lambda$1/451111351@73be3d84
16:45:13.837 [main] DEBUG org.example.a45.ThreadPool - 加入任务队列:org.example.a45.TestPool$$Lambda$1/451111351@16c0663d
16:45:13.837 [Thread-0] DEBUG org.example.a45.ThreadPool - 正在执行...org.example.a45.TestPool$$Lambda$1/451111351@2a18f23c
16:45:13.837 [Thread-1] DEBUG org.example.a45.TestPool - 1
16:45:13.837 [Thread-1] DEBUG org.example.a45.ThreadPool - 正在执行...org.example.a45.TestPool$$Lambda$1/451111351@d7b1517
16:45:13.837 [Thread-0] DEBUG org.example.a45.TestPool - 2
16:45:13.837 [Thread-1] DEBUG org.example.a45.TestPool - 3
16:45:13.838 [Thread-0] DEBUG org.example.a45.ThreadPool - 正在执行...org.example.a45.TestPool$$Lambda$1/451111351@16c0663d
16:45:13.838 [Thread-0] DEBUG org.example.a45.TestPool - 4

接下来自定义线程池的拒绝策略

为了避免线程队列满了导致线程死等,我们可以为线程池编写一些拒绝策略,例如:

  • 死等

  • 带超时等待

  • 放弃任务执行

  • 抛出异常

  • 让调用者自己执行

  • ...

这里我们可以使用设计模式中的策略模式:具体的操作抽象出一个接口,具体的实现将来由调用者传递进来

@FunctionalInterface    //拒绝策略
interface RejectPolicy<T>{
    void reject(BlockingQueue<T> queue, T task);
}

2. JDK提供的ThreadPoolExecutor线程池

image-20240728172750601

1)线程池状态

ThreadPoolExecutor使用了整数int的高3位表示线程池的状态,低29位表示线程池中的线程数量

线程池中的状态如下:

状态名 高三位 接收新任务 处理阻塞队列任务 说明
RUNNING 111 Y Y
SHUTDOWN(比较温和的停止) 000 N Y 不会接收新的任务,但会处理阻塞队列中剩余的任务
STOP(比较暴力的停止) 001 N N 会中断正在执行的任务,并抛弃阻塞队列中的任务
TIDYING 010 - - 任务全部执行完毕,活动线程数为0即将进入线程池的终结状态
TERMINATED 011 终结状态

从数组上比较:TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING.

为什么不将线程池的状态和线程数量分开使用两个整数存储呢?而是使用整数的不同位来存储呢?

这是因为这些信息都存储在一个原子变量ctl中,目的是将线程池的状态和线程个数合二为一,这样就可以使用一次CAS操作就可以进行赋值:

// c为旧值,ctlOf返回为新值
ctl.compareAndSet(c, cltOf(targetState, workerCountOf(c)));

// rs为线程池的高三位状态,wc为低29位表示线程池中线程的个数,ctl就是合并它们
private static int ctlOf(int rs, int wc){return rs | wc;}

2)ThreadPoolExecutor的构造方法

根据构造方法,JDK Executors类中提供了众多工厂方法来创建各种用途的线程池,其中参数最详细的就是下面的构造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数
  • keepAliveTime:救急线程的生存时间
  • unit:时间单位-针对救急线程
  • workQueue:阻塞队列
  • threadFactory:线程工厂-可以为线程创建时起个好名字
  • handler:拒绝策略

线程池的工作方式:

image-20240728214602863

image-20240728215058448

  • 线程池中刚开始没有线程,当一个任务提交到线程池后,线程池会创建一个新的线程来执行任务。
  • 当线程数达到coreSize并没有空闲线程时,这时再加入任务,新加的线程会被加入到workQueue中队列中排队,直到有空闲的线程。
  • 如果队列选择了有界队列,那么任务超过了队列大小时,会创建maximumPoolSize-corePoolSize数目的救急线程来救急。
  • 如果线程到达maximumPoolSize仍然有新任务这时会执行拒绝策略。拒绝策略jdk提供了4种实现,其它著名框架也提供了实现
    • AbortPolicy让调用者抛出RejectedExecutionException异常,这是默认策略
    • CallerRunsPolicy让调用者运行任务
    • DiscardPolicy放弃本次任务
    • DiscardOldestPolicy放弃队列中最早的任务,本任务取而代之、Dubbo的实现,在抛出RejectedExecutionException异常之前会记录日志,并dump线程栈信息,方便定位问题
    • Netty的实现,是创建一个新线程来执行任务
    • ActiveMQ的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
    • PinPoint的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
  • 当高峰过去后,超过corePoolSize的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime和unit来控制。

image-20240728230156573

除了ThreadPoolExecutor方法可以创建线程池外,还有其它的方法来创建线程池,例如下面的几个方法:

3)newFixedThreadPool

    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

特点:

  • 该线程池创建的线程数时固定的,没有紧急线程,最大线程数=核心线程数,因此也不需要超时时间
  • 阻塞队列是无界的,可以放任意数量的任务

适用范围:适用于任务数量已知,相对耗时的任务

举个例子:

public class A46Test {
    private static final Logger log = LoggerFactory.getLogger(A46Test.class);
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {
            private AtomicInteger syn = new AtomicInteger(1);
            @Override   // 自定义线程名
            public Thread newThread(Runnable r) {
                return new Thread(r, "mypool-t-"+syn.getAndIncrement());
            }
        });
        pool.execute(()->{
            log.debug("123");
        });

        pool.execute(()->{
            log.debug("123");
        });

        pool.execute(()->{
            log.debug("123");
        });
    }
}

运行结果如下:

10:49:44.768 [mypool-t-1] DEBUG org.example2.a46.A46Test - 123
10:49:44.768 [mypool-t-2] DEBUG org.example2.a46.A46Test - 123
10:49:44.772 [mypool-t-1] DEBUG org.example2.a46.A46Test - 123

4)newCachedThreadPool

newCachedThreadPool是带有缓冲功能的线程池

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

特点:

  • 核心线程数为0,最大线程数为int的最大类型,救急线程的空闲时间为60S,意味着
    • 全部都是救急线程(60S后可回收)
    • 救急线程可以无限创建
  • 队列采用了SynchronousQueue(同步队列),其实现特点为,它没有容量限制,没有线程来取是放不进去的(一手交钱,一手交货)
package org.example2.a46;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.SynchronousQueue;

public class A46Test {
    private static final Logger log = LoggerFactory.getLogger(A46Test.class);
    public static void main(String[] args) throws InterruptedException {
        SynchronousQueue<Integer> integers = new SynchronousQueue<>();
        new Thread(()->{
            try{
                log.debug("putting {}",1);
                integers.put(1);
                log.debug("putted {}",1);

                log.debug("putting {}",2);
                integers.put(2);
                log.debug("putted {}",2);
            } catch (InterruptedException e){
                e.printStackTrace();
            }
        },"t1").start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }


        new Thread(()->{
            try {
                log.debug("taking {}",1);
                integers.take();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t2").start();

        Thread.sleep(1000);

        new Thread(()->{
            try {
                log.debug("taking {}",2);
                integers.take();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t3").start();
    }
}
00:03:00.561 [t1] DEBUG org.example2.a46.A46Test - putting 1
00:03:01.569 [t2] DEBUG org.example2.a46.A46Test - taking 1
00:03:01.570 [t1] DEBUG org.example2.a46.A46Test - putted 1
00:03:01.570 [t1] DEBUG org.example2.a46.A46Test - putting 2
00:03:02.571 [t3] DEBUG org.example2.a46.A46Test - taking 2
00:03:02.571 [t1] DEBUG org.example2.a46.A46Test - putted 2

我们发现,准备putting 1的时候,没有线程来处理,因此会被阻塞住,当等到1S后t2线程启动起来了,该被阻塞的线程就会进入同步队列中消费。类似的 putting 2解释也一样。

  • 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕后,空闲一分钟后释放线程。
  • 适合任务数比较紧密,但每个任务执行的时间比较短的情况。

3)newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

使用场景:

  • 希望多个线程排队执行。线程固定数为1,任务数多于1时,会放入无界队列中排队。任务执行完毕后,这个唯一的线程也不会被释放(核心线程数=1)。

单例线程池和单线程的区别:

  • 自己创建一个单线程去执行串行任务,如果任务执行失败而终止而没有任何补救措施;而线程池还会创建一个线程,保证线程池的正常工作。如下面的演示代码:

    package org.example2.a47;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class A47Test {
        private static final Logger log  = LoggerFactory.getLogger(A47Test.class);
        public static void main(String[] args) {
            test2();
        }
    
        public static void test2(){
            ExecutorService pool = Executors.newSingleThreadExecutor();
            pool.execute(()->{
                log.debug("1");
                int i = 1/0;    // 失败
            });
    
            pool.execute(()->{
                log.debug("2");
            });
    
            pool.execute(()->{
                log.debug("3");
            });
        }
    }
    

    运行结果如下:

    14:15:10.057 [pool-1-thread-1] DEBUG org.example2.a47.A47Test - 1
    14:15:10.062 [pool-1-thread-2] DEBUG org.example2.a47.A47Test - 2
    14:15:10.062 [pool-1-thread-2] DEBUG org.example2.a47.A47Test - 3
    Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
    	at org.example2.a47.A47Test.lambda$test2$0(A47Test.java:19)
    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    	at java.lang.Thread.run(Thread.java:748)
    

    我们发现出异常以后,线程1已经出现异常结束掉了,但是单线程池又创建了一个线程2,继续去执行后面的方法!---> 单例线程池能够保证线程池中始终有一个可用的线程

  • Executors.newSingleThreadSxecutor()线程个数始终为1,不能修改

    • FinalizableDelegatedExecutorService应用的是装饰器模式,只对外暴露了ExecutorService接口,因此不能调用ThreadPoolExecutor中特有的方法。

4)提交任务

// 执行任务 
void execute(Runnable command);

// 提交任务task,用返回值Future获得任务完成后的结果
Future<?> submit(Runnable task);

// 提交tasks中的所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;

// 提交tasks中的所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;

// 提交tasks中的所有任务,只要有一个任务最先执行完毕,返回此任务的执行结果,其它任务取消

// 提交tasks中的所有任务,只要有一个任务最先执行完毕,返回此任务的执行结果,其它任务取消,带超时时间

编写一段简单的代码,演示使用线程池再执行任务完毕后返回任务的处理结果(submit配合Callable获取执行结果)---> 使用保护性暂停来接收另一个线程执行的结果:

public class TestSubmit {
    private static final Logger log = LoggerFactory.getLogger(TestSubmit.class);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        Future<Integer> future = pool.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.debug("线程池中的线程开始执行任务...");
                Thread.sleep(1000);
                return 1 + 1;	// 唤醒主线程
            }
        });

        // 获取线程池中线程执行后的结果(主线程阻塞等待结果)
        log.debug("{}",future.get());
    }
}

运行结果如下:

14:33:37.882 [pool-1-thread-1] DEBUG org.example2.a47.TestSubmit - 线程池中的线程开始执行任务...
14:33:38.894 [main] DEBUG org.example2.a47.TestSubmit - 2

当然上面的代码可以使用lambda进行简写:

public class TestSubmit {
    private static final Logger log = LoggerFactory.getLogger(TestSubmit.class);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        Future<Integer> future = pool.submit(() -> {
            log.debug("线程池中的线程开始执行任务...");
            Thread.sleep(1000);
            return 1 + 1;
        });

        // 获取线程池中线程执行后的结果
        log.debug("{}",future.get());
    }
}

接下来我们演示一次性往线程池中提交多个任务的示例:

public class TestInvokeAll {
    private static final Logger log = LoggerFactory.getLogger(TestInvokeAll.class);

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        List<Future<String>> futures = pool.invokeAll(Arrays.asList(() -> {
                    log.debug("begin...");
                    Thread.sleep(1000);
                    return "111";
                },

                () -> {
                    log.debug("begin...");
                    Thread.sleep(500);
                    return "222";
                },

                () -> {
                    log.debug("begin...");
                    Thread.sleep(2000);
                    return "333";
                }));


        futures.forEach(f->{
            try {
                log.debug("{}",f.get());
            } catch (InterruptedException | ExecutionException e) {
                throw new RuntimeException(e);
            }
        });
    }
}

运行结果如下:

14:45:36.741 [pool-1-thread-1] DEBUG org.example2.a47.TestInvokeAll - begin...
14:45:36.741 [pool-1-thread-2] DEBUG org.example2.a47.TestInvokeAll - begin...
14:45:37.260 [pool-1-thread-2] DEBUG org.example2.a47.TestInvokeAll - begin...
14:45:39.277 [main] DEBUG org.example2.a47.TestInvokeAll - 111
14:45:39.280 [main] DEBUG org.example2.a47.TestInvokeAll - 222
14:45:39.280 [main] DEBUG org.example2.a47.TestInvokeAll - 333

接下来我们演示一次性往线程池中提交多个任务但是只执行最早完成的任务:

public class TestInvokeAny {
    private static final Logger log = LoggerFactory.getLogger(TestInvokeAny.class);
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        String result = pool.invokeAny(Arrays.asList(() -> {
                    log.debug("begin...");
                    Thread.sleep(1000);
                    return "111";
                },

                () -> {
                    log.debug("begin...");
                    Thread.sleep(500);
                    return "222";
                },

                () -> {
                    log.debug("begin...");
                    Thread.sleep(2000);
                    return "333";
                }));

        log.debug("{}",result);
    }
}
15:30:05.361 [pool-1-thread-2] DEBUG org.example2.a47.TestInvokeAny - begin...
15:30:05.361 [pool-1-thread-1] DEBUG org.example2.a47.TestInvokeAny - begin...
15:30:05.878 [pool-1-thread-2] DEBUG org.example2.a47.TestInvokeAny - begin...
15:30:05.878 [main] DEBUG org.example2.a47.TestInvokeAny - 222

我们发现,线程池中的其中一个任务执行完毕后,剩余的任务会终止执行!

5)关闭线程池

shutdown

把线程池的状态改为 SHUTDOWN

  • 不会接收新的任务
  • 但已经提交的任务会执行完毕
  • 此方法不会阻塞调用线程的执行(shutdown是异步操作)

void shutdown()

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        // 首先对线程池进行加锁
        mainLock.lock();
        try {
            checkShutdownAccess();
            // 修改线程池的状态: 线程池状态会从 RUNNING  ---> SHUTDOWN
            advanceRunState(SHUTDOWN);
            // 仅仅会打断空闲的线程(没事可做的线程会被打断,正在执行任务的线程并不会被打断)
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        // 尝试终结(没有运行的线程立刻终结,如果还有运行的线程也不会等待,让这些线程自己运行完了自己结束) 
        tryTerminate();
    }

shutdownNow

会将线程池的状态从 RUNNING 改为 STOP状态

  • 不会接收新的任务
  • 会将队列中的任务返回
  • 并用interrupt的方式中断正在执行的任务

List shutdownNow();

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        // 首先对线程池进行加锁
        mainLock.lock();
        try {
            checkShutdownAccess();
            // 将线程池的状态改为 STOP
            advanceRunState(STOP);
            // 打断所有线程(包括空闲的线程和正在执行任务的线程)
            interruptWorkers();
            // 将队列中剩余的任务取出来并返回
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        // 尝试终结-->肯定会成功,此时活跃的线程数必定为0
        tryTerminate();
        return tasks;
    }

其它的方法

// 线程池的状态不是 RUNNING ,此方法就会返回true
boolean isShutdown();

// 线程池的状态是否为终结(TERMINATED)状态-->该状态线程池已经停止工作了
boolean isTerminated();

// 调用shutdown()方法后,由于调用线程不会等待所有任务执行结束,因此如果它想在线程池 TERMINATED 后做些善后的事情,可以利用此方法等待(主方法调用)
boolean awaitTermination(long timeout, TimeUnit unit);

线程池具体使用

接下来我们举个例子来演示线程池这些方法的使用:

package org.example2.a47;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class TestShutdown {
    private static final Logger log = LoggerFactory.getLogger(TestShutdown.class);
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        Future<Integer> resutFuture1 = pool.submit(() -> {
            log.debug("task 1 running...");
            Thread.sleep(1000);
            log.debug("task 1 finished...");
            return 1;
        });

        Future<Integer> resultFuture2 = pool.submit(()->{
            log.debug("task 2 running...");
            Thread.sleep(1000);
            log.debug("task 2 finished...");
            return 2;
        });

        Future<Integer> resultFuture3 = pool.submit(()->{
            log.debug("task 3 running...");
            Thread.sleep(1000);
            log.debug("task 3 finished...");
            return 3;
        });

        log.debug("shutdown");
        pool.shutdown();
    }
}

运行结果如下:

15:58:23.960 [main] DEBUG org.example2.a47.TestShutdown - shutdown
15:58:23.960 [pool-1-thread-1] DEBUG org.example2.a47.TestShutdown - task 1 running...
15:58:23.960 [pool-1-thread-2] DEBUG org.example2.a47.TestShutdown - task 2 running...
15:58:24.979 [pool-1-thread-2] DEBUG org.example2.a47.TestShutdown - task 2 finished...
15:58:24.979 [pool-1-thread-1] DEBUG org.example2.a47.TestShutdown - task 1 finished...
15:58:24.979 [pool-1-thread-2] DEBUG org.example2.a47.TestShutdown - task 3 running...
15:58:25.990 [pool-1-thread-2] DEBUG org.example2.a47.TestShutdown - task 3 finished...

我们发现,将三个任务提交完成后,立刻调用线程池的shutdown()方法并不会将提交后的任务取消和关闭,而是等待这些任务执行完毕后才关闭线程池。

当然,如果在调用shutwodn方法后再次提交任务,则会报错,也就是执行拒绝策略,如下面所示:

public class TestShutdown {
    private static final Logger log = LoggerFactory.getLogger(TestShutdown.class);
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        Future<Integer> resultFuture1 = pool.submit(() -> {
            log.debug("task 1 running...");
            Thread.sleep(1000);
            log.debug("task 1 finished...");
            return 1;
        });

        Future<Integer> resultFuture2 = pool.submit(()->{
            log.debug("task 2 running...");
            Thread.sleep(1000);
            log.debug("task 2 finished...");
            return 2;
        });

        Future<Integer> resultFuture3 = pool.submit(()->{
            log.debug("task 3 running...");
            Thread.sleep(1000);
            log.debug("task 3 finished...");
            return 3;
        });

        log.debug("shutdown");
        pool.shutdown();

        Future<Integer> resultFuture4 = pool.submit(()->{
            log.debug("task 4 running...");
            Thread.sleep(1000);
            log.debug("task 4 finished...");
            return 4;
        });
    }
}

16:05:58.806 [main] DEBUG org.example2.a47.TestShutdown - shutdown
16:05:58.806 [pool-1-thread-1] DEBUG org.example2.a47.TestShutdown - task 1 running...
16:05:58.806 [pool-1-thread-2] DEBUG org.example2.a47.TestShutdown - task 2 running...
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@546a03af rejected from java.util.concurrent.ThreadPoolExecutor@721e0f4f[Shutting down, pool size = 2, active threads = 2, queued tasks = 1, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134)
	at org.example2.a47.TestShutdown.main(TestShutdown.java:39)
16:05:59.815 [pool-1-thread-2] DEBUG org.example2.a47.TestShutdown - task 2 finished...
16:05:59.815 [pool-1-thread-1] DEBUG org.example2.a47.TestShutdown - task 1 finished...
16:05:59.815 [pool-1-thread-1] DEBUG org.example2.a47.TestShutdown - task 3 running...
16:06:00.829 [pool-1-thread-1] DEBUG org.example2.a47.TestShutdown - task 3 finished...

当然,我们在调用shutdown方法后可以处理一些善后工作,例如:

public class TestShutdown {
    private static final Logger log = LoggerFactory.getLogger(TestShutdown.class);
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        Future<Integer> resultFuture1 = pool.submit(() -> {
            log.debug("task 1 running...");
            Thread.sleep(1000);
            log.debug("task 1 finished...");
            return 1;
        });

        Future<Integer> resultFuture2 = pool.submit(()->{
            log.debug("task 2 running...");
            Thread.sleep(1000);
            log.debug("task 2 finished...");
            return 2;
        });

        Future<Integer> resultFuture3 = pool.submit(()->{
            log.debug("task 3 running...");
            Thread.sleep(1000);
            log.debug("task 3 finished...");
            return 3;
        });

        log.debug("shutdown");
        pool.shutdown();

        try {
            pool.awaitTermination(3, TimeUnit.SECONDS);
            log.debug("shutdown调用后可以完成一些善后工作");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

16:08:28.253 [main] DEBUG org.example2.a47.TestShutdown - shutdown
16:08:28.253 [pool-1-thread-2] DEBUG org.example2.a47.TestShutdown - task 2 running...
16:08:28.253 [pool-1-thread-1] DEBUG org.example2.a47.TestShutdown - task 1 running...
16:08:29.260 [pool-1-thread-1] DEBUG org.example2.a47.TestShutdown - task 1 finished...
16:08:29.260 [pool-1-thread-2] DEBUG org.example2.a47.TestShutdown - task 2 finished...
16:08:29.261 [pool-1-thread-1] DEBUG org.example2.a47.TestShutdown - task 3 running...
16:08:30.274 [pool-1-thread-1] DEBUG org.example2.a47.TestShutdown - task 3 finished...
16:08:30.275 [main] DEBUG org.example2.a47.TestShutdown - shutdown调用后可以完成一些善后工作

接着我们调用shutdowNow()方法:

public class TestShutdown {
    private static final Logger log = LoggerFactory.getLogger(TestShutdown.class);
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        Future<Integer> resultFuture1 = pool.submit(() -> {
            log.debug("task 1 running...");
            Thread.sleep(1000);
            log.debug("task 1 finished...");
            return 1;
        });

        Future<Integer> resultFuture2 = pool.submit(()->{
            log.debug("task 2 running...");
            Thread.sleep(1000);
            log.debug("task 2 finished...");
            return 2;
        });

        Future<Integer> resultFuture3 = pool.submit(()->{
            log.debug("task 3 running...");
            Thread.sleep(1000);
            log.debug("task 3 finished...");
            return 3;
        });

        log.debug("shutdownNow");
        List<Runnable> unExecutedTasks = pool.shutdownNow();    // 返回未执行的任务集合
        log.debug("未执行的任务队列:{}",unExecutedTasks);
    }
}

测试结果如下:

16:11:49.348 [main] DEBUG org.example2.a47.TestShutdown - shutdownNow
16:11:49.348 [pool-1-thread-1] DEBUG org.example2.a47.TestShutdown - task 1 running...
16:11:49.348 [pool-1-thread-2] DEBUG org.example2.a47.TestShutdown - task 2 running...
16:11:49.351 [main] DEBUG org.example2.a47.TestShutdown - 未执行的任务队列:[java.util.concurrent.FutureTask@1888ff2c]

6)异步模式之工作线程

6.1 定义

让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。
例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那么成本就太高了(对t比另一种多线程设计模式:Thread-Pcr-Message)

注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率。

例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工。

6.2 饥饿

固定大小的线程池会有饥饿现象(线程数量不足可能会导致饥饿现象)

  • 两个工人是同一个线程池中的两个线程
  • 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
    • 客人点餐:必须先点完餐,等菜做好,上菜,假定在此期间处理点餐的工人必须等待
    • 后厨做菜:没啥说的,做就是了
  • 比如工人A处理了点餐任务,接下来它要等着工人B把菜做好,然后上菜,他俩也配合的蛮好
  • 但现在同时来了两个客人,这个时候工人A和工人B都去处理点餐了,这时没人做饭了,死等

我们现在模拟就一个客户来点菜吃饭的情况:

package org.example2.a48;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class TestDeadLock {
    private static final Logger log = LoggerFactory.getLogger(TestDeadLock.class);
    static final List<String> MENU = Arrays.asList("地三鲜","宫保鸡丁","辣子鸡丁","烤鸡翅");
    static Random RANDOM = new Random();
    static String cooking(){
        return MENU.get(RANDOM.nextInt(MENU.size()));
    }

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        pool.execute(()->{
            log.debug("处理点餐");  // 点餐的任务
            Future<String> f = pool.submit(() -> {  // 做菜的任务
                log.debug("做菜");
                return cooking();
            });
            try{
                log.debug("上菜:{}",f.get());
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });
    }
}

测试结果如下:

21:04:25.050 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 处理点餐
21:04:25.055 [pool-1-thread-2] DEBUG org.example2.a48.TestDeadLock - 做菜
21:04:25.055 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 上菜:烤鸡翅

我们发现当只有一个客人来时运行并没有问题。但是我们现在再模拟一下同时来两个客人的情况:

package org.example2.a48;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class TestDeadLock {
    private static final Logger log = LoggerFactory.getLogger(TestDeadLock.class);
    static final List<String> MENU = Arrays.asList("地三鲜","宫保鸡丁","辣子鸡丁","烤鸡翅");
    static Random RANDOM = new Random();
    static String cooking(){
        return MENU.get(RANDOM.nextInt(MENU.size()));
    }

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        // 第一个客人
        pool.execute(()->{
            log.debug("处理点餐");  // 点餐的任务
            Future<String> f = pool.submit(() -> {  // 做菜的任务
                log.debug("做菜");
                return cooking();
            });
            try{
                log.debug("上菜:{}",f.get());
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });

        // 第二个客人
        pool.execute(()->{
            log.debug("处理点餐");  // 点餐的任务
            Future<String> f = pool.submit(() -> {  // 做菜的任务
                log.debug("做菜");
                return cooking();
            });
            try{
                log.debug("上菜:{}",f.get());
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });
    }
}

运行如下:

21:07:34.963 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 处理点餐
21:07:34.963 [pool-1-thread-2] DEBUG org.example2.a48.TestDeadLock - 处理点餐

我们发现,两个工人同时为两个客户处理点餐了,但是没有别的工人做饭了,此时呢,就陷入了死等(注意这可不是死锁!!!因为这两个工人线程并没有占用双方想要拥有的资源,而是他们在等待同一个资源)这是由于工作线程数不足导致任务无法继续执行的下去!也就是饥饿现象

6.3 饥饿的解决

那我们来解决一下饥饿的现象,有人认为,我们再加一个工作线程不就好了吗?例如下面的代码,线程池固定大小为3:

package org.example2.a48;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class TestDeadLock {
    private static final Logger log = LoggerFactory.getLogger(TestDeadLock.class);
    static final List<String> MENU = Arrays.asList("地三鲜","宫保鸡丁","辣子鸡丁","烤鸡翅");
    static Random RANDOM = new Random();
    static String cooking(){
        return MENU.get(RANDOM.nextInt(MENU.size()));
    }

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        // 第一个客人
        pool.execute(()->{
            log.debug("处理点餐");  // 点餐的任务
            Future<String> f = pool.submit(() -> {  // 做菜的任务
                log.debug("做菜");
                return cooking();
            });
            try{
                log.debug("上菜:{}",f.get());
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });

        // 第二个客人
        pool.execute(()->{
            log.debug("处理点餐");  // 点餐的任务
            Future<String> f = pool.submit(() -> {  // 做菜的任务
                log.debug("做菜");
                return cooking();
            });
            try{
                log.debug("上菜:{}",f.get());
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });
    }
}
21:14:22.704 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 处理点餐
21:14:22.704 [pool-1-thread-2] DEBUG org.example2.a48.TestDeadLock - 处理点餐
21:14:22.709 [pool-1-thread-3] DEBUG org.example2.a48.TestDeadLock - 做菜
21:14:22.709 [pool-1-thread-3] DEBUG org.example2.a48.TestDeadLock - 做菜
21:14:22.709 [pool-1-thread-2] DEBUG org.example2.a48.TestDeadLock - 上菜:烤鸡翅
21:14:22.709 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 上菜:地三鲜

我发现,完全可以处理两个客户的需求,但这能根本解决问题吗,很显然是不行的,因为来就餐的客户数量是未知的,我们无法预测我们究竟要创建几个工人线程来解决线程饥饿问题。为此,我们可以为不同类型的任务创建不同的线程池,这样就能避免饥饿问题,从而提高效率,为此,我们改造代码:

package org.example2.a48;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class TestDeadLock {
    private static final Logger log = LoggerFactory.getLogger(TestDeadLock.class);
    static final List<String> MENU = Arrays.asList("地三鲜","宫保鸡丁","辣子鸡丁","烤鸡翅");
    static Random RANDOM = new Random();
    static String cooking(){
        return MENU.get(RANDOM.nextInt(MENU.size()));
    }

    public static void main(String[] args) {
        // 服务员线程池
        ExecutorService waitPool = Executors.newFixedThreadPool(2);

        // 厨师线程池
        ExecutorService cookPool = Executors.newFixedThreadPool(2);
        // 第一个客人
        waitPool.execute(()->{
            log.debug("处理点餐");  // 点餐的任务
            Future<String> f = cookPool.submit(() -> {  // 做菜的任务
                log.debug("做菜");
                return cooking();
            });
            try{
                log.debug("上菜:{}",f.get());
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });

        // 第二个客人
        waitPool.execute(()->{
            log.debug("处理点餐");  // 点餐的任务
            Future<String> f = cookPool.submit(() -> {  // 做菜的任务
                log.debug("做菜");
                return cooking();
            });
            try{
                log.debug("上菜:{}",f.get());
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });
    }
}

21:19:44.589 [pool-1-thread-2] DEBUG org.example2.a48.TestDeadLock - 处理点餐
21:19:44.589 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 处理点餐
21:19:44.601 [pool-2-thread-1] DEBUG org.example2.a48.TestDeadLock - 做菜
21:19:44.601 [pool-2-thread-2] DEBUG org.example2.a48.TestDeadLock - 做菜
21:19:44.601 [pool-1-thread-2] DEBUG org.example2.a48.TestDeadLock - 上菜:宫保鸡丁
21:19:44.601 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 上菜:烤鸡翅

我们发现,这样就可以避免饥饿现象,我们多创建几个任务测试一下:

package org.example2.a48;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class TestDeadLock {
    private static final Logger log = LoggerFactory.getLogger(TestDeadLock.class);
    static final List<String> MENU = Arrays.asList("地三鲜","宫保鸡丁","辣子鸡丁","烤鸡翅");
    static Random RANDOM = new Random();
    static String cooking(){
        return MENU.get(RANDOM.nextInt(MENU.size()));
    }

    public static void main(String[] args) {
        // 服务员线程池
        ExecutorService waitPool = Executors.newFixedThreadPool(2);

        // 厨师线程池
        ExecutorService cookPool = Executors.newFixedThreadPool(2);
        // 第一个客人
        waitPool.execute(()->{
            log.debug("处理点餐");  // 点餐的任务
            Future<String> f = cookPool.submit(() -> {  // 做菜的任务
                log.debug("做菜");
                return cooking();
            });
            try{
                log.debug("上菜:{}",f.get());
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });

        // 第二个客人
        waitPool.execute(()->{
            log.debug("处理点餐");  // 点餐的任务
            Future<String> f = cookPool.submit(() -> {  // 做菜的任务
                log.debug("做菜");
                return cooking();
            });
            try{
                log.debug("上菜:{}",f.get());
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });

        // 第三个客人
        waitPool.execute(()->{
            log.debug("处理点餐");  // 点餐的任务
            Future<String> f = cookPool.submit(() -> {  // 做菜的任务
                log.debug("做菜");
                return cooking();
            });
            try{
                log.debug("上菜:{}",f.get());
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });

        // 第四个客人
        waitPool.execute(()->{
            log.debug("处理点餐");  // 点餐的任务
            Future<String> f = cookPool.submit(() -> {  // 做菜的任务
                log.debug("做菜");
                return cooking();
            });
            try{
                log.debug("上菜:{}",f.get());
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });

        // 第五个客人
        waitPool.execute(()->{
            log.debug("处理点餐");  // 点餐的任务
            Future<String> f = cookPool.submit(() -> {  // 做菜的任务
                log.debug("做菜");
                return cooking();
            });
            try{
                log.debug("上菜:{}",f.get());
            } catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }
        });
    }
}
21:21:23.106 [pool-1-thread-2] DEBUG org.example2.a48.TestDeadLock - 处理点餐
21:21:23.106 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 处理点餐
21:21:23.115 [pool-2-thread-1] DEBUG org.example2.a48.TestDeadLock - 做菜
21:21:23.115 [pool-2-thread-2] DEBUG org.example2.a48.TestDeadLock - 做菜
21:21:23.115 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 上菜:地三鲜
21:21:23.115 [pool-1-thread-2] DEBUG org.example2.a48.TestDeadLock - 上菜:烤鸡翅
21:21:23.118 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 处理点餐
21:21:23.118 [pool-1-thread-2] DEBUG org.example2.a48.TestDeadLock - 处理点餐
21:21:23.119 [pool-2-thread-2] DEBUG org.example2.a48.TestDeadLock - 做菜
21:21:23.119 [pool-2-thread-1] DEBUG org.example2.a48.TestDeadLock - 做菜
21:21:23.119 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 上菜:烤鸡翅
21:21:23.119 [pool-1-thread-2] DEBUG org.example2.a48.TestDeadLock - 上菜:地三鲜
21:21:23.119 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 处理点餐
21:21:23.120 [pool-2-thread-2] DEBUG org.example2.a48.TestDeadLock - 做菜
21:21:23.120 [pool-1-thread-1] DEBUG org.example2.a48.TestDeadLock - 上菜:宫保鸡丁

我们发现,依然可以稳定运行!并没有出现线程饥饿问题!

6.4 究竟创建多少线程池合适呢?

  • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
  • 过大会导致更多的线程上下文切换,占用更多内存

6.4.1 CPU密集型运算

通常采用cpu核数+1能够实现最优的CPU利用率,+1是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,此时该核心没有被利用的上,额外的这个线程就能顶上去占用该核心CPU资源,保证CPU时钟周期不被浪费(总之就是不能让CPU浪费掉)

6.4.2 IO密集型运算

CPU不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用CPU资源,但当你执行IO操作时、远程RPC调用时,包括进行数据库操作时,这时候CPU就闲下来了,你可以利用多线程提高它的利用率。经验公式如下:

线程数 = CPU核心数 * 期望CPU利用率 * CPU总时间数(CPU计算时间 + CPU等待时间) / CPU计算时间

例如4核CPU计算时间是50%,其它等待时间是50%,期望cpu被100%利用,套用公式
4 * 100% * 100% / 50% = 8

6.5 任务调度线程池

在『任务调度线程池』功能加入之前,可以使用java.util.Timer来实现定时功能,Timer的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。

我们这里看一下Timer的基本使用:

package org.example2.a48;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Timer;
import java.util.TimerTask;

public class TimerTest {
    private static final Logger log = LoggerFactory.getLogger(TimerTest.class);

    public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                try {
                    log.debug("task 1...");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };

        TimerTask task2 = new TimerTask() {
            @Override
            public void run() {
                log.debug("task 2...");
            }
        };

        log.debug("start...");
        timer.schedule(task1,1000); // 1s后执行任务1
        timer.schedule(task2,1000); // 1s后执行任务2
    }
}
22:13:36.451 [main] DEBUG org.example2.a48.TimerTest - start...
22:13:37.463 [Timer-0] DEBUG org.example2.a48.TimerTest - task 1...
22:13:39.467 [Timer-0] DEBUG org.example2.a48.TimerTest - task 2...

我们发现,主线程在36S运行时,1S后任务1开始运行了,此时任务2本身也要执行的,但是任务2主执行的时间比较长,需要执行2S,因此2S后任务2才开始执行。这也就说明:使用Timer做定时任务,如果一个任务所消耗的时间比较长,则会影响到另一个任务的执行(一般是延长任务开始执行的时间)

那么一个任务在执行时出现异常会出现什么情况?如下面所示:

package org.example2.a48;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Timer;
import java.util.TimerTask;

public class TimerTest {
    private static final Logger log = LoggerFactory.getLogger(TimerTest.class);

    public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                log.debug("task 1...");
                int i = 10 / 0;
            }
        };

        TimerTask task2 = new TimerTask() {
            @Override
            public void run() {
                log.debug("task 2...");
            }
        };

        log.debug("start...");
        timer.schedule(task1, 1000); // 1s后执行任务1
        timer.schedule(task2, 1000); // 1s后执行任务2
    }
}
22:19:06.944 [main] DEBUG org.example2.a48.TimerTest - start...
22:19:07.947 [Timer-0] DEBUG org.example2.a48.TimerTest - task 1...
Exception in thread "Timer-0" java.lang.ArithmeticException: / by zero
	at org.example2.a48.TimerTest$1.run(TimerTest.java:18)
	at java.util.TimerThread.mainLoop(Timer.java:555)
	at java.util.TimerThread.run(Timer.java:505)

我们发现,任务1在执行过程中出现了异常,任务2根本没有被执行!这也就导致了任务线程意外中止了。因此呢,这个Timer表现的很脆弱😃

6.6 ScheduledThreadPoolExecutor任务调度线程池(延迟执行)

下面我们演示一下带有任务调度功能的线程池来改进刚才的Timer:

package org.example2.a48;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolTest {
    private static final Logger log = LoggerFactory.getLogger(ScheduledThreadPoolTest.class);
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);

        pool.schedule(()->{
            log.debug("task1");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, 1,TimeUnit.SECONDS);

        pool.schedule(()->{
            log.debug("task2");
        }, 1,TimeUnit.SECONDS);
    }
}
22:27:44.945 [pool-1-thread-2] DEBUG o.e.a48.ScheduledThreadPoolTest - task2
22:27:44.945 [pool-1-thread-1] DEBUG o.e.a48.ScheduledThreadPoolTest - task1

我们发现这两个任务都是同时进行的,解决了Timer定时器的缺点:虽然第一个任务被延迟2S,但是并不会延迟第二个任务执行。当然,如果把线程池中的线程数改为1,多个任务也是串行执行的。接下来我们再看看如果一个任务没有正确的处理异常,会不会影响到第二个任务的执行:

package org.example2.a48;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolTest {
    private static final Logger log = LoggerFactory.getLogger(ScheduledThreadPoolTest.class);

    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);

        pool.schedule(() -> {
            log.debug("task1");
            int  i =10 / 0;
        }, 1, TimeUnit.SECONDS);

        pool.schedule(() -> {
            log.debug("task2");
        }, 1, TimeUnit.SECONDS);
    }
} 	
22:31:22.909 [pool-1-thread-2] DEBUG o.e.a48.ScheduledThreadPoolTest - task2
22:31:22.909 [pool-1-thread-1] DEBUG o.e.a48.ScheduledThreadPoolTest - task1

我们发现并不会。此外,即使把线程池中的线程数改为1,就是一个任务在运期间出现了异常,也不会影响到第二个任务的执行:

public class ScheduledThreadPoolTest {
    private static final Logger log = LoggerFactory.getLogger(ScheduledThreadPoolTest.class);

    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);

        pool.schedule(() -> {
            log.debug("task1");
            int  i =10 / 0;
        }, 1, TimeUnit.SECONDS);

        pool.schedule(() -> {
            log.debug("task2");
        }, 1, TimeUnit.SECONDS);
    }
}
22:34:17.137 [pool-1-thread-1] DEBUG o.e.a48.ScheduledThreadPoolTest - task1
22:34:17.140 [pool-1-thread-1] DEBUG o.e.a48.ScheduledThreadPoolTest - task2

6.7 ScheduledThreadPoolExecutor任务调度线程池(定时执行)

ScheduledThreadPoolExecutor除了可以延迟执行任务以外,还可以定时执行任务,也就是每隔一段时间执行一遍。例如下面的代码:

package org.example2.a48;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolTest {
    private static final Logger log = LoggerFactory.getLogger(ScheduledThreadPoolTest.class);

    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
        log.debug("start...");
        pool.scheduleAtFixedRate(()->{	// 1S后每隔1S执行一次任务
            log.debug("running...");
        },1, 1, TimeUnit.SECONDS);
    }
}
23:02:10.752 [main] DEBUG o.e.a48.ScheduledThreadPoolTest - start...
23:02:11.814 [pool-1-thread-1] DEBUG o.e.a48.ScheduledThreadPoolTest - running...
23:02:12.821 [pool-1-thread-1] DEBUG o.e.a48.ScheduledThreadPoolTest - running...
23:02:13.820 [pool-1-thread-1] DEBUG o.e.a48.ScheduledThreadPoolTest - running...
23:02:14.814 [pool-1-thread-1] DEBUG o.e.a48.ScheduledThreadPoolTest - running...

除了scheduleAtFixedRate方法外,还有一个方法是scheduleWithFixedDelay,这个方法基本和scheduleAtFixedRate功能一样,只不过是任务与任务之间相隔一定的时间才执行。

6.8 正确处理线程池异常

前面我们发现,如果我们在线程池中执行任务如果出现了异常,这个异常既没有被抛出来,也没有在控制台显示,因此呢,我们需要正确的处理异常。

第一个方法就是我们使用try catch代码块自己捕获异常。例如下面的代码:

public class ScheduledThreadPoolTest {
    private static final Logger log = LoggerFactory.getLogger(ScheduledThreadPoolTest.class);

    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
        log.debug("start...");
        pool.scheduleAtFixedRate(()->{
            try{
                log.debug("running...");
                int  i =10 / 0;
            } catch (Exception e){
                e.printStackTrace();
            }
        },1, 1, TimeUnit.SECONDS);
    }
}
java.lang.ArithmeticException: / by zero
	at org.example2.a48.ScheduledThreadPoolTest.lambda$main$0(ScheduledThreadPoolTest.java:19)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

第二个方法使用Callable配合Futurre可以使用get()方法获取异常信息,例如:

import java.util.concurrent.*;

public class ScheduledThreadPoolTest {
    private static final Logger log = LoggerFactory.getLogger(ScheduledThreadPoolTest.class);

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(1);
        Future<Boolean> task1 = pool.submit(() -> {
            log.debug("task1");
            int i = 1 / 0;
            return true;
        });
        log.debug("返回结果为:{}",task1.get());
    }
}
23:42:56.748 [pool-1-thread-1] DEBUG o.e.a48.ScheduledThreadPoolTest - task1
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
	at java.util.concurrent.FutureTask.report(FutureTask.java:122)
	at java.util.concurrent.FutureTask.get(FutureTask.java:192)
	at org.example2.a48.ScheduledThreadPoolTest.main(ScheduledThreadPoolTest.java:18)
Caused by: java.lang.ArithmeticException: / by zero
	at org.example2.a48.ScheduledThreadPoolTest.lambda$main$0(ScheduledThreadPoolTest.java:15)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

6.9 线程池应用-定时任务

举个例子:

如何让每周四的18:00:00 定时执行任务?

public class ScheduledThreadPoolTest {
    private static final Logger log = LoggerFactory.getLogger(ScheduledThreadPoolTest.class);

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
        // 计算当前时间和每周四的时间差
        LocalDateTime now = LocalDateTime.now();
        // 找到周四的时间
        LocalDateTime time = now.withHour(18).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY);

        // 如果当前时间大于周四,找到下一个周四
        if (now.compareTo(time)>0) {
            time = time.plusWeeks(1);
        }
        // 获取延迟时间,也就是两个时间之间的差
        long initialDelay = Duration.between(now, time).toMillis();

        // 计算一周的间隔时间
        long period = 1000 * 60 * 60 * 24 * 7;
        pool.scheduleAtFixedRate(() -> {

        }, initialDelay, period, TimeUnit.MILLISECONDS);
    }
}

6.10 Tomcat线程池

Tomcat在哪里用到了线程池?Tomcat的连接器部分,如下图所示:

image-20240804110234069

  • LimitLatch用来限流,可以控制最大连接个数,类似J.U.c中的Semaphore后面再讲
  • Acceptor只负责【接收新的socket连接】
  • Poller只负责监听socket channel是否有【可读的I/O事件】
  • 一旦可读,封装一个任务对象(socketProcessor),提交给Executor线程池处理
  • Executor线程池中的工作线程最终负责【处理请求】

Tomcat线程池扩展了ThreadPoolExecutor,但行为稍有不同

  • 如果总线程池数达到maximumPoolSize
    • 这时不会立即抛出拒绝执行异常
    • 而是再次尝试讲该任务放入等待队列中等待,如果还是失败,才会抛出拒绝执行异常

Tomcat7源码:

image-20240804103204497

public void execute(Runnable command,long timeout,TimeUnit unit){ submittedCount.incrementAndGet();
    try{
        super.execute(command);
    } catch (RejectedExecutionException rx){	// Tomcat终会捕获拒绝执行异常
    	if (super.getQueue()instanceof TaskQueue){
            // 首先拿到任务队列
    		final TaskQueue queue (TaskQueue)super.getQueue();
   			try{
                // 尝试将该任务加入到队列中,如果失败,就会抛出拒绝执行异常,成功则不会抛出异常
                if (queue.force(command,timeout,unit)){
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException("Queue capacity is full.");
                }
            } catch (InterruptedException x){
                submittedCount.decrementAndGet();
                Thread.interrupted();
                throw new RejectedExecutionException(x);
            }
        } else{
                submittedCount.decrementAndGet();
                throw rx;
        }
    }
}

taskQueue.java

public boolean force(Runnable o,long timeout,TimeUnit unit)throws InterruptedException
    if parent.isShutdown())
        throw new RejectedExedutionException("Executor not running,can't force a command into the queue");
    // 尝试放入队列
    return super.offer(o,timeout,unit);//forces the item onto the queue,to be used if the task is rejected
}

6.11 Tomcat线程池相关的配置

Connector配置

配置项 默认值 说明
acceptorThreadCount 1 acceptor线程数量
pollerThreadCount 1 poller线程数量
minSpareThreads 10 核心线程数,即coreThreadSize
maxThreads 200 最大线程数,即maximumPoolSize
executor - Executor名称,用来引用下面的Executor

Executor线程配置

配置项 默认值 说明
threadPriority 5 线程优先级
daemon true 是否是守护线程
minSpareThreads 25 核心线程数,即corePoolSize
maxThreads 200 最大线程数,即maximunPoolSize
maxIdleTime 60000 救急线程生存时间,默认是1min(单位是ms)
maxQueueSize Integer.MAXX_VALUE(无界队列) 阻塞队列的长度
prestartminSpareThreads false 核心线程是否在服务器启动时启动(核心线程是否是懒惰初始化)

这时就有人疑问了,既然Tomcat中阻塞队列时无界的,是不是意味着就不会创建救急线程了呢?因为我们知道,在JAVA线程池中,只有阻塞队列满了才会创建救急线程。

答案是不对的。Tomcat为救急线程的创建实现进行了调整,如下图所示:

image-20240804111425952

在Tomcat中,刚开始添加新任务时,首先会检查提交的任务数是否小于核心线程数

  • 如果提交的任务数小于核心线程数,则会加入到队列中等待核心线程的执行
  • 如果提交的任务数大于等于核心线程数,则进一步判断提交任务数与最大线程数进行比较:
    • 如果提交任务数小于最大线程数,则会将任务加入到阻塞队列
    • 如果提交的任务数大于最大线程数,则会创建救急线程,并将该任务加入到阻塞队列中

6.12 比较高级的线程池 Fork/Join

1)概念

Fork/Join是DK1.7加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的cpu密集型运算

所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解

Fok/Jo在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率

Fork/Join默认会创建与cpu核心数大小相同的线程池

2)使用

提交给Fork/Join线程池的任务需要继承RecursiveTask(有返回值)或RecursiveAction(没有返回值),例如下面定义了一个对1-n之间的整数求和的任务:

package org.example2.a48;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class TestForkJoin {
    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool(4);
        System.out.println(pool.invoke(new MyTask(5)));
        // new MyTask(5) = 5 + new MyTask(4) + new MyTask(3) + new MyTask(2) + new MyTask(1)
    }
}

// 求1-n之间整数的和
class MyTask extends RecursiveTask<Integer> {

    private Integer n;

    public MyTask(Integer n) {
        this.n = n;
    }

    @Override
    protected Integer compute() {
        // 终止条件
        if (n == 1) return 1;

        // 注意:其核心是将多个递归操作交给多线程来做,这一点和传统递归是不同的!
        MyTask task = new MyTask(n - 1);
        task.fork();    //拆分:让一个线程去执行此任务
        int result = n + task.join();    //获取任务结果(合并多个线程运算后的结果)
        return result;
    }
}

需要注意的是,并行是在计算时候的并行,最终的结果合并是串行的。用图来表示如下:

image-20240804120224592

posted @   LilyFlower  阅读(14)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
历史上的今天:
2021-08-04 计算机网络-4-2-分类的IP地址
2021-08-04 计算机网络-4-3-ARP地址解析协议以及IP数据报首部
2021-08-04 java 文件上传
2021-08-04 java文件上传工具包
2021-08-04 计算机网络-4-1-虚拟互联网络的概念
2021-08-04 计算机网络-3-1-数据链路层基本概念
2021-08-04 计算机网络-2-4-数字传输系统
点击右上角即可分享
微信分享提示