JAVA虚拟机并发编程读书笔记——第一部分 并发策略 第2章 分工原则

从顺序到并发
目标:学习如何估计要创建多少个线程、如何分解问题以及如何估算性能提升的程序

确定线程数:
当有多个任务处于就绪状态时,处理器核心需要在线程间频繁进行上下文切换,而这种切换对程序性能影响很大。
当一个任务执行IO操作时,其线程将会被阻塞,于是处理器进行上下文切换以便处理其他就绪线程。如果我们只有处理器可用核心数那么多个线程的话,则即使有待执行的任务也无法处理,因为我们已经没有更多的线程需要调度了。
线程数=CPU可用核心数/(1 - 阻塞系数) 其中阻塞系数在0和1之间。
计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系统则接近1.
确定两个关键参数:
1.处理器的核心数:Runtime.getRuntime().availabeProcessors();
2.任务的阻塞系数:a)猜测 b)性能分析工具 c)java.lang.management API来确定线程花在系统IO操作上的时间与CPU密集任务所耗时间的比值。

确定任务的数量:确定如何分解问题
事实证明,在解决问题的过程中使处理器一直保持忙碌状态比将负载均摊到每个子任务要实惠的多。从处理问题的角度来看,我们需要保证:只要还有待完成的任务,就不可能有空闲的处理器核心。将任务拆得比线程多,以使处理器一直不停工作来得更有效。

  1. IO密集型:文件、数据库和WEB服务调用
    例:为富有用户计算其资产净值
    a)确定线程数:
    获取系统可用的处理器核心数:Runtime.getRuntime().availabeProcessors(); —获取JVM可用的逻辑处理器数量
    应用程序的最少线程数应该等于可用的处理器核数。
    final int numberOfCores = Runtime.getRuntime().availableProcessors();
    final double blockingCoefficient = 0.9;
    final int poolSize = (int) (numberOfCores / (1 - blockingCoefficient));
    b)确定任务的数量
    我们首先要确定将股票总集拆分之后的子集数和计算所需的线程数。
    web服务通常能接受和处理大量并发请求(为了避免拒绝服务攻击,web服务会限制客户端发送的并发请求数),所以客户端的线程数和服务端的请求限制保持一致就可以了。
    web服务的请求大部分时间都花在等待服务器响应上了,所以阻塞系数会相当高,因此程序需要开的线程数可能是处理器核心数的若干倍。
    尽可能多的把股票总集进行拆分并将其调度给空闲线程执行。
    final List<Callable> partitions = new ArrayList<Callable>();
    //获取每只股票价格的任务逻辑放在Callable接口的匿名代码块中
    for (final String ticker : stocks.keySet()) {
    partitions.add(new Callable() {
    public Double call() throws Exception {
    return stocks.get(ticker) * YahooFinance.getPrice(ticker);
    }
    });
    }
    final ExecutorService executorPool = Executors
    .newFixedThreadPool(poolSize);
    //调度各个任务开始执行,在所有任务执行完毕之后,并返回一个Future对象集合 如果不想等待所有任务完成才拿到数据,而是每完成一个任务就返回一个结果的话,可以使用CompletionService
    final List<Future> valueOfStocks = executorPool.invokeAll(
    partitions, 10000, TimeUnit.SECONDS);
    //在主线程中修改这个可变变量,做到了隔离可变性而不是共享可变性,所以无需任何同步操作
    double netAssetValue = 0.0;
    for (final Future valueOfAStock : valueOfStocks)
    netAssetValue += valueOfAStock.get();
    executorPool.shutdown();

IO密集型程序的特性使得应用程序即使在处理核心数很少的情况下也可以实现相当好的并发度。当一个任务阻塞在IO操作上时,我们可以立即切换执行其他任务或启动其他IO操作请求。对于一个计算股票的程序来说40个总任务,20的线程能达到最好的并发效果,如果过多性能反而下降。正是由于web接口获取数据导致的延时,使得多线程并发技术在这里产生了相当不错的执行效果。

  1. 计算密集型
    例:计算在某个区间内所有素数的量
    a)确定线程数:
    获取系统可用的处理器核心数:Runtime.getRuntime().availabeProcessors(); —获取JVM可用的逻辑处理器数量
    应用程序的线程数应该等于可用的处理器核数。
    b)确定任务的数量

挂起一个非阻塞任务去执行另外一个非阻塞任务意义不大,反而会增加上下文切换的开销。
把线程数增加到比处理器核心数还多的方法是无效的。所以关键的问题是区间的划分数。
为了更高的利用率需要确保每个处理器核心都分摊到均匀的工作负载。

构建计算密集型程序的几点经验:
1.了任务的划分数应该等于处理器核心数。
2.了任务的划分数超过一定量之后,再增加子问题划分数对于性能的提升将十分有限。

线程池可以很好地进行线程生命周期和资源的管理、有效减少线程创建和销毁的开销并能快速响应调度任务的需求。

JAVA的旧线程API:
旧的API在功能上有很多缺陷。由于线程不允许重新启动,于是同时处理多个任务,我们通常需要不断开新的线程,而不是复用之前已经创建好的。
像wait()和notify()这样的函数需要要线程间同步,我们很难判断什么时候才是使用它们的时机。而且join()函数注意力都集中在处理线程消亡的逻辑上,从而忽视了对即将结束的任务的处理(任务完成并不代表线程消亡,反之亦然)。
synchronized关键字粒度太粗,没有线程获取到锁的情况下的超时逻辑,而且也不允许对互斥区域的并发读。基于synchronized的单元测试也很难。
java.util.concurrent
以前代码中使用Thread类及其方法的地方,现在都可以考虑用ExecutorService类及其相关接口来替换。
如果想要控制加锁的过程,则最好使用Lock接口及其方法。
用CyclicBarrier和CountdownLatch这样的同步工具代替wait/notify方法

Executor类被用作创建不同类型线程池的工厂类,其创建的线程池都只可以用ExecutorService接口进行管理。
固定大小的线程池允许我们配置线程池的大小,并能调度可用线程并发执行我们丢给它的任务。如果任务大于线程数,则所有任务会排队执行,且只要有可用的线程则等待队列中的任务就可以立即被执行。
带缓存的线程池会按需创建线程,并尽可能地复用已经创建好的线程。如果某个线程空闲超过1分钟,则该闲置线程将被关停。

在程序运行的情况下打开活动监视器(任务管理器)来观察处理器核心的活动。

有效的并发策略
影响性能的关键因素:线程数,每个子任务的工作负载以及完成每个子任务相对于其他子任务的耗时
我们应该尝试用通过一种简单的划分方式来实现子任务的均衡工作负载

小结:
1.我们需要将程序拆分成可以并发运行的多个子任务。
2.我们应该至少开处理器核心数那么多个线程,倘若问题的规模大到可以享受这私交我线程带来的好处的话。(如果问题规模小,线程管理和线程调度反而成为影响性能的瓶颈。)
3.对于计算密集型的应用程序,我们应该将线程数限制为与处理器的核心数相同。
4.对于IO密集型应用,阻塞时间是影响线程数量的关键因素。
5.可以用如下公式估算程序所需线程数量的关键因素
线程数=CPU可用核心数/(1-阻塞系数),其中0=<阻塞系数=<1
6.我们应该把问题分解成若干 个子任务,这样才能提高CPU的利用率并使其每个核心都有足够的活干。
7.我们必须避免共享可变状态,并用隔离可变性或共享不可变性取而代之。
8.我们应该充分利用现代线程API和线程池。

接下来,我们讨论一些解决状态问题的设计方法。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值