【web】java多线程(单例模式+阻塞队列+定时器+线程池)

简介: 主要介绍了多线程单例模式、阻塞队列、定时器、线程池、多线程面试考点。

【大家好,我是爱干饭的猿,本文是多线程初级入门,主要介绍了多线程单例模式、阻塞队列、定时器、线程池、多线程面试考点。

后续会继续分享线程常见的锁策略(共享锁VS独占锁、重入锁VS不可重入锁、公平锁VS不公平锁、乐观锁VS悲观锁)及其他重要知识点总结,如果喜欢这篇文章,点个赞👍,关注一下吧,】

上一篇文章:《【web】java多线程(吐血万字超详解)》


🤞目录🤞

💖1. 多线程案例

🌐1. 单例模式(singleton pattern)

1.1 懒汉模式

1.2 饿汉模式

🌐2 阻塞队列(blocking queue)

2.1 阻塞队列

2.2 生产者消费者模型:

2.3 标准库中的阻塞队列

2.4 实现阻塞队列

🌐3. 定时器

3.1 标准库中的定时器

3.2 实现定时器

🌐4 线程池

4.2 Executors 创建线程池的几种方式

4.3 利用线程池 创建多线程计算fib 数

4.4 实现线程池


🚝1. 多线程案例

🚀1. 单例模式(singleton pattern)

单例模式是通过代码,保护一个类,使得类在整个进程(应用)运行过程中有且只有一个。

常用在配置对象、控制类。

设计模式(design pattern):对一些解决通用问题的、经常书写得代码片段的总结与归纳。

1.1 懒汉模式

一开始就初始化

public class StarvingMode {
    // 是线程安全的
    // 类加载的时候执行
    // JVM 保证了类加载的过程是线程安全的
    private static StarvingMode instance = new StarvingMode();
    public static StarvingMode getInstance() {
        return instance;
    }
    // 将构造方法私有化,防止其他线程new
    private StarvingMode() {}
}

image.gif

1.2 饿汉模式

等到用的时候在进行初始化

a. 饿汉模式-单线程版

类加载的时候不创建实例,第一次使用的时候才创建实例

public class LazyModeV1 {
    private static LazyModeV1 instance = null;
    public static LazyModeV1 getInstance(){
        // 第一次调用这个方法时,说明我们应该实例化对象了
        // 原子性
        if (instance == null) {
            instance = new LazyModeV1();    // 只在第一次的时候执行
        }
        return instance;
    }
    // 将构造方法私有化,防止其他线程new
    private LazyModeV1(){};
}

image.gif

但是如果在多个线程中同时调用 getInstance 方法, 就可能导致创 建出多个实例,一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改 instance 了)

b. 饿汉模式-多线程版

加 synchronized 锁 使线程安全

public class LazyModeV2 {
    private static LazyModeV2 instance = null;
    // 加synchronized锁,但是这样性能太低,所以有了mode3
    public synchronized static LazyModeV2 getInstance(){
        // 第一次调用这个方法时,说明我们应该实例化对象了
        if (instance == null) {
            instance = new LazyModeV2();    // 只在第一次的时候执行
        }
         return instance;
    }
    private LazyModeV2(){};
}

image.gif

但是显而易见,如果简单粗暴的加锁,只在第一次初始化时为保证线程安全使用一次,在后续getInstance 时也要进行加锁解锁操作,降低性能。

c. 饿汉模式-多线程改进版

    1. 使用双重 if 判定, 降低锁竞争的频率
    2. 给 instance 加上了 volatile
    class LazyModeV3 {
        // volatile
        private volatile static LazyModeV3 instance = null;
        public static LazyModeV3 getInstance(){
            // 1. 第一次调用这个方法时,说明我们应该实例化对象了
            if (instance == null) {
                // 在第一次instance 没有初始化的时候
                // 没有锁保护,有多个线程可以走到这里 a, b, c, d
                // 2. **但是只有第一个线程a能加锁,a 加锁后并且实例化对象,
                //    **b, c, d 加锁进去后发现instance != null, 就不会再创建了
                synchronized (LazyModeV3.class) {
                    // 3. 加锁之后才能执行
                    // 第一个抢到锁的线程看instance 是 null
                    // 其他第一个抢到锁的线程看instance 是 null
                    // 保证instance 只实例化一次
                    if (instance == null) {
                        instance = new LazyModeV3();    // 只在第一次的时候执行
                        // 4. 但是还可能出问题,出现重排序,变成 1 -> 3 -> 2 其他线程掉instance就出现问题,
                        // 所以定义时就加上volatile,防止重排序;
                    }
                }
            }
            return instance;
        }
        private LazyModeV3(){};
    }

    image.gif


    🚀2 阻塞队列(blocking queue)

    2.1 阻塞队列

    阻塞队列是一种特殊的队列也遵守 "先进先出" 的原则

    阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

      • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素
      • 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素

      阻塞队列的一个典型应用场景就是 "生产者消费者模型".

      2.2 生产者消费者模型:

      生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

      生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取

        1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
        2. 阻塞队列也能使生产者和消费者之间 解耦

        2.3 标准库中的阻塞队列

        Java 标准库,JUC包下的blocking queue,是Queue 的子接口

        image.gif编辑

          • BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue(无上限)、ArrayBlockingQueue(有上限)
          • put 方法用于阻塞式的入队列, take 用于阻塞式的出队列
          • BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性
          • 都会抛出lnterruptedException 异常,可以被中断
          public class Main0 {
              public static void main(String[] args) throws InterruptedException {
                  BlockingQueue b1 = new LinkedBlockingDeque();
                  BlockingQueue<Integer> b2 = new ArrayBlockingQueue<>(3);
                  b2.put(1);
                  b2.put(2);
                  b2.put(3);
                  b2.put(4); // 插入第四个时就会阻塞
              }
          }

          image.gif

          2.4 实现阻塞队列

            • 通过 "循环队列" 的方式来实现.
            • 使用 synchronized 进行加锁控制.
            • put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定 队列就不满了, 因为同时可能是唤醒了多个线程).
            • take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
            public class MyArrayBlockingQueue {
                private long[] array;
                private int frontIndex;
                private int rearIndex;
                private int size;
                public MyArrayBlockingQueue (int capacity){
                    array = new long[capacity];
                    frontIndex = 0;
                    rearIndex = 0;
                    size = 0;
                }
                public synchronized void put (long val) throws InterruptedException {
                    // while 防止假唤醒
                    while(size == array.length){
                        this.wait();
                    }
                    // 预期:队列一定不是满的
                    array[rearIndex] = val;
                    rearIndex++;
                    if(rearIndex == array.length){
                        rearIndex = 0;
                    }
                    // notify();
                    // 在多生产者,多消费者时用notifyAll()
                    notifyAll();
                }
                public synchronized long take () throws InterruptedException {
                    while(size == 0){
                        wait();
                    }
                    long val = array[frontIndex];
                    frontIndex++;
                    if(frontIndex == array.length){
                        frontIndex = 0;
                    }
                    // notify();
                    // 在多生产者,多消费者时用notifyAll()
                    notifyAll();
                    return val;
                }
            }

            image.gif


            🚀3. 定时器

            定时器一种实际开发中非常常用的组件,类似于一个“闹钟”,达到特定时间执行某个特定的代码。

            3.1 标准库中的定时器

              • 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule
              • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执 行 (单位为毫秒)
              public class UserTimer {
                  public static void main(String[] args) {
                      Timer timer = new Timer();
                      TimerTask task = new TimerTask() {
                          @Override
                          public void run() {
                              System.out.println("闹钟响了");
                          }
                      };
                      //timer.schedule(task, 5000); // 5秒后执行任务
                      timer.schedule(task, 2000, 3000); // 2秒后执行任务,并且之后每三秒执行一次
                      while (true){} // 主线程死循环,所以之后的输出都不是主线程打印的
                  }
              }

              image.gif

              3.2 实现定时器

                • 一个带优先级的阻塞队列
                • 队列中的每个元素是一个 Task 对象.
                • Task 中带有一个时间属性, 队首元素就是即将执行的Task
                • 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行

                为啥要带优先级呢?

                因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带 优先级的队列就可以高效的把这个 delay 最小的任务找出来.  

                import java.util.concurrent.PriorityBlockingQueue;
                // 定义一个工作抽象类
                abstract class MyTimerTask implements Comparable<MyTimerTask> {
                    long runAt;     // 这个任务应该在何时运行(记录为 ms 为单位的时间戳)
                    abstract public void run();
                    @Override
                    public int compareTo(MyTimerTask o) {
                        if (runAt < o.runAt) {
                            return -1;
                        } else if (runAt > o.runAt) {
                            return 1;
                        } else {
                            return 0;
                        }
                    }
                }
                // 定时器
                public class MyTimer {
                    // 这里是普通属性,不是静态属性
                    // 优先级队列,要求元素具备比较能力
                    private final PriorityBlockingQueue<MyTimerTask> queue = new PriorityBlockingQueue<>();
                    private final Object newTaskComing = new Object();
                    public MyTimer() {
                        Worker worker = new Worker();
                        worker.start();
                    }
                    // 不能使用静态内部类,否则看不到外部类的属性
                    class Worker extends Thread {
                        @Override
                        public void run() {
                            while (true) {
                                MyTimerTask task = null;
                                try {
                                    task = queue.take();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                // task 应该有个应该执行的时刻(不能记录 delay)
                                long now = System.currentTimeMillis();
                                long delay = task.runAt - now;
                                if (delay <= 0) {
                                    task.run();
                                } else {
                                    try {
                                        // Thread.sleep(delay);    // 5s
                                        // 应该在两种条件下醒来:
                                        // 1. 有新的任务过来了(任务可能比当前最小的任务更靠前)
                                        // 2. 没有新任务来,但到了该执行该任务的时候了
                                        synchronized (newTaskComing) {
                                            newTaskComing.wait(delay); // 最多等待delay秒
                                        }
                                        // 如果当前时间已经在要执行任务的时间之后了
                                        // 说明任务的执行时间已过,所以应该去执行任务了
                                        // 否则,先把这个任务放回去(因为时间还没到),再去取最小的任务
                                        if (System.currentTimeMillis() >= task.runAt) {
                                            task.run();
                                        } else {
                                            queue.put(task);
                                        }
                                    } catch (InterruptedException e) {
                                        e.printStackTrace();
                                    }
                                }
                            }
                        }
                    }
                    public void schedule(MyTimerTask task, long delay) {
                        // 该方法非工作线程(主线程)调用
                        task.runAt = System.currentTimeMillis() + delay;
                        queue.put(task);
                        synchronized (newTaskComing) {
                            newTaskComing.notify();
                        }
                    }
                }

                image.gif


                🚀4 线程池

                因为创建线程 / 销毁线程 的开销较大,使用线程池就是减少每次启动、销毁线程的损耗

                4.1 标准库中的线程池

                Executor -> ExecutorService -> ThreadPoolExcutor() 实现类

                image.gif编辑

                  • corePoolSize: 正式员工的名额上限
                  • maximumPoolSize: 正式+临时的名额上限
                  • keepAliveTime + unit: 临时工允许空闲时间的上限
                  • workQueue: 任务队列
                  • handler: 拒绝(默认)、调用者允许、丢弃最老的、丢弃当前
                  import java.util.Scanner;
                  import java.util.concurrent.*;
                  public class Demo {
                      public static void main(String[] args) {
                          BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1);
                          ExecutorService service = new ThreadPoolExecutor(
                                  3, // 正式员工 10
                                  9, // 临时员工 20
                                  10, TimeUnit.SECONDS,
                                  queue,  // 阻塞队列
                                  new ThreadFactory() {
                                      @Override
                                      public Thread newThread(Runnable r) {
                                          Thread t = new Thread(r, "饭店厨师");
                                          return t;
                                      }
                                  }, // 线程工厂
                                  new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
                          );
                          // 定义任务
                          Runnable task = new Runnable() {
                              @Override
                              public void run() {
                                  try {
                                      TimeUnit.DAYS.sleep(365);
                                  } catch (InterruptedException e) {
                                      e.printStackTrace();
                                  }
                              }
                          };
                          // 把任务提交给线程池对象(公司)
                          Scanner s = new Scanner(System.in);
                          for (int i = 1; i < 100; i++) {
                              s.nextLine();
                              service.execute(task);
                              System.out.println(i);
                          }
                      }
                  }

                  image.gif

                  Executor 是接口

                  Executor 定义了一些固定策略的线程池

                  4.2 Executors 创建线程池的几种方式

                    • newFixedThreadPool: 创建固定线程数的线程池(只有正式员工)
                    • newCachedThreadPool: 创建线程数目动态增长的线程池(只有临时员工)
                    • newSingleThreadExecutor: 创建只包含单个线程的线程池(只有一个正式员工)
                    • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer
                    public class Demo2 {
                        public static void main(String[] args) {
                            // 不太建议在实际生产项目下使用
                            ExecutorService service = Executors.newFixedThreadPool(10);
                            ExecutorService service1 = Executors.newSingleThreadExecutor();
                            ExecutorService service2 = Executors.newCachedThreadPool();
                            Runnable task = new Runnable() {
                                @Override
                                public void run() {
                                }
                            };
                            service.execute(task);
                        }
                    }

                    image.gif

                    4.3 利用线程池 创建多线程计算fib 数

                    import java.util.Scanner;
                    import java.util.concurrent.ExecutorService;
                    import java.util.concurrent.Executors;
                    public class Demo3 {
                        static class CalcFib implements Runnable {
                            private final int n;
                            CalcFib(int n) {
                                this.n = n;
                            }
                            @Override
                            public void run() {
                                long r = fib(n);
                                System.out.printf("fib(%d) = %d\n", n, r);
                            }
                            private long fib(int n) {
                                if (n == 0 || n == 1) {
                                    return 1;
                                }
                                return fib(n - 1) + fib(n - 2);
                            }
                        }
                        public static void main(String[] args) {
                            Scanner scanner = new Scanner(System.in);
                            ExecutorService service = Executors.newFixedThreadPool(10);
                            while (true) {
                                System.out.print("提交数字: ");
                                int n = scanner.nextInt();
                                Runnable task = new CalcFib(n);
                                service.execute(task);
                            }
                        }
                    }

                    image.gif

                    4.4 实现线程池

                    image.gif编辑

                    总结:

                    线程中线程是按需创建:

                      1. 一开始一个线程都没有︰随着任务提交,创建core线程(当前线程数<corePoolSize)
                      2. 优先提交队列,直到队列满
                      3. 创建临时工去处理 > corePoolSize的线程,直到maximumPoolSize
                      4. 执行拒绝策略
                      import java.util.concurrent.*;
                      // 线程池类
                      public class MyThreadPoolExecutor implements Executor {
                          private int currentCoreSize;      // 当前正式员工的数量
                          private final int corePoolSize;   // 正式员工的数量上限
                          private int currentTemporarySize; // 当前临时员工的数量
                          private final int temporaryPoolSize;      // 临时员工的数量上限
                          private final ThreadFactory threadFactory;// 创建线程的工厂对象
                          // 临时工摸鱼的时间上限
                          private final long keepAliveTime;
                          private final TimeUnit unit;
                          // 传递任务的阻塞队列
                          private final BlockingQueue<Runnable> workQueue;
                          public MyThreadPoolExecutor(int corePoolSize,
                                                      int maximumPoolSize,
                                                      long keepAliveTime,
                                                      TimeUnit unit,
                                                      BlockingQueue<Runnable> workQueue,
                                                      ThreadFactory threadFactory,
                                                      RejectedExecutionHandler handler) {
                              this.corePoolSize = corePoolSize;
                              this.temporaryPoolSize = maximumPoolSize - corePoolSize;
                              this.workQueue = workQueue;
                              this.threadFactory = threadFactory;
                              this.keepAliveTime = keepAliveTime;
                              this.unit = unit;
                          }
                          // 向线程池中提交任务
                          @Override
                          public void execute(Runnable command) {
                              // 1. 如果正式员工的数量还低于正式员工的数量上限,则优先创建正式员工处理任务
                              // 1.1 需要管理,当前正式员工有多少,正式员工的数量上限有多少?
                              if (currentCoreSize < corePoolSize) {
                                  // 优先创建正式员工进行处理
                                  // 创建一个线程,这个线程中的任务就是不断地取任务-做任务,但是不需要考虑退出的问题
                                  CoreJob job = new CoreJob(workQueue, command);
                      //            Thread thread = new Thread(job);    // 不使用工厂创建的线程
                                  Thread thread = threadFactory.newThread(job);   // thread 代表的就是正式员工
                                  String name = String.format("正式员工-%d", currentCoreSize);
                                  thread.setName(name);
                                  thread.start();
                                  // 只是两种不同的策略,没有谁是正确的说法
                                  // 1. 把 command 放到队列中;command 的执行次序是在队列已有的任务之后
                                  // 2. 创建正式员工的时候,就把 command 提交给正式员工,让 command 优先执行
                                  // 我们这里采用第二种方案,主要原因就是 java 官方的就是使用的第二种策略
                                  currentCoreSize++;
                                  return;
                              }
                              // 走到这里,说明正式员工的数量 == 正式员工的上限了
                              // 2. 优先把任务放入队列中,如果放入成功,execute 执行结束,否则还需要继续
                              // 2.1 需要一个阻塞队列
                              // workQueue.put(command); // 带阻塞的放入,是否满足这里的需求?
                              // 我们这里希望的是立即得到结果
                              boolean success = workQueue.offer(command);
                              if (success == true) {
                                  // 说明放入队列成功
                                  return;
                              }
                              // 队列也已经放满了
                              // 3. 继续判断,临时工的数量有没有到上限,如果没有到达,创建新的临时工来处理
                              if (currentTemporarySize < temporaryPoolSize) {
                                  // 创建临时工进行处理
                                  TemporaryJob job = new TemporaryJob(keepAliveTime, unit, workQueue, command);
                                  //Thread thread = new Thread(job);    // 不使用工厂创建的线程
                                  Thread thread = threadFactory.newThread(job);   // thread 代表的就是临时员工
                                  String name = String.format("临时员工-%d", currentTemporarySize);
                                  thread.setName(name);
                                  thread.start();
                                  currentTemporarySize++;
                                  return;
                              }
                              // 4. 执行拒绝策略
                              // 为了实现方便,暂时不考虑其他策略
                              throw new RejectedExecutionException();
                          }
                          // 一个正式员工线程要完成的工作
                          class CoreJob implements Runnable {
                              // 需要阻塞队列
                              private final BlockingQueue<Runnable> workQueue;
                              private Runnable firstCommand;
                              CoreJob(BlockingQueue<Runnable> workQueue, Runnable firstCommand) {
                                  this.workQueue = workQueue;
                                  this.firstCommand = firstCommand;
                              }
                              @Override
                              public void run() {
                                  try {
                                      firstCommand.run();     // 优先先把刚提交的任务先做掉了
                                      firstCommand = null;    // 这里设置 null 的意思是,不影响 firstCommand 对象被 GC 时的回收
                                      while (!Thread.interrupted()) {
                                          Runnable command = workQueue.take();
                                          command.run();
                                      }
                                  } catch (InterruptedException ignored) {}
                              }
                          }
                          // 一个临时员工线程要完成的工作
                          class TemporaryJob implements Runnable {
                              // 需要阻塞队列
                              private final BlockingQueue<Runnable> workQueue;
                              private final long keepAliveTime;
                              private final TimeUnit unit;
                              private Runnable firstCommand;
                              TemporaryJob(long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, Runnable firstCommand) {
                                  this.keepAliveTime = keepAliveTime;
                                  this.unit = unit;
                                  this.workQueue = workQueue;
                                  this.firstCommand = firstCommand;
                              }
                              @Override
                              public void run() {
                                  try {
                                      firstCommand.run();     // 优先先把刚提交的任务先做掉了
                                      firstCommand = null;    // 这里设置 null 的意思是,不影响 firstCommand 对象被 GC 时的回收
                                      // 一旦超过一定时间没有任务,临时工是需要退出的
                                      // 1. keepAliveTime + unit 记录起来
                                      // 2. 怎么就知道超过多久没有任务了?如果一定时间内都无法从队列中取出来任务,则认为摸鱼时间够了
                                      while (!Thread.interrupted()) {
                      //                Runnable command = workQueue.take();
                                          Runnable command = workQueue.poll(keepAliveTime, unit);
                                          if (command == null) {
                                              // 说明,没有取到任务
                                              // 说明超时时间已到
                                              // 说明该线程已经 keepAliveTime + unit 时间没有工作了
                                              // 所以,可以退出了
                                              break;
                                          }
                                          command.run();
                                      }
                                  } catch (InterruptedException ignored) {}
                              }
                          }
                      }

                      image.gif


                      分享到此,感谢大家观看!!!

                      如果你喜欢这篇文章,请点赞关注吧,或者如果你对文章有什么困惑,可以私信我。

                      🏓🏓🏓

                      相关文章
                      |
                      2天前
                      |
                      NoSQL Redis
                      单线程传奇Redis,为何引入多线程?
                      Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
                      12 1
                      |
                      1月前
                      |
                      监控 安全 Java
                      在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
                      【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
                      113 38
                      |
                      1月前
                      |
                      Java
                      .如何根据 CPU 核心数设计线程池线程数量
                      IO 密集型:核心数*2 计算密集型: 核心数+1 为什么加 1?即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费。
                      44 4
                      |
                      1月前
                      |
                      Java
                      线程池内部机制:线程的保活与回收策略
                      【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
                      87 2
                      |
                      1月前
                      |
                      Prometheus 监控 Cloud Native
                      在 Java 中,如何使用线程池监控以及动态调整线程池?
                      【10月更文挑战第22天】线程池的监控和动态调整是一项重要的任务,需要我们结合具体的应用场景和需求,选择合适的方法和策略,以确保线程池始终处于最优状态,提高系统的性能和稳定性。
                      281 2
                      |
                      1月前
                      |
                      数据采集 Java Python
                      爬取小说资源的Python实践:从单线程到多线程的效率飞跃
                      本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
                      66 0
                      |
                      2月前
                      |
                      存储 消息中间件 资源调度
                      C++ 多线程之初识多线程
                      这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
                      60 1
                      |
                      2月前
                      |
                      Java 开发者
                      在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
                      【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
                      32 3
                      |
                      2月前
                      |
                      Java 开发者
                      在Java多线程编程中,选择合适的线程创建方法至关重要
                      【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
                      25 2
                      |
                      2月前
                      |
                      Java
                      Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
                      【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
                      41 2