Java多线程编程-(15)- 关于锁优化的几点建议

一、背景

在《 Java多线程编程-(11)-从volatile和synchronized的底层实现原理看Java虚拟机对锁优化所做的努力》 这一篇文章中,我们大致介绍了Java虚拟机对锁优化所做的努力,提到了:偏向锁、轻量级锁、重量级锁以及自旋锁。

通过上面的学习,我们应该很清楚的知道了在多线程并发情况下如何保证数据的安全性和一致性的两种主要方法:一种是加锁,另一种是使用ThreadLocal。锁是一种以时间换空间的方式,而ThreadLocal是一种以空间换时间的方式。而关于ThreadLocal的正确使用,以及不正确的使用会造成的OOM已经在前边的文章中有所学习,下边就锁的问题在进一步探讨一下。

二、为什么还要进一步探讨锁

我们知道加锁同步的时候,同一时刻只允许一个线程访问临界资源的。因此,在高并发的情况下激烈的锁竞争以及上下文切换会导致程序的性能下降,就像并不是所有东西都是最优的一样,同样对于锁来说也是有很多可以进行优化的地方。

三、有关锁优化的几点建议

1、减少锁持有的时间

首先看一段代码:

可以看出只有mutextMethod() 方法是有同步需求的,而method1()method2() 方法是不需要进行同步的,但是如果method1()、method2() 是两个耗时的方法的话,那么整个锁持有的时间就会增加,很显然是一种不合理的设计,正确的方式应该使用如下的方式:

这样的话,只在有必要的时候进行同步,这样就明显减少了线程持有锁的时间,从而提高系统的性能。

2、减小锁粒度

减小锁粒度是一种削弱多线程锁竞争的有效方法。我们知道HashMap不是线程安全的而HashTable是线程安全的,但是我们在使用HashTable的时候,无论是进行读还是进行写操作都需要现获取锁,当有一个线程获取锁之后进行操作,其他的线程就必须进行阻塞等待,可见在高并发的情况下HashTable的性能会显著下降。

为了解决HashTable效率低下的问题ConcurrentHashMap出现了!

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

HashMap和ConcurrentHashMap锁的区别:

在默认的情况下,ConcurrentHashMap有16个Segment,也就是有16把锁,这样的话只要不同的线程获取不同锁锁住某一个Segment,这样的话就可以实现高并发的操作,这也是减小锁粒度 的一个典型使用。

3、使用读写锁替换独占锁

前几篇文章中,我们大致了解了可以使用ReentrantLock 实现线程的同步,ReentrantLock具有完全互斥排他的效果,即同一时间只能有一个线程在执行ReentrantLock.lock()之后的任务。

类似于我们集合中有同步类容器并发类容器,HashTable是完全排他的,即使是读也只能同步执行,而ConcurrentHashMap就可以实现同一时刻多个线程之间并发。为了提高效率,ReentrantLock的升级版ReentrantReadWriteLock就可以实现效率的提升。

ReentrantReadWriteLock有两个锁:一个是与读相关的锁,称为“共享锁”;另一个是与写相关的锁,称为“排它锁”。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。

在没有线程进行写操作时,进行读操作的多个线程都可以获取到读锁,而写操作的线程只有获取写锁后才能进行写入操作。即:多个线程可以同时进行读操作,但是同一时刻只允许一个线程进行写操作。

4、锁分离

如果将读写锁的思想进一步延伸,就是锁分离。也就是说:只要操作互不影响,锁就可以分离。最典型的一个例子就是LinkedBlockingQueue ,示意图如下:

LinkedBlockingQueue是基于链表的,take和put方法分别是向链表中取数据和写数据,他们的操作一个是从队列的头部一个是队列的尾部,从理论上说他们是不冲突的,也就是说可以锁分离的。

如果使用独占锁的话,则要求两个操作在进行时首先要获取当前队列的锁,那么takeput就不是先真正的并发了,因此,在JDK中正是实现了两种不同的锁,一个是takeLock一个是putLock

5、锁粗化

首先举个简单的例子:

根据上述的代码,如果第一次和第二次加锁和线程上下文切换的时间超过了method1()、method2()method3()、method4() 的时间,那么我们倒不如使用下边的方式:

改进后的代码的执行时间可能小于上述分别加锁的时间,这就是锁粗化,也是一种锁优化的方式,但是要根据具体的场景。

6、锁消除

锁消除是在编译器级别的事情。

在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。

也许你会觉得奇怪,既然有些对象不可能被多线程访问,那为什么要加锁呢?写代码时直接不加锁不就好了。

但是有时,这些锁并不是程序员所写的,有的是JDK实现中就有锁的,比如VectorStringBuffer这样的类,它们中的很多方法都是有锁的。当我们在一些不会有线程安全的情况下使用这些类的方法时,达到某些条件时,编译器会将锁消除来提高性能。

7、Java虚拟机对锁的优化

可参考:Java多线程编程-(13)-从volatile和synchronized的底层实现原理看Java虚拟机对锁优化所做的努力

原文发布于微信公众号 - Java后端技术(JavaITWork)

原文发表时间:2017-10-31

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java架构沉思录

优雅实现延时任务之Redis篇

PS:这篇文章昨天已经推送过了,不过忘了标原创,今天标个原创再发一次,昨天看了的可以不用往下看了。

24820
来自专栏云计算

JClouds的命令行界面

我已经使用jclouds一年多了,也一直为它的进步做贡献。目前为止,我已经在很多领域广泛地使用它,特别是在 Fuse Ecosystem 。总之,它是一个特别棒...

26570
来自专栏owent

关于BUS通信系统的一些思考(二)

虽然我很不愿意再设计一套BUS系统,但是现有的一些确实都没有特别符合我的口味的。所以还是尝试设计一个出来。

10430
来自专栏Linyb极客之路

Redis开发常用规范

虽然Redis支持持久化,但是Redis的数据存储全部都是在内存中的,成本昂贵。建议根据业务只将高频热数据存储到Redis中【QPS大于5000】,对于低频冷数...

17220
来自专栏大魏分享(微信公众号:david-share)

Java学习笔记第一篇:坦克大战游戏

一、Java学习笔记系列 笔者大学时候学的编程语言是C和汇编,毕业以后并未从事过开发工作,也没有接触过Java。但近两年的PaaS、CI/CD主要是以Java应...

78350
来自专栏Java架构沉思录

优雅实现延时任务之Redis篇

延时任务,顾名思义,就是延迟一段时间后才执行的任务。举个例子,假设我们有个发布资讯的功能,运营需要在每天早上7点准时发布资讯,但是早上7点大家都还没上班,这个时...

17330
来自专栏MixLab科技+设计实验室

设计师编程指南之Sketch插件开发 1

发现网上关于sketch插件开发的指南太少了,而且都不一定可以成功运行,于是我就写了这个系列的文章: 1 我们需要了解的语法特点 sketch 是基于 Coc...

70780
来自专栏Android开发与分享

【Android】Android开发架构规范【转】

38790
来自专栏自然语言处理

数据分析:基于Python的自定义文件格式转换系统

       无论读者现在是做数据挖掘、数据分析、自然语言处理、智能对话系统、商品推荐系统等等,都不可避免的涉及语料的问题即大数据。数据来源无非分为结构化数据、...

32250
来自专栏大内老A

[WCF-Discovery]让服务自动发送上/下线通知[原理篇]

到目前为止,我们所介绍的都是基于客户端驱动的服务发现模式,也就是说客户端主动发出请求以探测和解析可用的目标服务。在介绍WS-Discovery的时候,我们还谈到...

22660

扫码关注云+社区

领取腾讯云代金券