线程池学习Note

8.1 线程池

设计线程池的原因

  • 线程是一种系统资源,每次创建一个新的线程,都需要占用分配内存空间,如果高并发下, 对于每个任务,如果都开启一个线程去处理的话,对内存的占用过大,甚至爆出OOM。
  • 线程数量过多,而CPU只有几个,那必定由很多线程处于等待,而频繁发生线程上下文切换,也会导致效率问题

所以尽可能的重用已有的线程去处理任务。

阻塞队列:放任务的,线程池暂时处理不了的任务先放到阻塞队列里来;线程池能处理了,再从阻塞队列中去取任务执行

// 注意一下这个ReentrantLock的从条件变量中唤醒,之前没有正确理解
//  现在的理解是: 从ReentrantLock的条件变量唤醒之后,跟synchronized从waitSet中唤醒一样,要进入entryList中等待获取锁,
//              获取锁成功之后, 才能运行, 不然你以为, 最后的那个释放锁是怎么释放的
@Slf4j(topic = "c.BlockingQueue")
public class BlockingQueue<T> {
     

    // 任务队列
    private ArrayDeque<T> queue = new ArrayDeque<>();

    // 锁
    private ReentrantLock lock = new ReentrantLock();

    // 生产者条件变量 (任务队列满了的时候, 等待)
    Condition fullWaitSet = lock.newCondition();

    // 消费者条件变量 (任务队列为空的时候, 等待)
    Condition emptyWaitSet = lock.newCondition();

    // 任务队列最大容量
    private int capacity;

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

    // 带超时阻塞获取
    public T poll(long timeout, TimeUnit unit) {
     
        long nanos = unit.toNanos(timeout);
        lock.lock();
        try {
     
            while (queue.isEmpty()) {
     
                if (nanos <= 0) {
      // nanos小于或等于0, 就代表不需要等待了
                    return null;
                }
                try {
     
                    nanos = emptyWaitSet.awaitNanos(nanos); // 返回值是nanos减去等待的时间(即还需等待的时间)
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();  // 通知因为满了而等待的线程(因为满了而等待的线程肯定是等着往队列里面添加任务嘛)
            return t;
        } finally {
     
            lock.unlock(); // 这里才释放的锁
        }
    }

    // 阻塞获取
    public T take() {
     
        lock.lock();
        try {
     
            while (queue.isEmpty()) {
      // 当任务队列中没有任务时,就等待
                try {
     
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal(); // 唤醒在fullWaitSet中等待的随机一个线程,但这个被唤醒的线程并不是马上运行,
            return t;     // 而是要进入entryList中等待获取锁,锁被释放了,它才能去竞争锁(注意一下,这里还并未释放锁哦)
        } finally {
            //     下面的这个put方法在lock.lock()这句,在take方法里获取锁但未释放前,外面线程进不来,
            lock.unlock();//     里面的线程又在entryList中等待获取锁,所以保证了安全
        }   // 这里才释放的锁
    }

    // 阻塞添加
    public void put(T element) {
     
        lock.lock();
        try {
     
            while (queue.size() == capacity) {
      // 当任务队列满时, 就等待(此时,因为不能添加任务了嘛)
                try {
     
                    log.debug("任务队列已满,等待加入任务队列: {}",element);
                    fullWaitSet.await();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列: {}",element);
            queue.addLast(element);
            emptyWaitSet.signal();
        } finally {
     
            lock.unlock();
        }


    }
}

线程池

@Slf4j(topic = "c.ThreadPool")
public class ThreadPool {
     

    // 任务队列
    private BlockingQueue<Runnable> taskQueue;

    // 工作线程集合
    HashSet<Worker> workers = new HashSet<>();

    // 核心线程数
    private int coreSize;

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

    // 超时时间的时间单位
    private TimeUnit timeUnit;

    // 执行任务:
    //     如果当前的线程数还没有超过核心线程数,那么就创建一个线程去执行这个提交的任务
    //     如果当前的线程数已经超过了核心线程数,那么就把这个任务放到BlockingQueue中
    public void execute(Runnable task) {
     
        synchronized (workers) {
       // 这里既包含对worker的读,也包含对worker的写,所以需要保护
            if (workers.size() < coreSize) {
     
                Worker worker = new Worker(task);
                log.debug("创建线程: {}, 关联任务: {}",worker,task);
                workers.add(worker);
                worker.start();
            } else {
     
                taskQueue.put(task);
            }
        }
    }

    // 构造一个自定义线程池对象
    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit,int queueCapacity) {
     
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapacity);
    }

    class Worker extends Thread{
     
        // 执行的任务
        private Runnable task;

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

        // Thread#start 方法会开启线程执行线程线程对象的run方法,所以这里重写run方法
        @Override
        public void run() {
      // 自己的任务执行完了的话,再看下任务队列里面还有没有任务,没想到里面是个坑,哈哈
            // while (task != null || (task = taskQueue.take())!=null) { // 死等
            //                    没有任务就等待,有任务就返回,感觉这个线程就是个工具人,除非时调用的是超时获取的方法
            while (task != null || (task = taskQueue.poll(timeout,timeUnit))!=null) {
       
                try {
                                                       
                    log.debug("执行任务...{}",task);
                    task.run();
                } finally {
     
                    task = null; // task执行完了的话,就置为null
                }
            }
            synchronized (workers) {
     
                workers.remove(this);
                log.debug("移除线程: {}",this);
            }
        }
    }

}

测试

@Slf4j(topic = "c.TestPool")
public class TestPool {
     
    public static void main(String[] args) {
     
        ThreadPool pool = new ThreadPool(2, 1000, TimeUnit.MILLISECONDS, 10);
        for (int i = 0; i < 5; i++) {
     
            final int j = i;
            pool.execute(()->{
     
                log.debug("{}",j);
            });
        }
    }
}

测试结果

  • 两个核心线程,它们执行添加的5个任务,有3个任务添加到了任务队列里面,两个线程处理完了自己的任务之后,再从任务队列中去取任务,然后执行。如果超过规定的时间还没取到任务,这个线程的任务就完成了,然后就把这个线程移除掉,
17:37:44.339 [main] DEBUG c.ThreadPool - 创建线程: Thread[Thread-0,5,main], 关联任务: task@1323468230
17:37:44.342 [main] DEBUG c.ThreadPool - 创建线程: Thread[Thread-1,5,main], 关联任务: task@897697267
17:37:44.342 [Thread-0] DEBUG c.ThreadPool - 执行任务...task@1323468230
17:37:44.342 [main] DEBUG c.BlockingQueue - 加入任务队列: element@38997010
17:37:44.342 [Thread-0] DEBUG c.TestPool - 0
17:37:44.342 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1942406066
17:37:44.342 [Thread-1] DEBUG c.ThreadPool - 执行任务...task@897697267
17:37:44.342 [Thread-1] DEBUG c.TestPool - 1
17:37:44.342 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1213415012
17:37:44.343 [Thread-0] DEBUG c.ThreadPool - 执行任务...task@38997010
17:37:44.343 [Thread-1] DEBUG c.ThreadPool - 执行任务...task@1942406066
17:37:44.343 [Thread-0] DEBUG c.TestPool - 2
17:37:44.343 [Thread-1] DEBUG c.TestPool - 3
17:37:44.343 [Thread-0] DEBUG c.ThreadPool - 执行任务...task@1213415012
17:37:44.343 [Thread-0] DEBUG c.TestPool - 4
17:37:45.343 [Thread-1] DEBUG c.ThreadPool - 移除线程: Thread[Thread-1,5,main]
17:37:45.343 [Thread-0] DEBUG c.ThreadPool - 移除线程: Thread[Thread-0,5,main]

但是上面存在问题:假设如果main线程提交的任务,超过了任务队列的大小,并且核心线程也处理不过来,那么main线程就被阻塞住了,测试如下。

并且我们可以看到,main线程本来要提交15个线程,但是只有2个正在被执行,10个添加到了任务队列,还剩3个没有着落,main线程就被阻塞住了。

@Slf4j(topic = "c.TestPool")
public class TestPool {
     
    public static void main(String[] args) {
     
        ThreadPool pool = new ThreadPool(2, 1000, TimeUnit.MILLISECONDS, 10);
        for (int i = 0; i < 15; i++) {
     
            final int j = i;
            pool.execute(()->{
     
                try {
     
                    Thread.sleep(1000000L);  // 模拟处理时长
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
                log.debug("{}",j);
            });
        }
    }
}

/*测试结果*/ // 程序没有结束,/*
17:43:55.790 [main] DEBUG c.ThreadPool - 创建线程: Thread[Thread-0,5,main], 关联任务: task@1323468230
17:43:55.795 [main] DEBUG c.ThreadPool - 创建线程: Thread[Thread-1,5,main], 关联任务: task@897697267
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@38997010
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1942406066
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1213415012
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1688376486
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@2114664380
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@999661724
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1793329556
17:43:55.797 [main] DEBUG c.BlockingQueue - 加入任务队列: element@445884362
17:43:55.797 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1031980531
17:43:55.797 [main] DEBUG c.BlockingQueue - 加入任务队列: element@721748895
17:43:55.797 [main] DEBUG c.BlockingQueue - 任务队列已满,等待加入任务队列: element@1642534850
17:43:55.798 [Thread-0] DEBUG c.ThreadPool - 执行任务...task@1323468230
17:43:55.799 [Thread-1] DEBUG c.ThreadPool - 执行任务...task@897697267
*/

但是上面存在问题:假设如果main线程提交的任务,超过了任务队列的大小,并且核心线程也处理不过来,那么main线程就被阻塞住了,测试如下。

并且我们可以看到,main线程本来要提交15个线程,但是只有2个正在被执行,10个添加到了任务队列,还剩3个没有着落,main线程就被阻塞住了,可以看到main线程现在控制不了这个局面了,所以线程池还应该设计成让main线程可以选择遇到这种情况下应该要怎么做,是死等?让调用者放弃任务?让调用者超时等待?让调用者抛出异常?

@Slf4j(topic = "c.TestPool")
public class TestPool {
     
    public static void main(String[] args) {
     
        ThreadPool pool = new ThreadPool(2, 1000, TimeUnit.MILLISECONDS, 10);
        for (int i = 0; i < 15; i++) {
     
            final int j = i;
            pool.execute(()->{
     
                try {
     
                    Thread.sleep(1000000L);  // 模拟处理时长
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
                log.debug("{}",j);
            });
        }
    }
}

/*测试结果*/ // 程序没有结束,/*
17:43:55.790 [main] DEBUG c.ThreadPool - 创建线程: Thread[Thread-0,5,main], 关联任务: task@1323468230
17:43:55.795 [main] DEBUG c.ThreadPool - 创建线程: Thread[Thread-1,5,main], 关联任务: task@897697267
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@38997010
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1942406066
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1213415012
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1688376486
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@2114664380
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@999661724
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1793329556
17:43:55.797 [main] DEBUG c.BlockingQueue - 加入任务队列: element@445884362
17:43:55.797 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1031980531
17:43:55.797 [main] DEBUG c.BlockingQueue - 加入任务队列: element@721748895
17:43:55.797 [main] DEBUG c.BlockingQueue - 任务队列已满,等待加入任务队列: element@1642534850
17:43:55.798 [Thread-0] DEBUG c.ThreadPool - 执行任务...task@1323468230
17:43:55.799 [Thread-1] DEBUG c.ThreadPool - 执行任务...task@897697267
*/

因此抽象出一个拒绝策略接口,把任务队列满了时的代码控制权交给main线程决定

其实跟上面比较,改动的并不多,主要是:当核心线程满了的时候的逻辑,当还有任务提交时,就把任务继续交给任务队列,而在任务队列里面添加一个逻辑:当任务队列满了的时候,就执行拒绝策略,拒绝策略回传的参数就是当前的队列this和当前提交的任务,把这两个参数交给线程池的属性拒绝策略对象的reject方法处理。

public interface RejectPolicy<T> {
     

    void reject(BlockingQueue<T> blockingQueue, T task);

}
// 注意一下这个ReentrantLock的从条件变量中唤醒,之前没有正确理解
//  现在的理解是: 从ReentrantLock的条件变量唤醒之后,跟synchronized从waitSet中唤醒一样,要进入entryList中等待获取锁,
//              获取锁成功之后, 才能运行, 不然你以为, 最后的那个释放锁是怎么释放的
@Slf4j(topic = "c.BlockingQueue")
public class BlockingQueue<T> {
     

    // 任务队列
    private ArrayDeque<T> queue = new ArrayDeque<>();

    // 锁
    private ReentrantLock lock = new ReentrantLock();

    // 生产者条件变量 (任务队列满了的时候, 等待)
    Condition fullWaitSet = lock.newCondition();

    // 消费者条件变量 (任务队列为空的时候, 等待)
    Condition emptyWaitSet = lock.newCondition();

    // 任务队列最大容量
    private int capacity;

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

    // 带超时阻塞获取
    public T poll(long timeout, TimeUnit unit) {
     
        lock.lock();
        try {
     
            long nanos = unit.toNanos(timeout);
            while (queue.isEmpty()) {
     
                if (nanos <= 0) {
      // nanos小于或等于0, 就代表不需要等待了
                    return null;
                }
                try {
     
                    nanos = emptyWaitSet.awaitNanos(nanos); // 返回值是nanos减去等待的时间(即还需等待的时间)
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();  // 通知因为满了而等待的线程(因为满了而等待的线程肯定是等着往队列里面添加任务嘛)
            return t;
        } finally {
     
            lock.unlock(); // 这里才释放的锁
        }
    }

    // 阻塞获取
    public T take() {
     
        lock.lock();
        try {
     
            while (queue.isEmpty()) {
      // 当任务队列中没有任务时,就等待
                try {
     
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal(); // 唤醒在fullWaitSet中等待的随机一个线程,但这个被唤醒的线程并不是马上运行,
            return t;     // 而是要进入entryList中等待获取锁,锁被释放了,它才能去竞争锁(注意一下,这里还并未释放锁哦)
        } finally {
            //     下面的这个put方法在lock.lock()这句,在take方法里获取锁但未释放前,外面线程进不来,
            lock.unlock();//     里面的线程又在entryList中等待获取锁,所以保证了安全
        }   // 这里才释放的锁
    }

    // 阻塞添加
    public void put(T task) {
     
        lock.lock();
        try {
     
            while (queue.size() == capacity) {
      // 当任务队列满时, 就等待(此时,因为不能添加任务了嘛)
                try {
     
                    log.debug("任务队列已满,等待加入任务队列: {}", "task@" + task.hashCode());
                    fullWaitSet.await();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列: {}", "element@" + task.hashCode());
            queue.addLast(task);
            emptyWaitSet.signal();
        } finally {
     
            lock.unlock();
        }
    }

    // 带超时阻塞添加
    public boolean offer(T task, long timeout, TimeUnit timeUnit) {
     
        lock.lock();
        try {
     
            long nanos = timeUnit.toNanos(timeout);
            while (queue.size() == capacity) {
      // 当任务队列满时, 就等待(此时,因为不能添加任务了嘛)
                try {
     
                    if (nanos <= 0) {
     
                        return false;
                    }
                    log.debug("任务队列已满,等待加入任务队列: {}", "task@" + task.hashCode());
                    nanos = fullWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列: {}", "element@" + task.hashCode());
            queue.addLast(task);
            emptyWaitSet.signal();
            return true;
        } finally {
     
            lock.unlock();
        }
    }

    // 在上面原本的put方法上, 引入拒绝策略接口
    public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
     
        lock.lock();
        try {
     
            if (queue.size() == capacity) {
      // 当任务队列满时, 就等待(此时,因为不能添加任务了嘛)
                log.debug("任务队列已满,执行拒绝策略...{}", "task@" + task.hashCode());
                rejectPolicy.reject(this, task);
            } else {
     
                log.debug("加入任务队列: {}", "element@" + task.hashCode());
                queue.addLast(task);
                emptyWaitSet.signal();
            }
        } finally {
     
            lock.unlock();
        }
    }
}
@Slf4j(topic = "c.ThreadPool")
public class ThreadPool {
     

    // 任务队列
    private BlockingQueue<Runnable> taskQueue;

    // 工作线程集合
    HashSet<Worker> workers = new HashSet<>();

    // 核心线程数
    private int coreSize;

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

    // 超时时间的时间单位
    private TimeUnit timeUnit;

    private RejectPolicy rejectPolicy;

    // 执行任务:
    //     如果当前的线程数还没有超过核心线程数,那么就创建一个线程去执行这个提交的任务
    //     如果当前的线程数已经超过了核心线程数,那么就把这个任务放到BlockingQueue中
    public void execute(Runnable task) {
     
        synchronized (workers) {
       // 这里既包含对worker的读,也包含对worker的写,所以需要保护
            if (workers.size() < coreSize) {
     
                Worker worker = new Worker(task);
                log.debug("创建线程: {}, 关联任务: {}",worker,"task@"+task.hashCode());
                workers.add(worker);
                worker.start();
            } else {
     
                // taskQueue.put(task); // 死等 (原来是死等,现在改成带有拒绝策略的做法)
                // 代码运行到这里,提交的任务已经超过了核心线程数,此时考虑到两种情况
                //    1. 提交的任务还可以放到阻塞队列中
                //    2. 阻塞队列如果也满了,需要把代码的控制权返还到调用者,让调用者决定采取什么方式?
                //                    1. 死等 2.超时等 3.放弃任务 4.抛出异常 ...
                //                    所以这里,抽象出一个接口出来RejectPolicy,让调用者在创建线程池时,传入
                //                    因此,这里的拒绝策略是当前线程池的一个属性,
                //                    而如果在提交的任务已经超过了核心线程数的情况下,把接下来的运行交给任务队列去做
                taskQueue.tryPut(rejectPolicy,task);
            }
        }
    }

    // 构造一个自定义线程池对象
    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit,
                      int queueCapacity,RejectPolicy<Runnable> rejectPolicy) {
     
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapacity);
        this.rejectPolicy = rejectPolicy;
    }

    class Worker extends Thread{
     
        // 执行的任务
        private Runnable task;

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

        // Thread#start 方法会开启线程执行线程线程对象的run方法,所以这里重写run方法
        @Override
        public void run() {
      // 自己的任务执行完了的话,再看下任务队列里面还有没有任务,没想到里面是个坑,哈哈
            // while (task != null || (task = taskQueue.take())!=null) { // 死等
            // 没有任务就等待,有任务就返回,感觉这个线程就是个工具人
            while (task != null || (task = taskQueue.poll(timeout,timeUnit))!=null) {
       
                try {
                                                       // 除非时调用的是超时获取的方法
                    log.debug("执行任务...{}","task@"+task.hashCode());
                    task.run();
                } finally {
     
                    task = null; // task执行完了的话,就置为null
                }
            }
            synchronized (workers) {
     
                workers.remove(this);
                log.debug("移除线程: {}",this);
            }
        }
    }

}
@Slf4j(topic = "c.TestPool")
public class TestPool {
     
    public static void main(String[] args) {
     
        ThreadPool pool = new ThreadPool(2, 1000, TimeUnit.MILLISECONDS, 10,
                (queue, task) -> {
     
                    // 1.死等
                    // queue.put(task);
                    // 2.超时等待
                    // boolean flag = queue.offer(task, 500, TimeUnit.MILLISECONDS);
                    // log.debug("添加任务结果: {}", flag);
                    // 3.让调用者放弃任务
                    // log.debug("什么都不干,直接放弃任务...");
                    // 4.抛出异常, 当然此时,主线程后面添加的任务都没了
                    // throw new RuntimeException("任务队列满了");
                    // 5.主线程自己做
                    // task.run();
                }
        );
        for (int i = 0; i < 15; i++) {
     
            final int j = i;
            pool.execute(() -> {
     
                try {
     
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
                log.debug("{}", j);
            });
        }
    }
}

你可能感兴趣的:(线程池学习Note)