导语
  之前的分享中主要介绍了虚拟机内部的对锁机制的优化与具体实现,在实际的开发过程中,还可以通过在应用层的合理优化,达到保证性能的目的,那么下面就学习介绍一下在应用层中如何进行锁的优化。

文章目录

  • 锁在应用层的优化思路
    • 减少持有锁的时间
    • 减小锁粒度
    • 锁分离
    • 锁粗化

锁在应用层的优化思路

减少持有锁的时间

  对于在应用层面上进行的并发控制,在锁竞争的过程中,单个线程对锁的持有时间与系统的性能有直接的关系。线程持有锁的时间过长,那么锁的竞争就越激烈。所以在开发的过程中,应该尽量减少某个锁的占用时间,从而减少线程之间互斥时间。例如

public synchronized void syncMethod(){othercode1();mutexMethod();othercode2();
}

  在syncMethod()中,假设只有mutextMethod()方法有同步的需求,而othercode1()和othercode2() 并不需要做同步操作,如果这两个方法在操作过程中占用的时间较长,就会很耗时间。此时,如果并发量太大,这种对于整个方法进行同步的方式就会导致线程等待增加。因为我们知道,如果一个线程获取到锁之后,需要在所有的任务执行完成之后才会释放锁,在这个过程中,其他线程无法获取到该锁。

  解决这种问题一个比较好的解决方案就是,只需要在又必须要同步的方法上进行同步操作,这样就能明显的减少线程持有锁的时间,达到提高吞吐量的目的。

public void syncMethod(){othercode1();synchronized(this){mutexMethod();}othercode2();
}

  在改进的代码中,只对需要同步的方法进行加锁,整体占用锁的时间比较短,因此可以有更高的并行度,这种手段在JDK的源码包中也可以很容易的找到,例如再处理正则表达的式的Pattern类。

    public Matcher matcher(CharSequence input) {if (!compiled) {synchronized(this) {if (!compiled)compile();}}Matcher m = new Matcher(this, input);return m;}

  matcher()有条件的进行申请锁,只有在表达式未编译的时候,进行局部加锁,这种处理方式可以大大提升matcher()的执行效率和可靠性。

注意:需要注意的是减少持有时间有助于减小锁冲突的可能性,进而提升系统的并发能力。

减小锁粒度

  减小锁的粒度也是一种削弱多线程锁竞争的有效手段。比较典型的实现场景就是ConcurrentHashMap类的实现。对一个普通的集合对象的多线程同步来说,最常用的方式就是堆get()和add()的进行同步。每当堆集合进行add()操作或者get()操作的时候,总是获得集合对象锁。所以实际上,在同一时间并没有两个线程同时操作集合,如果是高并发的场景中,就会影响吞吐量。

  作为JDK并发包中的重要组成部分,ConcurrentHashMap 类很好的使用到了拆分对象的方式,从而提高了ConcurrentHashMap的吞吐量。ConcurrentHashMap将整个HashMap分成了若干的段,每个段都有对应的子HashMap。

  如果需要在ConcurrentHashMap 中增加一个新的内容,并不是将整个的HashMap加锁,而是首先根据HashCode得到该内容应该被存放到哪个段中,然后对该段加锁,并完成整个的put()操作。在多线程环境中,如果多个线程同时进行put()操作,只要加入的内容不存放到同一个段内,则线程间可以做到并行操作。

  在默认的情况下,ConcurrentHashMap 拥有16个段,有一种特殊的情况就是ConcurrentHashMap同时接受16个线程同时插入,从而大大提升了吞吐量。如下图,6个线程同时对ConcurrentHashMap进行访问,线程1、2、3 分别访问1、2、3 段,由于1,2,3段都有独立的锁进行保护,因此3个线程可以同时访问ConcurrentHashMap ,线程4,5,6 也需要访问1,2,3,但必须等待前面的线程结束访问之后才能进入ConcurrentHashMap。

  减少锁粒度会引入一个新的问题,就是当系统需要获取到全局锁定的时候,消耗的资源会比较多。以ConcurrentHashMap 类为例,虽然put()方法很好地分离了锁,但是当试图访问ConcurrentHashMap 全局信息的时候,就需要同时获取到所有段的锁才能顺利实现全局加锁。李丽茹ConcurrentHashMap 的size()方法,它将返回ConcurrentHashMap的有效表项,也就是整体表项之和,这个操作就需要将全局加锁。

sum = 0;
for(int i = 0;i<segments.length;++i){segments[i].lock();
}for(int i = 0;i<segments.length;++i){sum+=segments[i].count;
}
for(int i = 0;i<segments.length;++i){segments[i].unlock();
}

  可以看到,在计算总数的时候,先要获得所有段的锁,然后再求和。但ConcurrentHashMap的size()方法并不总是这样执行的,实际上size()先使用无锁的方式求和,如果失败才会尝试加锁的方式,不管如何,在高并发场合ConcurrentHashMap的size()方法的性能依然要比HashMap要低。

注意:所谓减少锁粒度,就是值缩小锁定的对象范围,从而减小锁冲突的可能性,提高系统的并发能力。

锁分离

  锁分离是减小颗粒度的一个特例,它依据应用程序的功能特点,将一个独占锁分成多个锁,比较典型的案例就是java.util.concurrent.LinkedBlockingQueue 的实现。

  在LinkedBlockingQueue的实现中,take() 和 put()分别实现了从队列中取得数据和往队列中增加数据的功能。虽然两个函数都是对当前队列的修改操作,但是由于LinkedBlockingQueue 是基于链表的操作,而两个操作又分别出现在队列的两端,从理论上来说,两者是不冲突的。如图所示,

  如果使用独占锁,则要求在两个操作进行的时候获取当前队列的独占锁,那么take()和put()就不是真正的并发操作,在运行时,它们会彼此等待锁资源的释放。这种情况下,锁竞争会相对比较激烈,从而影响程序在高并发时的性能。

  在JDK实现中,并不是采用这种方式,取而代之的是用两把不同的锁分离了take()和put()操作。

    /** Lock held by take, poll, etc */private final ReentrantLock takeLock = new ReentrantLock();/** Wait queue for waiting takes */private final Condition notEmpty = takeLock.newCondition();/** Lock held by put, offer, etc */private final ReentrantLock putLock = new ReentrantLock();/** Wait queue for waiting puts */private final Condition notFull = putLock.newCondition();

  上面代码定义了takeLock和putLock,它们分别在take()操作和put()操作中使用。因此,take()和put()就相互独立了,它们之间并不存在锁竞争关系。只需要在take()和take()之间,put()和put()之间分别对takeLock 和putLock进行竞争,从而降低了锁竞争的可能性。

take实现

 public E take() throws InterruptedException {E x;int c = -1;final AtomicInteger count = this.count;final ReentrantLock takeLock = this.takeLock;takeLock.lockInterruptibly();  // 不能有两个线程同时获取数据try {while (count.get() == 0) { // 如果当前没有可用的数据,一直等待put()操作的通知notEmpty.await();}x = dequeue(); c = count.getAndDecrement();if (c > 1)notEmpty.signal();  //通知其他未中断的线程} finally {takeLock.unlock();}if (c == capacity)signalNotFull();return x;}

put实现

    public void put(E e) throws InterruptedException {if (e == null) throw new NullPointerException();// Note: convention in all put/take/etc is to preset local var// holding count negative to indicate failure unless set.int c = -1;Node<E> node = new Node<E>(e);final ReentrantLock putLock = this.putLock;final AtomicInteger count = this.count;putLock.lockInterruptibly();try {while (count.get() == capacity) {notFull.await();}enqueue(node);c = count.getAndIncrement();if (c + 1 < capacity)notFull.signal();} finally {putLock.unlock();}if (c == 0)signalNotEmpty();}

  通过takeLock 和putLock两把锁,LinkedBlockingQueue 实现了取数据和写数据的分离,实现了读写分离,实现了真正意义上的并发操作。

锁粗化

  一般情况下,为了保证多线程的有效并发,会要求每个线程持有锁的时间尽量短,也就是说在使用完公共资源之后,应该立即释放资源,只有这样,才能保证后续线程能尽快的获取到资源从而保证任务可以正常执行。但是一切的事务都需要有个度,如果对于同样的锁不断的请求释放,本身就是一种资源的消耗。这样反而不利于性能优化,另外说到偏向锁,它也是一种获取释放的过程。

  因此,虚拟机在遇到一连串连续的对同一锁不断的进行请求和释放的操作的时候,便会将所有的锁操作整合成对锁的一次请求,从而减少对锁请求的同步次数,这个操作叫做锁粗化。

public void demoMethod(){synchronized(lock){do sth.}//做其他不需要同步的操作,但能很快的完毕synchronized(lock){do sth}
}

  上面的操作会被整合为如下的新式

public void demoMethod(){synchronized(lock){do sth.}
}

  在软件开发过程中,开发人员应该在合理的场景中进行锁的粗化,尤其是在循环内使用锁操作的时候。

for{synchronized(lock)
}

  在上面操作优化的时候可以修改为

synchronized(lock){for()
}

 &emps;显然,第一种情况请求锁过于频繁,第二种情况只需要请求一次锁,因此后者的性能会高于前者,随着循环次数的增加,这种差异会越来越明显。

注意:性能优化就是根据运行时的真实情况对各个资源点进行权衡的过程。锁的粗化的思想和减少锁的持有时间相反,在不同的场景下效果是不同的,开发者需要根据实际的情况进行权衡,之前博客中提到的偏向锁、自旋锁作为虚拟机内部的锁优化策略,也不是绝对能提高系统性能的,对于锁的优化应该是在不同的场景,多种方案的权衡考虑。

垃圾回收算法与实现系列-锁在应用层的优化思路相关推荐

  1. 垃圾回收算法与实现系列-锁在Java虚拟机中的实现和优化

    导语   上篇分享中提到了对象头Mark Word 的基本概念之后,接下来就可以深入到虚拟机内部了.在多线程程序中,线程之间的竞争是不可避免的,并且这是一种多线程程序的常态.那么如何高效的处理多线程的 ...

  2. 垃圾回收算法与实现系列-学习GC之前的准备工作

    导语   在学习垃圾回收算法之前,首先需要了解什么是Heap.什么是Root.什么是Object.什么是Stack.什么是Pointer,这写概念都是什么,为什么要在垃圾回收算法中使用,使用这些东西有 ...

  3. 垃圾回收算法与实现系列-String在虚拟机中的实现

    导语   String 字符串一直作为各种编程语言的核心内容存在.作为动态字符的一种是实现方案,应用很广泛.每一种计算机语言对于这种数据结构都进行了特殊的优化和实现.在Java中,String作为引用 ...

  4. 垃圾回收算法与实现系列-GC 标记-清除算法

    导语   在GC 中最重要的算法就是GC标记-清除算法(Mark-Sweep GC).在很多的场景下都还是在使用这个算法来进行垃圾回收操作.就如如同它的名字一样先标记,然后清除.下面就来看看标记清除算 ...

  5. 垃圾回收算法与实现系列-Java堆内存溢出原因

    导语   内存一直是所有开发人员探索的一片天地,再JVM中,内存往往会被分为几块,了解不同的内存区域对编写出优质的代码有很大的帮助.堆内存作为JVM中比较重要的区域,有很多值得我们探索的地方.下面就来 ...

  6. 垃圾回收算法与实现系列-JVM无锁实现

    导语   为了确保多线程场景下数据安全,使用锁机制一直是一种优秀的解决方案,但是再高并发场景下,对锁的竞争可能成为性能瓶颈.为此,有出现了一种新的解决方案,被称为是非阻塞同步的方案.这种实现方式不需要 ...

  7. 垃圾回收算法与实现系列-线程安全与锁简介

    导语   锁是多线程软件开发的必要工具,它的基本作用是保护临界区资源不被多个线程同时访问进而受到破坏.如果由于多线程访问造成数据不一致,那么系统将会得到一个错误的结果.通过锁可以让多个线程排队一个一个 ...

  8. 垃圾回收算法与实现系列-Java的Class文件详解

    导语   对于JVM来说,Class文件作为虚拟机的一个重要接口.无论使用什么样的语言,进行软件开发,只要能将源码编译为Class文件,并放到正确的路径下,那么这种语言就可以被JVM所执行.可以说Cl ...

  9. bat判断文件是否存在_BAT面试必问题系列:JVM判断对象是否已死和四种垃圾回收算法总结...

    JVM系列: 面试题一:判断对象是否已死 判断对象是否已死就是找出哪些对象是已经死掉的,以后不会再用到的,就像地上有废纸.饮料瓶和百元大钞,扫地前要先判断出地上废纸和饮料瓶是垃圾,百元大钞不是垃圾.判 ...

最新文章

  1. 在github上创建新分支
  2. mybatis中getMapper是怎么通过动态代理得到dao接口的实现类并执行mapper文件sql语句的
  3. Wintellect Collection Classes for .NET now on CodePlex
  4. jmeter生成html报告修改,jmeter:测试后生成html报告
  5. tyvj1102 单词的划分
  6. UDK编辑器 49条小提示(转)
  7. zip4j实现文件压缩与解压缩 common-compress压缩与解压缩
  8. 南方cass10.1中文版
  9. 【无人机】【2017.10】无人机(UAV)在地学野外工作中建立增强型虚拟野外指南(EVFG)的作用研究
  10. VOT Toolkit工具配置和使用--Python版本
  11. hadoop启动cgroups,centos6.5+hadoop2.7.2
  12. 360cdn能挡住cc攻击_人人用得起的CDN加速服务,可防御CC攻击
  13. javascript event click/dblclick left/right区分左键、右键、双击事件,排除点击事件与拖拽事件冲突,做防抖优化
  14. oracle rebuild online,Oracle index rebuild online 与 rebuild 及 drop index 后重建
  15. LaTeX设置图片左对齐
  16. Java基础篇--继承(inherit),多态(Polymorphism)
  17. Compound学习(一) README.md
  18. 【android】EditText输入银行卡号每四位空一格
  19. android ndk r8 mac,c – 智能指针不适用于Android NDK r8
  20. Linux7安装keepalive,Centos 7下安装配置keepalive

热门文章

  1. 解决 transaction-manager Attribute transaction-manager is not allowed here
  2. splitpane如何设置竖条的宽度_如何用 CSS 画三角形和箭头
  3. linux下vim 选择文本,删除,复制,粘贴
  4. Python版——博客网站九 编写Android版本的移动App
  5. 以数据为中心的存储观
  6. Eclipse 编码区-保护色-快捷大全
  7. linux下硬盘测速工具hdparm
  8. Fedora Linux中配置JDK5或JDK6环境变量
  9. Andriod Studio两种签名机制V1和V2的区别
  10. rsync 删除大量小文件