专栏首页Java后端技术栈Java多线程编程-(15)- 关于锁优化的几点建议

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),作者:徐刘根

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

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

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java多线程编程-(9)-使用线程池实现线程的复用和一些坑的避免

    假设这里有一个系统,大概每秒需要处理5万条数据,这5万条数据为一个批次,而这没秒发送的5万条数据数据需要经过两个处理过程,第一步是数据存入数据库,第二步是对数据...

    Java后端技术
  • Java多线程编程-(17)-读写锁ReentrantReadWriteLock深入分析

    上两篇的内容中已经介绍到了锁的实现主要有ReentrantLock和ReentrantReadWriteLock。

    Java后端技术
  • Java多线程编程-(1)-线程安全和锁Synchronized概念

    (1)在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单位都是进程。

    Java后端技术
  • 线程的基础概念与理论

    虽说线程的数量是有限的,但是我们平时在用的过程中并没有,发现线程受限制,这就涉及到CPU的时间片轮转机制了,也称为RR调度

    彼岸舞
  • 多线程基础(十八):ReentrantReadWriteLock源码分析

    ReentrantReadWriteLock是基于AQS实现的可重入的读写锁。这个锁在使用的时候将锁分为了两个部分,ReadLock和WriteLock。实际上...

    冬天里的懒猫
  • JAVA线程之ThreadLocal与栈封闭(六)

    PS:这次说了线程封闭的概念,其实很容易理解只要知道在ThreadLocal是JVM内部维护了一个Map就可以了。栈封闭没有纤细概述,跟局部变量是一个概念。

    IT故事会
  • 源码分析-使用newFixedThreadPool线程池导致的内存飙升问题

    使用无界队列的线程池会导致内存飙升吗?面试官经常会问这个问题,本文将基于源码,去分析newFixedThreadPool线程池导致的内存飙升问题,希望能加深大家...

    捡田螺的小男孩
  • 【Android】RxJava的使用(四)线程控制 —— Scheduler

    Gavin-ZYX
  • 高并发编程必备基础(上)

    借用Java并发编程实践中的话"编写正确的程序并不容易,而编写正常的并发程序就更难了",相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没...

    加多
  • 高并发编程-ReentrantLock公平锁深入解析

    ReentrantLock是一个可重入的互斥锁,它不但具有synchronized实现的同步方法和同步代码块的基本行为和语义,而且具备很强的扩展性。Reentr...

    JavaQ

扫码关注云+社区

领取腾讯云代金券