并发性能优化 – 降低锁粒度

原文链接  作者:Adrianos Dadis 译者:买蓉(sky.mairong@gmail.com) 校对:方腾飞

在高负载多线程应用中性能是非常重要的。为了达到更好的性能,开发者必须意识到并发的重要性。当我们需要使用并发时, 常常有一个资源必须被两个或多个线程共享。

在这种情况下,就存在一个竞争条件,也就是其中一个线程可以得到锁(锁与特定资源绑定),其他想要得到锁的线程会被阻塞。这个同步机制的实现是有代价的,为了向你提供一个好用的同步模型,JVM和操作系统都要消耗资源。有三个最重要的因素使并发的实现会消耗大量资源,它们是:

  • 上下文切换
  • 内存同步
  • 阻塞

为了写出针对同步的优化代码,你必须认识到这三个因素以及如何减少它们。在写这样的代码时你需要注意很多东西。在本文中,我会向你介绍一种通过降低锁粒度的技术来减少这些因素。

让我们从一个基本原则开始:不要长时间持有不必要的锁。

在获得锁之前做完所有需要做的事,只把锁用在需要同步的资源上,用完之后立即释放它。我们来看一个简单的例子:

public class HelloSync {
    private Map dictionary = new HashMap();
    public synchronized void borringDeveloper(String key, String value) {
        long startTime = (new java.util.Date()).getTime();
        value = value + "_"+startTime;
        dictionary.put(key, value);
        System.out.println("I did this in "+
     ((new java.util.Date()).getTime() - startTime)+" miliseconds");
    }
}

在这个例子中,我们违反了基本原则,因为我们创建了两个Date对象,调用了System.out.println(),还做了很多次String连接操作,但唯一需要做同步的操作是“dictionary.put(key, value);”。让我们来修改代码,把同步方法变成只包含这句的同步块,得到下面更优化的代码:

public class HelloSync {
    private Map dictionary = new HashMap();
    public void borringDeveloper(String key, String value) {
        long startTime = (new java.util.Date()).getTime();
        value = value + "_"+startTime;
        synchronized (dictionary) {
            dictionary.put(key, value);
        }
        System.out.println("I did this in "+
 ((new java.util.Date()).getTime() - startTime)+" miliseconds");
    }
}

上面的代码可以进一步优化,但这里只想传达出这种想法。如果你对如何进一步优化感兴趣,请参考java.util.concurrent.ConcurrentHashMap.

那么,我们怎么降低锁粒度呢?简单来说,就是通过尽可能少的请求锁。基本的想法是,分别用不同的锁来保护同一个类中多个独立的状态变量,而不是对整个类域只使用一个锁。我们来看下面这个我在很多应用中见到过的简单例子:

public class Grocery {
    private final ArrayList fruits = new ArrayList();
    private final ArrayList vegetables = new ArrayList();
    public synchronized void addFruit(int index, String fruit) {
        fruits.add(index, fruit);
    }
    public synchronized void removeFruit(int index) {
        fruits.remove(index);
    }
    public synchronized void addVegetable(int index, String vegetable) {
        vegetables.add(index, vegetable);
    }
    public synchronized void removeVegetable(int index) {
        vegetables.remove(index);
    }
}

杂货店主可以对他的杂货铺中的蔬菜和水果进行添加/删除操作。上面对杂货铺的实现,通过基本的Grocery 锁来保护fruits和vegetables,因为同步是在方法域完成的。事实上,我们可以不使用这个大范围的锁,而是针对每个资源(fruits和vegetables)分别使用一个锁。来看一下改进后的代码:

public class Grocery {
    private final ArrayList fruits = new ArrayList();
    private final ArrayList vegetables = new ArrayList();
    public void addFruit(int index, String fruit) {
        synchronized(fruits) fruits.add(index, fruit);
    }
    public void removeFruit(int index) {
        synchronized(fruits) {fruits.remove(index);}
    }
    public void addVegetable(int index, String vegetable) {
        synchronized(vegetables) vegetables.add(index, vegetable);
    }
    public void removeVegetable(int index) {
        synchronized(vegetables) vegetables.remove(index);
    }
}

在使用了两个锁后(把锁分离),我们会发现比起之前用一个整体锁,锁阻塞的情况更少了。当我们把这个技术用在有中度锁争抢的锁上时,优化提升会更明显。如果把该方法应用到轻微锁争抢的锁上,改进虽然比较小,但还是有效果的。但是如果把它用在有重度锁争抢的锁上时,你必须认识到结果并非总是更好。

请有选择性的使用这个技术。如果你怀疑一个锁是重度争抢锁请按下面的方法来确认是否使用上面的技术:

  • 确认你的产品会有多少争抢度,将这个争抢度乘以三倍或五倍(甚至10倍,如果你想准备的万无一失)
  • 基于这个争抢度做适当的测试
  • 比较两种方案的测试结果,然后挑选出最合适的

用于改进同步性能的技术还有很多,但对所有的技术来说最基本的原则只有一个:不要长时间持有不必要的锁。

这条基本原则可以如我之前向你们解释的那样理解成“尽可能少的请求锁”,也可以有其他解释(实现方法),我将在之后的文章中进一步介绍。

两个最重要的建议:

  • 请了解一下java.util.concurrent包里的类(及其子包),因为其中有很多聪明而且有用的实现
  • 并发代码大多数都可以通过使用好的设计模式来简化。请将Enterprise Integration Patterns熟记于心,它们可以让你不必熬夜。
时间: 2024-11-05 14:37:58

并发性能优化 – 降低锁粒度的相关文章

java降低锁粒度优化并发性能教程

在高负载多线程应用中性能是非常重要的.为了达到更好的性能,开发者必须意识到并发的重要性.当我们需要使用并发时, 常常有一个资源必须被两个或多个线程共享. 在这种情况下,就存在一个竞争条件,也就是其中一个线程可以得到锁(锁与特定资源绑定),其他想要得到锁的线程会被阻塞.这个同步机制的实现是有代价的,为了向你提供一个好用的同步模型,JVM和操作系统都要消耗资源.有三个最重要的因素使并发的实现会消耗大量资源,它们是:     上下文切换     内存同步     阻塞 为了写出针对同步的优化代码,你必

Nginx服务器配置性能优化方案_nginx

高层的配置nginx.conf文件中,Nginx中有少数的几个高级配置在模块部分之上. user www-data; pid /var/run/nginx.pid; worker_processes auto; worker_rlimit_nofile 100000; user和pid应该按默认设置 - 我们不会更改这些内容,因为更改与否没有什么不同. worker_processes 定义了nginx对外提供web服务时的worker进程数.最优值取决于许多因素,包括(但不限于)CPU核的数量

MySQL 性能优化:性能提升 50%,延迟降低 60%

当我进入 Pinterest 时,我的头三个星期是在本部度过的,在那里最新工程把解决生产问题的成果应用到了整个软件栈中.在本部,我们通过构建 Pinterest 来学习 Pinterest 是怎样被构建的,并且,仅仅在几天里就提交代码.做出有意义的贡献也不是不常见.在 Pinterest ,新进来的工程师可以灵活地选择参加哪个组,而且作为在本部工作经历的一部分,编写不同部分的代码可以有助于做出这个选择.本部的人通常会做不同的项目,而我的项目则是深入研究 MySQL 的性能优化. Pinteres

秋色园QBlog技术原理解析:性能优化篇:access的并发极限及超级分库分散并发方案(十六)

上节回顾:   上节 秋色园QBlog技术原理解析:性能优化篇:数据库文章表分表及分库减压方案(十五) 中, 介绍了 秋色园QBlog 在性能优化方面,从技术的优化手段,开始步入数据库设计优化,并从数据的使用情况上进行了分析,从而将文章内容进行分离,得到新的分表,由于内容比较大,进而分了库,达到一种基础减压.   本节内容:   本节将介绍秋色园 QBlog 的Super分库方案,以及何以如此Super分库的原因.   描述说明:   在进行上了上节的分库方案后,虽然感觉一度秋色园QBlog的访

秋色园QBlog技术原理解析:性能优化篇:字节、缓存、并发(十二)

文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文件的作用 2: 秋色园QBlog技术原理解析:认识整站处理流程(二) --介绍秋色园业务处理流程 3: 秋色园QBlog技术原理解析:UrlRewrite之无后缀URL原理(三) --介绍如何实现无后缀URL 4: 秋色园QBlog技术原理解析:UrlRewrite之URL重定向体系(四) --介绍URL如何定位到处理程序 5: 秋色园QBlog技术原理解析:Module之页面基类设计(五) --介绍创建

【HIBERNATE框架开发之九】HIBERNATE 性能优化笔记!(遍历、一级/二级/查询/缓存、乐观悲观锁等优化算法)

本站文章均为 李华明Himi 原创,转载务必在明显处注明:  转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/hibernate/825.html 1.   循环分页或者循环进行部分读取处理数据的时候,使用 session.clear() ;   2.    对应1+N(N+1)问题使用如下解决方式: 1): 使用createCriteria进行查询(join fetch) 2):HQL -> join fetch 3): 使用@fetch设置LAZ

ASP.NET性能优化小结(ASP.NET&C#)

ASP.NET: 一.返回多个数据集 检查你的访问数据库的代码,看是否存在着要返回多次的请求.每次往返降低了你的应用程序的每秒能够响应请求的次数.通过在单个数据库请求中返回多个结果集,可以减少与数据库通信的时间,使你的系统具有扩展性,也可以减少数据库服务器响应请求的工作量. 如果用动态的SQL语句来返回多个数据集,那用存储过程来替代动态的SQL语句会更好些.是否把业务逻辑写到存储过程中,这个有点争议.但是我认为,把业务逻辑写到存储过程里面可以限制返回结果集的大小,减小网络数据的流量,在逻辑层也不

《Oracle数据库性能优化方法论和最佳实践》——2.6 流程、资源和组件优化方法论

2.6 流程.资源和组件优化方法论 流程.资源和组件优化方法论是本书几位作者综合多年性能优化方法论实践提出的最新的Oracle业务系统性能优化方法论.流程.资源和组件优化方法论以流程响应分析为核心,辅助以流程处理的组件和涉及的资源分析,发现导致性能问题的根本原因,并采取适当的手段进行性能改善.本书从流程.资源和组件优化方法论出发,全面构建性能优化的可测量体系,并通过大量的性能优化实践案例来验证方法论的有效性.2.6.1 吞吐量和响应时间关系曲线 吞吐量和响应时间关系曲线是流程.资源和组件优化方法

性能优化之Java(Android)代码优化

1.降低执行时间 这部分包括:缓存.数据存储优化.算法优化.JNI.逻辑优化.需求优化几种优化方式.(1). 缓存 缓存主要包括对象缓存.IO缓存.网络缓存.DB缓存,对象缓存能减少内存的分配,IO缓存减少磁盘的读写次数,网络缓存减少网络传输,DB缓存较少Database的访问次数. 在内存.文件.数据库.网络的读写速度中,内存都是最优的,且速度数量级差别,所以尽量将需要频繁访问或访问一次消耗较大的数据存储在缓存中. Android中常使用缓存: a. 线程池 b. Android图片缓存,An