Java高并发 -- 线程池

Java高并发 -- 线程池

主要是学习慕课网实战视频《Java并发编程入门与高并发面试》的笔记

在使用线程池后,创建线程变成了从线程池里获得空闲线程,关闭线程变成了将线程归坏给线程池。

JDK有一套Executor框架,大概包括Executor、ExecutorService、AbstractExeccutorService、ThreadPoolExecutor、Executors等成员,位于java.util.concurrent包下。它们之间的关系如下:

Executor是顶层的接口,ExecutorService接口继承了它,AbstrctExecutorService继承了ExecutorService,ThreadPoolExecutor继承了AbstrctExecutorService。如果用<——表示继承,<--表示实现接口,它们的关系可表示如下:

Executor(接口) <—— ExecutorService(接口) <-- AbstrctExecutorService(抽象类) <—— ThreadPoolExecutor(类)

Executors是单独的一个类,可以看成是“线程池工厂”,它有很多静态方法,比如:

  • newFixedThreadPool(int nThread)
  • newSingleThreadExecutor()
  • newCachedThreadPool()
  • newSingleThreadScheduledExecutor()
  • newScheduledThreadPool(int corePoolSize)

newFixedThreadPool该方法返回一个固定线程数的线程池。当有新任务提交时,如果线程池中有空闲线程就立即执行,否则会进入任务队列中,等到有空闲线程了才能执行。

newSingleThreadExecutor,该方法返回只有一个线程的线程池,处理策略和上面一样。实际上就是上面的参数指定为1而已。

newCachedThreadPool该方法返回一个可根据实际情况调整线程数的线程池,任务提交后,如果有空闲线程可以复用,则优先复用。若线程池中的线程全部在工作,而此时有新任务,则会创建新的线程来处理任务,所有线程执行完后会将线程归还给线程池。

newScheduledThreadPool返回一个ScheduledExecutorService对象,可以有计划地执行任务,比如在某个延时之后开始执行,或者周期性地执行某个任务。可以指定线程数量。

newSingleThreadScheduledExecutor实现了和上面一样的功能,不过线程池的大小为1。

ScheduledExecutorService有三个方法可以有计划地执行任务。如:

  • schedule(Runnable command, long delay, TimeUnit unit);该方法可以在给定的延时后,执行一个任务;
  • scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);该方法以任务开始执行的时间为initialDelay,加上周期period,就是下一个任务开始执行的时间,以此类推,因此这个方法任务调度的频率是一定的;
  • scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);该方法表示每执行完一个任务,延迟delay的时间后,开始执行下一个任务,initialDelay还是表示任务开始的初始时延,上一个任务结束的时间点与下一个任务开始的时间点之差是固定的,固定为delay

即使单个任务的执行时间超过调度周期,scheduleAtFixedRate也不会让多个任务堆叠,比如任务执行需要8s,而调度周期是2s,调度第二个任务时,第一个还没执行完,因此为了避免任务堆叠,此时调度周期会变成8s;而采用scheduleWithFixedDelay,两个任务之间的实际间隔会变成10s,8s的执行+2s的delay。

Executors是线程池的工厂类,通过调用它的静态方法如

Executors.newCachedThreadPool();
Executors.newFixedThreadPool(n);

可返回一个线程池。这些静态方法统一返回一个ThreadPoolExecutor,只是参数不同而已。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {}

包括以上几个参数,其中:

  • corePoolSize:指定了线程池中线程的数量;
  • maximumPoolSize:线程池中的最大线程数量;
  • keepAliveTime:当线程池中线程数量超过corePoolSize时,多余的空闲线程的存活时间;
  • unit:上一个参数keepAliveTime的单位
  • 任务队列,被提交但还未被执行额任务
  • threadFactory:线程工厂,用于创建线程,一般用默认工厂即可。
  • handler:拒绝策略。当任务太多来不及处理的时候,采用什么方法拒绝任务。

最重要的是任务队列和拒绝策略。

任务队列主要有ArrayBlockingQueue有界队列、LinkedBlockingQueue无界队列、SynchronousQueue直接提交队列。

使用ArrayBlockingQueue,当线程池中实际线程数小于核心线程数时,直接创建线程执行任务;当大于核心线程数而小于最大线程数时,提交到任务队列中;因为这个队列是有界的,当队列满时,在不大于最大线程的前提下,创建线程执行任务;若大于最大线程数,执行拒绝策略。

使用LinkedBlockingQueue时,当线程池中实际线程数小于核心线程数时,直接创建线程执行任务;当大于核心线程数而小于最大线程数时,提交到任务队列中;因为这个队列是有无界的,所以之后提交的任务都会进入任务队列中。newFixedThreadPool就采用了无界队列,同时指定核心线程和最大线程数一样。

使用SynchronousQueue时,该队列没有容量,对提交任务的不做保存,直接增加新线程来执行任务。newCachedThreadPool使用的是直接提交队列,核心线程数是0,最大线程数是整型的最大值,keepAliveTime是60s,因此当新任务提交时,若没有空闲线程都是新增线程来执行任务,不过由于核心线程数是0,当60s就会回收空闲线程。

当实际线程数超过maxPoolSize时,该采取什么样的策略?

  • AbortPolicy:丢弃任务并抛出异常;
  • CallerRunPolicy:该任务被线程池拒绝,由调用execute方法的线程执行该任务;
  • DiscardOldestPolicy:丢弃最老的一个,也就是马上要执行的一个任务;
  • DiscardPolicy:默默丢弃被拒绝的任务,体现在代码中就是什么也不做。

下面看看CallerRunPolicy怎么拒绝的

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }

DiscardOldestPolicy是这样做的

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll(); // 最老的一个请求在队列头部
            e.execute(r);
        }
    }

实现一个简单的线程池

实现一个类似于Executors.newFixedThreadPool(n)的固定大小线程池,当小于corePoolSize时候,优先创建线程去执行该任务;当超过该值时,将任务提交到任务队列中,然后各个线程从任务队列中取任务来执行。

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

public class MyThreadPool {
    private int workerCount;
    private int corePoolSize;
    private BlockingQueue<Runnable> workQueue;
    private Set<Worker> workers;
    private volatile boolean RUNNING = true;
    public MyThreadPool(int corePoolSize) {
        this.corePoolSize = corePoolSize;
        workQueue = new LinkedBlockingQueue<>();
        workers = new HashSet<>();
    }

    public void execute(Runnable r) {
        if (workerCount < corePoolSize) {
            addWorker(r);
        } else {
            try {
                workQueue.put(r);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void addWorker(Runnable r) {
        workerCount++;
        Worker worker = new Worker(r);
        Thread t = worker.thread;
        workers.add(worker);
        t.start();
    }

    class Worker implements Runnable {
        Runnable task;
        Thread thread;

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

        @Override
        public void run() {
            while (RUNNING) {
                Runnable task = this.task;
                // 执行当前的任务,所以把这个任务置空,以免造成死循环
                this.task = null;
                if (task != null || (task = getTask()) != null) {
                    task.run();
                }
            }
        }
    }

    private Runnable getTask() {
        Runnable r = null;
        try {
            r = workQueue.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return r;
    }


    public static void main(String[] args) {
        MyThreadPool threadPool = new MyThreadPool(5);
        Runnable r = new Writer();
        for (int i = 0; i < 10; i++) {
            threadPool.execute(r);
        }
    }


}

class Writer implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " ");
    }
}

Worker实现了Runnale,是真正执行任务的类。当线程池中工作线程小于核心线程时候,调用addWorker直接start线程执行它的第一个任务。否则,将任务放入任务队列中,等线程来执行它们。Worker中的run方法是一个死循环,执行第一个任务(addWorker时调用start方法执行的那个任务),或者通过getTask方法不断从任务队列中取得任务来执行。正是getTask方法实现了线程的复用,即一个线程虽然只能调用一次start方法,但是后续的任务可以在Worker的run方法里直接调用任务的run方法得以执行。简单来说就是在Worker的run里调用任务的run方法。

任务全部执行完毕后,线程池需要被关闭,否则程序一直死循环。上述代码中并没有实现shutdown()方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容