第十二章Java多线程程序的性能调优

Java虚拟机对内部锁的优化

锁消除
锁消除
锁消除优化能否被实施还取决于被调用的同步方法(或者带同步块的方法)是否能够被内联。
在锁消除的作用下,利用ThreadLocal将一个线程安全的对象(比如Random)作为一个线程特有对象来使用,不仅可以避免锁的争用,还可以彻底消除这些对象内部所使用的锁的开销。
锁粗化
锁粗化
锁粗化可能导致一个线程持续持有一个锁的时间变长,从而使得同步在该锁之上的其他线程在申请锁时的等待时间变长。

偏向锁
Java虚拟机会为每个对象维护一个偏好(Bias),即一个对象对应的内部锁第1次被一个线程获得,那么这个线程就会被记录为该对象的偏好线程。这个线程后续无论是再次申请该锁还是释放该锁,都无须借助原先昂贵的原子操作,从而减少了锁的申请与释放的开销。
当一个对象的偏好线程以外的其他线程申请该对象的内部锁时,Java虚拟机需要收回该对象对原偏好线程的“偏好"并重新设置该对象的偏好线程。

适应性锁
存在锁争用时我们可以将线程暂停(这种策略适合系统中绝大多数线程对该锁的持有时间较长的场景),或者采用忙等(这种策略比较适合绝大多数线程对该锁的持有时间较短的场景)
适应性锁就是能综合使用两种策略。这种优化也需要JIT编译器介入

优化对锁的使用

锁的开销与锁争用监视
上下文切换与线程调度的开销;内存同步、编译器优化受限的开销;限制可伸缩性。
减小锁的开销的一个基本思路就是消除锁的使用(使用锁的替代品)或者降低锁的争用程度。
影响锁的争用程度的因素有两个:程序申请锁的频率已经锁通常被持有的时间跨度。
使用可参数化锁
如果一个方法或者类内部锁使用的锁实例可以由该方法、类的客户端代码指定,那么我们称这个锁是可参数化的。
减小临界区的长度
减小临界区的长度可以减少锁被持有的时间从而降低锁被争用的概率,这有利于减少锁的开销。另外,减少锁的持有时间有利于Java虚拟机的适用性锁优化发挥作用。
临界区操作一般可以分为预处理操作、共享变量访问操作以及后处理操作。其中预处理操作和后处理操作往往是不涉及共享变量的访问的,因此把这两种操作挪到临界区之外可以在不导致线程安全问题的前提下减小临界区的长度。
减小锁的粒度
通过拆分锁减小锁的粒度示意图
锁分段是指对同一个数据结构内不同部分的数据使用不同锁实例进行加锁的技术。
ConcurrentHashMap执行put操作时的不同线程只要其提供的key值不一样,那么它们所需要使用的锁实例也可能是不一样的。

减少系统内耗:上下文切换

控制线程数量

public class ImplicitControlThreadCount{
    final ExecutorService executorService = Executors.newCachedThreadPool();
    final Semaphore semaphore = new Semaphore(Runtime.getRuntime().availableProcessors() * 2);

    public void doSomething(final String data) throws InterruptedException {
        semaphore.acquire();
        Runnable task = new Runnable(){
            @Override
            public void run(){
                try{
                    process(data);
                } finally {
                    semaphore.release();
                }
            }
        };

        executorService.submit(task);
    }

    private void process(String data){
        //
    }
}

**避免在临界区中执行阻塞式I/O(Blocking I/O)等阻塞操作。**临界区中的阻塞操作会增加引导这个临界区的锁被争用的可能性。而被争用的锁又可能导致上下文切换,因此在临界区中执行阻塞操作会进一步增加上下文切换。
避免在临界区中执行比较耗时的操作
减少Java虚拟机的垃圾回收
由于移动存活对象意味着这些对象所在的内存地址发生变化,因此在移动存活对象前垃圾回收器需要将所有应用线程暂停,并在移动结束后再将所有应用线程活动唤醒。

伪共享

由于一个缓存行中可以存储多个变量的副本,因此即便是在两个线程各自仅访问各自的共享变量(它们之间不存在共同的共享变量)的情况下,一个线程更新其共享变量也可能导致另外一个线程访问其共享变量时产生缓存未命中,这种现象就被称为伪共享。
Java对象内存布局
Java对象在内存中的存储包括对象头(Object Header)和实例字段。其中,对象头会使用2个字(Word)的存储空间:第一个字用于存储对象的HashCode、锁的相关信息(比如偏向锁的偏向线程的ID)等信息;第二个字用于存储对象所属的类的指针。
规则1:对象是以8字节为粒度(Granularity)进行对齐(Aligned)的。
规则2:对象中的实例字段按照如下顺序而非其源代码声明顺序排列。

  • long型变量和double型变量
  • int型变量和float型变量
  • short型变量和char型变量
  • boolean型变量和byte型变量
  • 引用型变量
    规则3: 继承自父类的实例字段不会与类本身定义的实例字段混杂在一起进行存储。
    伪共享的侦测与消除
    消除伪共享的一个方法就是填充。填充就是通过在类中添加一些“无用的”实例变量来“干扰”对象的内存布局,以使特定的实例变量(或者某个实例)能够独自占用一个缓存行的空间,从而避免这些实例变量(或者实例)与其他实例变量(或者实例)被加载到同一个缓存行之中。
    但是对缓存行宽度的依赖使得填充这种技术存在硬件层面的可移植性问题,对Java对象内存布局的依赖同样也使得填充这种技术存在软件层面的可移植性问题。
    JDK1.8在@sun.misc.Contended注释的字段(或者类的实例)的前和后各自填充大小为缓存行宽度的2倍的填充空间。Java虚拟机则根据这个注解进行填充来使得被注解的实例变量或者类的实例能够被加载到单独的一个缓存行之中。
    减少共享变量的访问频率有助于降低伪共享问题出现的频率。

本章小结

  1. Java虚拟机自Java 6开始对内部锁进行了若干优化:锁消除、锁粗化、偏向锁以及适应性锁。除锁消除是Java 7开始引入的,其他优化均是在Java 6开始引入的,这些优化仅在Java虚拟机的server模式下起作用。这些优化默认都是开启的,且多数优化都可能依赖于JIT的内联优化,并且其本身也可能是通过JIT编译实现的。因此,这些优化都有其开销。锁消除优化能够彻底消除锁的开销,它依赖于逃逸分析技术。锁粗化优化能够减少线程申请/释放锁的频率,其代价是使临界区长度变大,从而可能导致进程在申请锁时的等待时间变长。偏向锁优化可以减小锁的申请/释放开销,它不适用于争用程度较高的锁。适应性锁优化可以减小锁申请的开销,有利于减少上下文切换。
  2. 锁的开销主要是由争用锁引起的。这些开销主要包括:上下文切换与线程调度开销、内存同步、编译器优化受限的开销以及限制可伸缩性。降低锁的开销可以从使用锁的替代品、降低锁的争用程度以及减少线程所需申请的锁的数量这几个方面入手。
  3. 使用可参数化锁可以减少线程所需申请的锁的数量从而降低锁的开销,但是它在一定程度上破坏了封装性。
  4. 减小临界区的长度可以减少锁的持有时间,从而降低锁的争用程度。减小临界区的长度有利于适用性锁优化发挥作用。在不影响线程安全的前提下,将临界区中的阻塞式I/O等阻塞操作以及较耗时的操作挪到临界区之外可以减小临界区的长度。
  5. 减小锁的粒度可以降低锁的申请频率从而降低锁的争用程度。减小锁的粒度常用技术包括锁拆分技术和锁分段技术。锁拆分技术在高争用情况下的效果可能并不明显;锁分段技术会使得对整个对象进行加锁比较困难乃至不可能。
  6. 减小上下文切换可以从这几个方面入手:控制线程数量、避免在临界区中执行阻塞式I/O等阻塞操作、避免在临界区中执行比较耗时的操作和减少Java虚拟机垃圾回收。
  7. 运用多线程设计模式也有助于提升多线程程序的性能,但是程序的复杂性也可能相应增加。
  8. 伪共享产生的前提是多个线程访问被缓存到同一个缓存行中的不同变量,它会导致大量的缓存未命中,从而增加内存访问操作的开销。了解Java对象的内存布局有助于分析与消除伪共享。Java对象内存布局的规则包括:对象是以8字节为粒度进行的对齐的、对象中的实例字段并非按照其源代码声明顺序排列以及继承自父类的实例字段不会与类本身定义的字段混杂在一起进行存储等。使用jol工具可以查看具体对象的内存布局情况。判断伪共享是否存在可以从分析多个线程是否存在共同的共享变量入手,并通过jol以及Linux内核工具perf来进一步分析与确认。伪共享可通过手工填充、自动填充以及降低共享变量的访问频率这几个方面来消除与规避。手工填充与自动填充可以在无须调整程序算法的前提下消除伪共享。手工填充的缺点比较多,使用该方法我们必须直到缓存行的宽度、Java对象的具体内存布局,这使得该方法存在硬件、软件层面的可移植性问题,并对人员的要求比较高。并且,我们还需要避免手工填充的字段被Java虚拟机优化掉。自动填充依赖于@Contened注解,它避免了手动填充的缺点,但是其消耗的额外空间更多。Java虚拟机对自动填充的之间需要通过Java虚拟机的开关“-XX:-RestrictContented"开启。虽然减少共享变量的访问频率所带来的效果可能比较明显,但是由于它可能涉及程序算法的调整,因此其适用范围比较有限。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值