Java并发编程(二)- 分工

目录

分工问题

线程池

Java线程池的基本用法

线程池添加线程的逻辑

线程池的重要参数:工作队列

线程池的重要参数:拒绝策略

创建线程池的快捷方法

线程数量

线程池使用原则

获取线程执行结果 - Future

终止线程池

CompletableFuture - 多线程异步编程

CompletionService - 多线程批量执行异步任务

Fork/Join - 多线程分治任务


分工问题

        分工问题指如何高效地拆解任务,并分配给线程;

        解决分工问题的方案,主要是靠多线程和相关操作来解决;

        多线程的并行、聚合、批量,分治操作;

        常规的并行任务,可以通过线程池+Future的方案来解决;

        任务之间有聚合关系,AND、OR聚合,可以通过 CompletableFuture来解决;

        任务批量并行,可以通过 CompletionService 来解决;

        分治操作,使用Fork/Join。

                

线程池

        Java中为了更好的管理多线程,一般使用线程池;

        线程池是一种生产者 - 消费者模式;

        线程池的使用方是生产者,线程池本身是消费者;

        线程池能避免线程频繁创建、销毁的问题,而且能够限制线程的最大数量。

Java线程池的基本用法

public ThreadPoolExecutor(
    int corePoolSize, //表示线程池保有的最小线程数
    int maximumPoolSize, //表示线程池创建的最大线程数。
    long keepAliveTime, //如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue, //工作队列
    ThreadFactory threadFactory, //通过这个参数你可以自定义如何创建线程,例如可以给线程指定一个有意义的名字。
    RejectedExecutionHandler handler) //通过这个参数可以自定义任务的拒绝策略

线程池添加线程的逻辑

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) { //1,线程数比corePoolSize数量少,创建新线程执行任务;
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) { //2,线程数达到corePoolSize,把任务缓存到队列中;
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false)) //3,任务队列已满,线程数还没达到maximumPoolSize数量,创建新建仓执行任务,否则执行拒绝策略;
        reject(command);
}

        当有新任务到来时,线程池添加线程的逻辑:

        1,线程数比corePoolSize数量少,创建新线程执行任务;

        2,线程数达到corePoolSize,把任务缓存到队列中;

        3,任务队列已满,线程数还没达到maximumPoolSize数量,创建新建仓执行任务,否则执行拒绝策略;

线程池的重要参数:工作队列

        BlockingQueue 是双缓冲队列。BlockingQueue 允许两个线程同时向队列一个存储,一个取

出操作。在保证并发安全的同时,提高了队列的存取效率。

        1,ArrayBlockingQueue:规定大小的 BlockingQueue,其构造必须指定大小。其所含的对象

是 FIFO 顺序排序的。

        2,LinkedBlockingQueue:大小不固定的 BlockingQueue,若其构造时指定大小,生成的

BlockingQueue 有大小限制,不指定大小,其大小有 Integer.MAX_VALUE 来决定。其所含的对象

是 FIFO 顺序排序的。

        3,PriorityBlockingQueue:类似于 LinkedBlockingQueue,但是其所含对象的排序不是

FIFO,而是依据对象的自然顺序或者构造函数的 Comparator 决定。

        4,SynchronizedQueue:特殊的 BlockingQueue,对其的操作必须是放和取交替完成。

线程池的重要参数:拒绝策略

        1,ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出 RejectedExecutionException异常;

        2,ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常;

        3,ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝

的任务;

        4,ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务;

创建线程池的快捷方法

        调用Executors类的静态方法;

        1,newSingleThreadExecutor

        创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行

所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所

有任务的执行顺序按照任务的提交顺序执行。

        2,newFixedThreadPool

        创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大

小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池

会补充一个新线程。

        3,newCachedThreadPool

        创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收

部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理

任务。

        此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创

建的最大线程大小。

        4,newScheduledThreadPool

        创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。

线程数量

        如果是 CPU 密集型应用:线程的数量 =CPU 核数

        如果是 IO 密集型应用:1 +(I/O 耗时 / CPU 耗时)* CPU 核数

        I/O 耗时 / CPU 耗时可以使用2作为初始值;

        以理论值为起始点,通过压测,动态调整线程数;

线程池使用原则

        建议使用有界队列,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都

无法处理,这是致命问题。

        建议在创建线程池时,清晰地指明拒绝策略。

        建议在实际工作中给线程赋予一个业务相关的名字。

        有依赖关系的任务,需要为不同的任务创建不同的线程池,如果使用同一个线程池容易导致

线程死锁。

         

获取线程执行结果 - Future

        ThreadPoolExecutor的3个 submit()方法;

// 提交Runnable任务

Future submit(Runnable task);

// 提交Callable任务

Future submit(Callable task);

// 提交Runnable任务及结果引用

Future submit(Runnable task, T result);

        1,提交 Runnable 任务 submit(Runnable task) :这个方法的参数是一个 Runnable 接口,

Runnable 接口的 run() 方法是没有返回值的,所以 submit(Runnable task) 这个方法返回的 Future

仅可以用来断言任务已经结束了,类似于 Thread.join()。

        2,提交 Callable 任务 submit(Callable task):这个方法的参数是一个 Callable 接口,它只有

一个 call() 方法,并且这个方法是有返回值的,所以这个方法返回的 Future 对象可以通过调用其

get() 方法来获取任务的执行结果。

        3,提交 Runnable 任务和结果引用 submit(Runnable task, T result):这个方法返回的 Future

对象 f,f.get() 的返回值就是传给 submit() 方法的参数 result。result 相当于主线程和子线程之间的

桥梁,通过它主子线程可以共享数据。

        

        Future 接口的get方法:

        获得任务执行结果的 get() , get(timeout, unit);

        两个 get() 方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用 get() 方法

的线程会阻塞,直到任务执行完才会被唤醒。

         

终止线程池

        线程池提供了两个方法:shutdown()和shutdownNow()

        线程池执行 shutdown() 后,就会拒绝接收新的任务,但是会等待线程池中正在执行的任务和

已经进入阻塞队列的任务都执行完之后才最终关闭线程池。

        线程池执行 shutdownNow() 后,会拒绝接收新的任务,同时还会中断线程池中正在执行的任

务,已经进入阻塞队列的任务也被剥夺了执行的机会,不过这些被剥夺执行机会的任务会作为

shutdownNow() 方法的返回值返回。

        如果提交到线程池的任务不允许取消,那就不能使用 shutdownNow() 方法终止线程池。如果

提交到线程池的任务允许后续以补偿的方式重新执行,也是可以使用 shutdownNow() 方法终止线

程池的。

        shutdown()和shutdownNow()组合使用:

        调用shutdown()停止接受任务,等待已有任务执行完毕;

        等待一段较长时间,调用boolean awaitTermination(timeOut, unit),判断线程池中剩余任务是

否都执行完毕;如果没有执行完毕,调用shutdownNow()强制关闭;

CompletableFuture - 多线程异步编程

CompletableFuture<Void> f1 = CompletableFuture.runAsync(()->{ //步骤1 });
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(()->{return "步骤2";});
CompletableFuture<String> f3 = f1.thenCombine(f2, (f1Result, f2Result)->{return "步骤3";});

        步骤1,步骤2并发执行,都执行完成之后,执行步骤3;

        CompletableFuture 类还实现了 CompletionStage 接口,CompletionStage 接口可以描述串

行关系、AND 聚合关系、OR 聚合关系以及异常处理。

CompletionService - 多线程批量执行异步任务

        使用阻塞队列实现批量执行异步任务;

// 创建阻塞队列

BlockingQueue bq = new LinkedBlockingQueue<>();

executor.execute(()->{ return "步骤1" });

executor.execute(()->{ return "步骤2" });

executor.execute(()->{ return "步骤3" });

for (int i=0; i

Integer r = bq.take();

executor.execute(()->handle(r));

}

        CompletionService 的实现原理也是内部维护了一个阻塞队列,当任务执行结束就把任务的执

行结果加入到阻塞队列中, CompletionService 是把任务执行结果的 Future 对象加入到阻塞队列

中。

        使用CompletionService 实现批量执行异步任务;

// 创建线程池

ExecutorService executor = Executors.newFixedThreadPool(3);

// 创建CompletionService

CompletionService cs = new ExecutorCompletionService<>(executor);

cs.submit(()->{ return "步骤1" });

cs.submit(()->{ return "步骤2" });

cs.submit(()->{ return "步骤3" });

for (int i=0; i

Integer r = cs.take().get();

executor.execute(()->handle(r));

}

Fork/Join - 多线程分治任务

        分治任务模型可分为两个阶段:

        1,任务分解,将任务迭代地分解为子任务,直至子任务可以直接计算出结果;

        2,结果合并,逐层合并子任务的执行结果,直至获得最终结果。

        Fork 对应的是分治任务模型里的任务分解,Join 对应的是结果合并。

        Fork/Join 计算框架主要包含两部分,一部分是分治任务的线程池 ForkJoinPool,另一部分是

分治任务 ForkJoinTask。

        ForkJoinTask 是一个抽象类,它的方法有很多,最核心的是 fork() 方法和 join() 方法,其中

fork() 方法会异步地执行一个子任务,而 join() 方法则会阻塞当前线程来等待子任务的执行结果。

        ForkJoinTask 有两个子类——RecursiveAction 和 RecursiveTask。

        RecursiveAction 定义的 compute() 没有返回值,而 RecursiveTask 定义的 compute() 方法是

有返回值的。   

static void main(String[] args){

    //创建分治任务线程池

    ForkJoinPool fjp = new ForkJoinPool(4);

    //创建分治任务

    Fibonacci fib = new Fibonacci(30);

    //启动分治任务

    Integer result = fjp.invoke(fib);

    //输出结果

    System.out.println(result);

}

//递归任务

static class Fibonacci extends RecursiveTask{

    final int n;

    Fibonacci(int n){
    
        this.n = n;
    
    }

    protected Integer compute(){

        if (n <= 1)

            return n;

        Fibonacci f1 = new Fibonacci(n - 1);

        //创建子任务

        f1.fork();

        Fibonacci f2 = new Fibonacci(n - 2);

        //等待子任务结果,并合并结果

        return f2.compute() + f1.join();

    }

}

     

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值