JAVA锁优化

简介: 锁优化思路 最好的方式不加锁,如果必须加锁,可以从如下几个方面入手进行锁优化: 1. 减少锁持有时间 2. 减小锁粒度 3. 锁分离 4. 锁粗化 减少锁的持有时间 减少锁的持有时间,即减少锁内代码执行时间,可以通过减少锁内代码量实现,例如避免给整个方法加锁、将不需要加锁的代码移出去,例如: p.

锁优化思路

最好的方式不加锁,如果必须加锁,可以从如下几个方面入手进行锁优化:

1. 减少锁持有时间
2. 减小锁粒度
3. 读写锁替代独占锁
4. 锁分离
5. 锁粗化

减少锁的持有时间

减少锁的持有时间,即减少锁内代码执行时间,可以通过减少锁内代码量实现,例如避免给整个方法加锁、将不需要加锁的代码移出去,例如:

 public synchronized void doSomething() { 
     System.out.println("before");
     needLockCode(); 
     System.out.println("after");
 }
 
 改为:
 
 public void doSomething() { 
     System.out.println("before");
     synchronized(this){ 
         needLockCode(); 
     } 
     System.out.println("after");
 }

或:

 public void doSomething() { 
     synchronized(this){ 
         System.out.println("before");
         needLockCode(); 
         System.out.println("after");
     } 
 }
 
 改为:
 
  public void doSomething() { 
     System.out.println("before");
     synchronized(this){ 
         needLockCode(); 
     } 
     System.out.println("after");
 }

减小锁的粒度

减小锁的粒度,这个偏向于减小被锁住代码涉及的影响范围的减小,降低锁竞争的几率,例如jdk5的ConcurrentHashMap,ConcurrentHashMap不会为整个hash表加锁,而是将Hash表划分为多个分段,对每个段加锁,这样减小了锁粒度,提升了并发处理效果。

再如假设有对象object,如果加锁后,不允许对object操作,此时锁粒度相当于object对象,如果实际上object只有一个名为needLock字段可能会出现并发问题,此时将锁加在这个字段上即可。

读写锁替代独占锁

ReentrantLock和synchronized使用的是独占锁,无论是读或写都保证同时只有一个线程执行被锁代码。但是单纯的读实际上不会引起并发问题。尤其是对于读多写少的场景,可以将读和写的锁分离开来,可以有效提升系统的并发能力。

读写锁在同一时刻可以允许多个线程访问,但是在写线程访问时,所有的读线程和其他写线程都会被阻塞。读写锁维护了一对锁:读锁和写锁。一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。

当执行读操作的时候,需要获取读锁,在并发访问的时候,读锁不会被阻塞;在执行写操作时线程必须要获取写锁,当已经有线程持有写锁的情况下,所有的线程都会被阻塞。读锁和写锁关系:

读锁与读锁可以共享;
读锁与写锁互斥;
写锁与写锁互斥。

ReentrantReadWriteLock是提供了读锁和写锁:

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    ...
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;

    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    ...
}

锁分离

在读写锁的思想上做进一步的延伸,如果对两个上下文互相不依赖、互相不影响的操作使用了同一把锁,这时候可以把锁进行拆分,根据不同的功能拆分不同的锁, 进行有效的锁分离。

一个典型的示例便是LinkedBlockingQueue,在它内部,take()和put()分别实现了从队列中取得数据和往队列中增加数据的功能,虽然两个方法都对当前队列进行了修改操作,但由于当前队列为链表实现,两个操作分别作用于队列的前端和尾端,从理论上说,两者并不冲突。

如果使用独占锁,那么同一时间两个操作不能同时进行,会因为等待锁资源而阻塞。但是两个操作实际上是不冲突的,这时候可以使take()和put()各自使用一把锁,提高并发效率。LinkedBlockingQueue中为两个操作分别准备了takeLock和putLock:

 1     /** Lock held by take, poll, etc */
 2     private final ReentrantLock takeLock = new ReentrantLock();
 3 
 4     /** Wait queue for waiting takes */
 5     private final Condition notEmpty = takeLock.newCondition();
 6 
 7     /** Lock held by put, offer, etc */
 8     private final ReentrantLock putLock = new ReentrantLock();
 9 
10     /** Wait queue for waiting puts */
11     private final Condition notFull = putLock.newCondition();

锁粗化

必要的时候,将被锁住的代码量变多、锁持有时间更长也是锁优化的方式,但优化结果一定要使整体的执行效率变的更好,例如:

 for(int i = 0; i < 100; i++) {
     synchronized(lock) {
         needLockCode();             
     }
 }

 改为:

 synchronized(lock) {
     for(int i = 0; i < 100; i++) {
         needLockCode();
     }
 }

改造后,尽管每个线程每次持有锁的时间变长了,但减少了每个线程请求和释放锁的次数,而请求和释放锁也是要消耗资源的。

虚拟机的锁优化

1、自旋锁与自适应自旋

由于挂起线程和恢复线程都需要转入内核态完成,给系统带来很大压力,同时,共享数据的锁定状态只会持续很短的一段时间,因此去挂起和恢复线程很不值得。因此,可以使线程执行一个自我循环,因为对于执行时间短的代码这一会可能就会释放锁,而线程就不需要进行一次阻塞与唤醒。

自旋等待不能代替阻塞,自旋本身虽然避免了线程切换的开销,但是会占用处理器时间,如果锁被占用时间短,自旋等待效果好;反之,自旋的线程只会白白浪费处理器资源;因此,要限制自旋等待时间,自旋次数默认值是10次,超过次数仍然没有成功获取锁,就挂起线程,进入同步阻塞状态。

自适应自旋更智能一些,它根据前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定自旋次数,如果对于某个锁的自旋很少有成功获得过锁,就不自旋了,避免浪费CPU资源。如果自旋等待刚刚成功获得过锁,并且持有锁的线程在运行,则认为此次自旋很有可能成功,就允许自旋更多的次数。

2. 锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的目的主要是判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把他们当作栈数据对待,认为它们是线程私有的,同步加锁自然就无需进行。

有时候锁是开发者无意中涉及到的,例如对于下面代码:

    public static String getStr(String s1, String s2) {
        return s1 + s2;
    }

只进行了字符串的拼接,但其中的s1 + s2可能被虚拟机优化为:

    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();

而append()涉及了synchronized:

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

append()中的锁就是sb对象,如果该对象在方法中new的话,sb对象就不会逃逸到方法以外,jvm认为此时不必要加锁,此处的锁就被安全的消除了。

3. 锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。

但如果一系列操作频繁对同一个对象加锁解锁,或者加锁操作再循环体内,会耗费性能,这时虚拟机会扩大加锁范围来减少获取锁、释放锁的操作。具体可以看上文示例。

4. 轻量级锁

轻量级锁是JDK6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

在代码进入同步块的时候,如果同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录( Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word的锁标志位(Mark Word的最后2bit)将转变为“00”,即表示此又对象处于轻量级锁定状态。

如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了,如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,自旋失败后要膨胀为重量级锁,Mark Word中存储的就是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争”,这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

5. 偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。

当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如Locking、Unlocking及对Mark Word的Update等)。当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。

也就是说,偏向锁会偏向第一个获得它的线程,只有当其它线程尝试竞争偏向锁时,偏向模式才会失效。偏向锁是为了避免某个线程反复执行获取、释放同一把锁时的性能消耗,即如果仍是同个线程去获得这个锁,偏向锁模式会直接进入同步块,不需要再次获得锁。

锁的作用效果

偏向锁是为了避免某个线程反复执行获取、释放同一把锁时的性能消耗,而轻量级锁和自旋锁是为了避免重量级锁,因为重量级锁属于操作系统层面的互斥操作,挂起和唤醒线程是非常消耗资源的操作。

锁获取过程

最终,锁的获取过程会是,首先会尝试轻量级锁,轻量级锁会使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在多线程对锁资源的竞争。此时会会尝试自旋锁,如果自旋失败,最终只能膨胀为重量级锁。

除重量级锁外,以上锁均为乐观锁。

注:本文内容参考自:《Java高并发程序设计》、《深入理解Java虚拟机》

目录
相关文章
|
7天前
|
Java
Java中ReentrantLock释放锁代码解析
Java中ReentrantLock释放锁代码解析
24 8
|
12天前
|
存储 缓存 算法
优化 Java 后台代码的关键要点
【4月更文挑战第5天】本文探讨了优化 Java 后台代码的关键点,包括选用合适的数据结构与算法、减少不必要的对象创建、利用 Java 8 新特性、并发与多线程处理、数据库和缓存优化、代码分析与性能调优、避免阻塞调用、JVM 调优以及精简第三方库。通过这些方法,开发者可以提高系统性能、降低资源消耗,提升用户体验并减少运营成本。
|
14天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【4月更文挑战第3天】 在Java并发编程中,线程池是一种重要的资源管理工具,它能有效地控制和管理线程的数量,提高系统性能。本文将深入探讨Java线程池的工作原理、应用场景以及优化策略,帮助读者更好地理解和应用线程池。
|
7天前
|
Java 调度
Java中常见锁的分类及概念分析
Java中常见锁的分类及概念分析
13 0
|
1天前
|
SQL 缓存 Java
Java数据库连接池:优化数据库访问性能
【4月更文挑战第16天】本文探讨了Java数据库连接池的重要性和优势,它能减少延迟、提高效率并增强系统的可伸缩性和稳定性。通过选择如Apache DBCP、C3P0或HikariCP等连接池技术,并进行正确配置和集成,开发者可以优化数据库访问性能。此外,批处理、缓存、索引优化和SQL调整也是提升性能的有效手段。掌握数据库连接池的使用是优化Java企业级应用的关键。
|
3天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
4天前
|
Java 编译器
Java并发编程中的锁优化策略
【4月更文挑战第13天】 在Java并发编程中,锁是一种常见的同步机制,用于保证多个线程之间的数据一致性。然而,不当的锁使用可能导致性能下降,甚至死锁。本文将探讨Java并发编程中的锁优化策略,包括锁粗化、锁消除、锁降级等方法,以提高程序的执行效率。
11 4
|
10天前
|
设计模式 缓存 安全
分析设计模式对Java应用性能的影响,并提供优化策略
【4月更文挑战第7天】本文分析了7种常见设计模式对Java应用性能的影响及优化策略:单例模式可采用双重检查锁定、枚举实现或对象池优化;工厂方法和抽象工厂模式可通过对象池和缓存减少对象创建开销;建造者模式应减少构建步骤,简化复杂对象;原型模式优化克隆方法或使用序列化提高复制效率;适配器模式尽量减少使用,或合并多个适配器;观察者模式限制观察者数量并使用异步通知。设计模式需根据应用场景谨慎选用,兼顾代码质量和性能。
|
11天前
|
安全 Java 调度
深入理解Java中的线程安全与锁机制
【4月更文挑战第6天】 在并发编程领域,Java语言提供了强大的线程支持和同步机制来确保多线程环境下的数据一致性和线程安全性。本文将深入探讨Java中线程安全的概念、常见的线程安全问题以及如何使用不同的锁机制来解决这些问题。我们将从基本的synchronized关键字开始,到显式锁(如ReentrantLock),再到读写锁(ReadWriteLock)的讨论,并结合实例代码来展示它们在实际开发中的应用。通过本文,读者不仅能够理解线程安全的重要性,还能掌握如何有效地在Java中应用各种锁机制以保障程序的稳定运行。
|
12天前
|
Java 编译器
Java并发编程中的锁优化策略
【4月更文挑战第5天】随着多核处理器的普及,并发编程在提高程序性能方面发挥着越来越重要的作用。在Java中,锁是实现并发控制的关键机制。本文将探讨Java并发编程中的锁优化策略,包括锁粗化、锁消除、锁排序等技术,以提高程序的执行效率和降低资源争用。