线程的最主要目的是提高程序的运行性能,但性能的提升会导致复杂性的提升,又会导致安全性和活跃性的风险

一、对性能的思考

  • 提升性能意味着用更少的资源做更多地事情。要想通过并发来获得更好的性能,就要更有效地利用现有处理资源
  • 线程使用的额外的性能开销:线程之间的协调(例如加锁、触发信号以及内存同步等),增加的上下文切换,线程的创建和销毁,以及线程的调度等

1、性能与可伸缩性(多块vs多少)

性能通过服务时间、延迟时间、吞吐率、效率、可伸缩性以及容量等衡量

当进行性能调优时,其目的通常是用更小的代价完成相同的工作

可伸缩性指的是:当增加计算资源时(例如CPU、内存、存储容量或I/O带宽),程序的吞吐量或者处理能力响应地增加

在进行可伸缩性调优时,其目的是设法将问题的计算并行化,从而能利用更多地计算资源来完成更多的工作。

我们通常会接受每个工作单元执行更长的时间或消耗更多的计算资源,以换取应用程序在增加更多资源的情况下处理更高的负载。(多少更重要)

2、评估各种性能权衡因素

  • ”更快“的含义是什么?
  • 该方法在什么条件下运行得更快?在低负载还是高负载的情况下?大数据集还是小数据集?能否通过测试结果来验证你的答案?
  • 这些条件在运行环境中的发生频率?能否通过测试结果来验证你的答案?
  • 在其他不同条件的环境中能否使用这里的代码?
  • 在实现这种性能提升时需要付出哪些隐含地代价,例如增加开发风险或维护开销?这种权衡是否合适?

在对性能的调优时,一定要有明确的性能需求

二、Amdahl定律

Amdahl定律描述的是:在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中可并行组件与串行组件所占的比重。

Speedup <= 1 / (F + (1 - F) / N) ------- F是必须被串行执行的部分所占比例,N是机器中含有处理器的个数

  • 当N趋近无穷大时,最大的加速比趋近于1/F
  • 如下图,串行比例越高的程序到达瓶颈需要的处理器数越少,瓶颈的处理器利用率越低

在所有并发程序中都存在串行部分(例:存储结果的共享容器,从共享队列中取出任务)

1、框架中隐藏着串行部分

通过比较当增加线程时吞吐量的变化,推断出框架中串行部分所占比例

  • synchronizedLinkedList有更高的串行比例,更容易到达瓶颈,最高加速比更低
  • 到达瓶颈后小幅的下降表示增多线程时加速比的提高已经小于由于线程切换带来性能的损失

2、Amdahl定律的应用

串行执行比例 => 最大加速比 => 达到最大加速比的线程数量

三、线程引入的开销

多个线程的调度和协调过程总都需要一定的性能开销:对于为了提升性能而引入的线程来说,并行带来的性能提升必须超过并发导致的开销

1、上下文切换

  过程:保存当前运行线程的执行上下文,并将新调度进来的线程的上下文设置为当前上下文

  • 切换上下文需要一定的开销,而在线程调度过程中需要访问操作系统和JVM共享的数据结构
  • 上下文切换将导致一些缓存缺失,因而线程在首次调度运行时会更加缓慢

2、内存同步

  内存栅栏:在synchronized和volatile提供的可见性保证中可能会使用一些特殊指令

  • 内存栅栏可以刷新缓存,在内存栅栏中,大多数操作都是不能被重排序的

解决:

  • JVM优化去掉不会发生竞争的锁
  • 找出不需同步的本地栈元素
  • 锁粒度粗化,将近邻的锁合并,减少锁请求和锁释放的次数

3、阻塞——当在锁上发生竞争时,竞争失败的线程肯定会阻塞

  自旋等待——通过循环不断地尝试获取锁

  挂起——产生额外上下文开销

四、减少锁的竞争

并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。(锁的请求频率 + 每次持有该锁的时间)

1、缩小锁的范围(“快进快出”)

  目标:尽可能缩短持有锁的时间

  方法:超出共享变量,只对操作共享变量的代码加锁

  理论:根据Amdahl定律,减少了必须串行执行的部分

  注意:必要的原子操作不能分别加锁;锁粒度细化造成更多的同步开销,JVM会自动进行锁粒度粗化

2、减少锁粒度——是不同组对象持有不同的锁

锁分解:如果一个锁需要保护多个相互独立的状态变量,那么可以将这个锁分解为多个锁,并且每个锁只保护一个变量,从而提高可伸缩性,并最终降低每个锁被请求的频率。

对锁分解后每个新的细粒度锁上的访问将减少,分摊到两个锁上

对竞争适中的锁进行分解时,实际上是把这些锁转变为”非竞争“的锁,从而有效地提高性能和可伸缩性。

 1 @ThreadSafe
2 public class ServerStatusAfterSplit {
3 @GuardedBy("users") public final Set<String> users;
4 @GuardedBy("queries") public final Set<String> queries;
5
6 public ServerStatusAfterSplit() {
7 users = new HashSet<String>();
8 queries = new HashSet<String>();
9 }
10
11 public void addUser(String u) {
12 synchronized (users) {
13 users.add(u);
14 }
15 }
16
17 public void addQuery(String q) {
18 synchronized (queries) {
19 queries.add(q);
20 }
21 }

3、锁分段——对一组独立对象上的锁分解

竞争激烈的锁进行分解时,两个锁可能竞争仍很激励,性能提高不明显

例:在ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。

挑战:有些操作需要独占整个对象,即需要全部的锁,这样开销更大。但有些操作即使需要获得全部的锁,但也不需要同时获得

 1     public Object get(Object key) {
2 int hash = hash(key);
3 synchronized (locks[hash % N_LOCKS]) {
4 for (Node m = buckets[hash]; m != null; m = m.next)
5 if (m.key.equals(key))
6 return m.value;
7 }
8 return null;
9 }
10
11 public void clear() {
12 for (int i = 0; i < buckets.length; i++) {
13 synchronized (locks[i % N_LOCKS]) {
14 buckets[i] = null;
15 }
16 }
17 }

4、避免热点区域

如果程序采用锁分段或分解技术,那么一定要表现出在锁上的竞争频率高于在锁保护的数据上发生竞争的频率(例:ConcurrentHashMap和Map中的每一項)

热点域:数据上发生很高频率的竞争(例:HashMap.size())

解决:ConcurrentHashMap为每个分段都维护一个独立的size计数,并通过每个分段的锁来维护总size

5、代替独占锁的方法

  ReadWriteLock:如果多个读取操作都不会修改共享资源,那么这些读取操作可以同时访问该共享资源,但在执行写入操作时必须以独占方式来获取锁

  原子变量:降低更新“热点域”时的开销,例如竞态计数器、序列发生器、或者对链表数据结构中头节点的引用。原子变量类提供了在整数或者对象引用上的细粒度原子操作(因此可伸缩性更高),并使用了现代处理器中提供的底层并发原语(例如比较并交换)

6、监控CPU利用率

  linux命令:vmstat或mpstat

  cpu利用不充分的原因:

    • 负载不充足。
    • I/O密集。*nix可用iostat, windows用perfmon。
    • 外部限制。如数据库服务,web服务等。
    • 锁竞争。可通过jstack等查看栈信息。

如果CPU的利用率很高,并且总会有可运行的线程在等待CPU,那么当增加更多地处理器时,程序的性能可能会得到提升。

7、对对象池说不

  当线程分配新的对象时,基本上不需要在线程之间进行协调,因为对象分配器通常会使用线程本地的内存块,所以不需要在堆数据结构上进行同步。然而,如果这些线程从对象池中请求一个对象,那么就需要通过某种同步来协调对象池数据结构的访问,从而使某个线程被阻塞。

对象分配操作的开销比同步的开销更低。

五、减少上下文切换的开销

减少锁的持有时间,因为持有时间越长,就越容易发生竞争,就月容易发生阻塞。当任务在运行和阻塞这两个状态之间转换时,就相当于一次上下文切换。

java并发编程实战:第十一章----性能和可伸缩性的更多相关文章

  1. Java并发编程实战 第11章 性能与可伸缩性

    关于性能 性能的衡量标准有很多,如: 服务时间,等待时间用来衡量程序的"运行速度""多快". 吞吐量,生产量用于衡量程序的"处理能力",能够 ...

  2. Java并发编程实战---第六章:任务执行

    废话开篇 今天开始学习Java并发编程实战,很多大牛都推荐,所以为了能在并发编程的道路上留下点书本上的知识,所以也就有了这篇博文.今天主要学习的是任务执行章节,主要讲了任务执行定义.Executor. ...

  3. Java并发编程实战.笔记十一(非阻塞同步机制)

    关于非阻塞算法CAS. 比较并交换CAS:CAS包含了3个操作数---需要读写的内存位置V,进行比较的值A和拟写入的新值B.当且仅当V的值等于A时,CAS才会通过原子的方式用新值B来更新V的值,否则不 ...

  4. Java并发编程实战 第16章 Java内存模型

    什么是内存模型 JMM(Java内存模型)规定了JVM必须遵循一组最小保证,这组保证规定了对变量的写入操作在何时将对其他线程可见. JMM为程序中所有的操作定义了一个偏序关系,称为Happens-Be ...

  5. 【java并发编程实战】第一章笔记

    1.线程安全的定义 当多个线程访问某个类时,不管允许环境采用何种调度方式或者这些线程如何交替执行,这个类都能表现出正确的行为 如果一个类既不包含任何域,也不包含任何对其他类中域的引用.则它一定是无状态 ...

  6. Java并发编程实战 第8章 线程池的使用

    合理的控制线程池的大小: 下面内容来自网络.不过跟作者说的一致.不想自己敲了.留个记录. 要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析: 任务的性质:CPU密集型任务.IO ...

  7. java并发编程实战:第二章----线程安全性

    一个对象是否需要是线程安全的取决于它是否被多个线程访问. 当多个线程访问同一个可变状态量时如果没有使用正确的同步规则,就有可能出错.解决办法: 不在线程之间共享该变量 将状态变量修改为不可变的 在访问 ...

  8. Java并发编程实战 第15章 原子变量和非阻塞同步机制

    非阻塞的同步机制 简单的说,那就是又要实现同步,又不使用锁. 与基于锁的方案相比,非阻塞算法的实现要麻烦的多,但是它的可伸缩性和活跃性上拥有巨大的优势. 实现非阻塞算法的常见方法就是使用volatil ...

  9. Java并发编程实战 第10章 避免活跃性危险

    死锁 经典的死锁:哲学家进餐问题.5个哲学家 5个筷子 如果没有哲学家都占了一个筷子 互相等待筷子 陷入死锁 数据库设计系统中一般有死锁检测,通过在表示等待关系的有向图中搜索循环来实现. JVM没有死 ...

  10. Java并发编程实战 第5章 构建基础模块

    同步容器类 Vector和HashTable和Collections.synchronizedXXX 都是使用监视器模式实现的. 暂且不考虑性能问题,使用同步容器类要注意: 只能保证单个操作的同步. ...

随机推荐

  1. Bootstrap-CL:进度条

    ylbtech-Bootstrap-CL:进度条 1.返回顶部 1. Bootstrap 进度条 本章将讲解 Bootstrap 进度条.在本教程中,您将看到如何使用 Bootstrap 创建加载.重 ...

  2. TCL数组

    数组是一组使用索引对应元素的排列方式.常规数组的语法如下所示. set ArrayName(Index) value 用于创建简单数组的例子,如下所示. #!/usr/bin/tclsh set la ...

  3. Java利用ScriptEngineManager对计算公式的支持

    1.ScriptEngineManager是JDK6提出的相关方法,这方式的主要目的就是用来对脚本语言的处理.这里只是简单介绍一下对我们常用的数学公式的应用. 2.ScriptEngineManage ...

  4. 在 Laravel 5 中集成七牛云存储实现云存储功能(非上传)

    本扩展包基于https://github.com/qiniu/php-sdk开发,是七牛云储存 Laravel 5 Storage版,通过本扩展包可以在Laravel 5中集成七牛云存储功能. 1.安 ...

  5. OpenCL 第一个计算程序,两向量之和

    ▶ 一个完整的两向量加和的过程,包括查询平台.查询设备.创建山下文.创建命令队列.编译程序.创建内核.设置内核参数.执行内核.数据拷贝等. ● C 代码 #include <stdio.h> ...

  6. Pthreads 信号量,路障,条件变量

    ▶ 使用信号量来进行线程间信息传递 ● 代码 #include <stdio.h> #include <pthread.h> #include <semaphore.h& ...

  7. 如何设置mysql允许外网访问

    修改表,登录mysql数据库,切换到mysql数据库,使用sql语句查看"select host,user from user ;" console: >use mysql; ...

  8. @@ERROR和@@ROWCOUNT的用法

    1.         @ERROR 当前一个语句遇到错误,则返回错误号,否则返回0.需要注意的是@ERROR在每一条语句执行后会被立刻重置,因此应该在要验证的语句执行后检查数值或者是将它保存到局部变量 ...

  9. HDFS设计理念

    [HDFS设计理念] 1. 读取整个数据集的时间延迟比读取第一条记录的延迟更重要. 2. HDFS以高延迟为代价,要求低时间延迟数据访问的应用,不适合在HDFS上运行. 3. namenode决定了集 ...

  10. springmvc web.xml配置之 -- ContextLoaderListener

    首先回归一下web.xml的常用配置,看一个示例: <context-param> <param-name>contextConfigLocation</param-na ...