锁的优化和注意事项

锁优化分为代码层面的优化和jvm层面的优化

1. 代码层面的锁优化的思路和方法

一旦用到锁,就说明这是阻塞式的,所以在并发度上一般来说都会比无锁的情况低一点。

这里提到的锁优化,是指在阻塞式的情况下,如何让性能不要变得太差。但是再怎么优化,一般来说性能都会比无锁的情况差一点。

ReentrantLock中的tryLock,偏向于一种无锁的方式,因为在tryLock判断时,并不会把自己挂起。

锁优化的思路和方法总结一下,有以下几种。

  • 减少锁持有时间
  • 减小锁粒度
  • 锁分离
  • 锁粗化
  • 锁消除

1.1 减少锁持有时间

public synchronized void syncMethod(){  

        othercode1();  

        mutextMethod();  

        othercode2(); 

    }

像上述代码这样,在进入方法前就要得到锁,其他线程就要在外面等待。

这里优化的一点在于,要减少其他线程等待的时间,所以,只用在有线程安全要求的程序上加锁

public void syncMethod(){  

        othercode1();  

        synchronized(this)

        {

            mutextMethod();  

        }

        othercode2(); 

    }

1.2 减小锁粒度

将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。

最最典型的减小锁粒度的案例就是ConcurrentHashMap。

1.3 锁分离

最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。

读写分离思想可以延伸,只要操作互不影响,锁就可以分离。

比如LinkedBlockingQueue 从头部取出,从尾部放数据。JDK并发包2中提到的ForkJoinPool中的工作窃取。

1.4 锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。

举个例子:

public void demoMethod(){  

        synchronized(lock){   

            //do sth.  

        }  

        //做其他不需要的同步的工作,但能很快执行完毕  

        synchronized(lock){   

            //do sth.  

        } 

    }

这种情况,根据锁粗化的思想,应该合并

public void demoMethod(){  

        //整合成一次锁请求 

        synchronized(lock){   

            //do sth.   

            //做其他不需要的同步的工作,但能很快执行完毕  

        }

    }

当然这是有前提的,前提就是中间的那些不需要同步的工作是很快执行完成的。

再举一个极端的例子:

for(int i=0;i<CIRCLE;i++){
   synchronized(lock){  

   }
}

在一个循环内不同得获得锁。虽然JDK内部会对这个代码做些优化,但是还不如直接写成

synchronized(lock){
    for(int i=0;i<CIRCLE;i++){ 

    }
}

当然如果有需求说,这样的循环太久,需要给其他线程不要等待太久,那只能写成上面那种。如果没有这样类似的需求,还是直接写成下面那种比较好。

1.5 锁消除

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

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

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

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

比如:

public static void main(String args[]) throws InterruptedException {

        long start = System.currentTimeMillis();

        for (int i = 0; i < 2000000; i++) {

            createStringBuffer("JVM", "Diagnosis");

        }

        long bufferCost = System.currentTimeMillis() - start;

        System.out.println("craeteStringBuffer: " + bufferCost + " ms");

    }

    public static String createStringBuffer(String s1, String s2) {

        StringBuffer sb = new StringBuffer();

        sb.append(s1);

        sb.append(s2);

        return sb.toString();

    } 

上述代码中的StringBuffer.append是一个同步操作,但是StringBuffer却是一个局部变量,并且方法也并没有把StringBuffer返回,所以不可能会有多线程去访问它。

那么此时StringBuffer中的同步操作就是没有意义的。

开启锁消除是在JVM参数上设置的,当然需要在server模式下:

  -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

并且要开启逃逸分析。 逃逸分析的作用呢,就是看看变量是否有可能逃出作用域的范围。

比如上述的StringBuffer,上述代码中craeteStringBuffer的返回是一个String,所以这个局部变量StringBuffer在其他地方都不会被使用。如果将craeteStringBuffer改成

public static StringBuffer craeteStringBuffer(String s1, String s2) {

        StringBuffer sb = new StringBuffer();

        sb.append(s1);

        sb.append(s2);

        return sb;

    }

那么这个 StringBuffer被返回后,是有可能被任何其他地方所使用的(譬如被主函数将返回结果put进map啊等等)。那么JVM的逃逸分析可以分析出,这个局部变量 StringBuffer逃出了它的作用域。

所以基于逃逸分析,JVM可以判断,如果这个局部变量StringBuffer并没有逃出它的作用域,那么可以确定这个StringBuffer并不会被多线程所访问,那么就可以把这些多余的锁给去掉来提高性能。

当JVM参数为:

  -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

输出:

  craeteStringBuffer: 302 ms

JVM参数为:

  -server -XX:+DoEscapeAnalysis -XX:-EliminateLocks

输出:

  craeteStringBuffer: 660 ms

显然,锁消除的效果还是很明显的。

2. 虚拟机内的锁优化

在JVM中,每个对象都有一个对象头。

Mark Word,对象头的标记,32位(32位系统)。

描述对象的hash、锁信息,垃圾回收标记,年龄还会保存指向锁记录的指针,指向monitor的指针,偏向锁线程ID等。

简单来说,对象头就是要保存一些系统性的信息。

2.1 偏向锁

所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程 。

大部分情况是没有竞争的(某个同步块大多数情况都不会出现多线程同时竞争锁),所以可以通过偏向来提高性能。即在无竞争时,之前获得锁的线程再次获得锁时,会判断是否偏向锁指向我,那么该线程将不用再次获得锁,直接就可以进入同步块。

偏向锁的实施就是将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark

当其他线程请求相同的锁时,偏向模式结束

JVM默认启用偏向锁 -XX:+UseBiasedLocking

在竞争激烈的场合,偏向锁会增加系统负担(每次都要加一次是否偏向的判断)

偏向锁的例子:

public class Test {

    public static List<Integer> numberList = new Vector<Integer>();

    public static void main(String[] args) throws InterruptedException {

        long begin = System.currentTimeMillis();

        int count = 0;

        int startnum = 0;

        while (count < 10000000) {

            numberList.add(startnum);

            startnum += 2;

            count++;

        }

        long end = System.currentTimeMillis();

        System.out.println(end - begin);

    }

}

Vector是一个线程安全的类,内部使用了锁机制。每次add都会进行锁请求。上述代码只有main一个线程再反复add请求锁。

使用如下的JVM参数来设置偏向锁:

  -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

BiasedLockingStartupDelay表示系统启动几秒钟后启用偏向锁。默认为4秒,原因在于,系统刚启动时,一般数据竞争是比较激烈的,此时启用偏向锁会降低性能。

由于这里为了测试偏向锁的性能,所以把延迟偏向锁的时间设置为0。

此时输出为9209

下面关闭偏向锁:

  -XX:-UseBiasedLocking

输出为9627

一般在无竞争时,启用偏向锁性能会提高5%左右。

2.2 轻量级锁

Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意。

原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖操作系统互斥(mutex)来实现的。

互斥是一种会导致线程挂起,并在较短的时间内又需要重新调度回原线程的,较为消耗资源的操作。

为了优化Java的Lock机制,从Java6开始引入了轻量级锁的概念。

轻量级锁(Lightweight Locking)本意是为了减少多线程进入互斥的几率,并不是要替代互斥。

它利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG),尝试在进入互斥前,进行补救。

如果偏向锁失败,那么系统会进行轻量级锁的操作。它存在的目的是尽可能不用动用操作系统层面的互斥,因为那个性能会比较差。因为JVM本身就是一个应用,所以希望在应用层面上就解决线程同步问题。

总结一下就是轻量级锁是一种快速的锁定方法,在进入互斥之前,使用CAS操作来尝试加锁,尽量不要用操作系统层面的互斥,提高了性能。

那么当偏向锁失败时,轻量级锁的步骤:

1.将对象头的Mark指针保存到锁对象中(这里的对象指的就是锁住的对象,比如synchronized (this){},this就是这里的对象)。

  lock->set_displaced_header(mark);

2.将对象头设置为指向锁的指针(在线程栈空间中)。

if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(),mark)){
     TEVENT (slow_enter: release stacklock) ;
     return ; 

}

lock位于线程栈中。所以判断一个线程是否持有这把锁,只要判断这个对象头指向的空间是否在这个线程栈的地址空间当中。

如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁),就是操作系统层面的同步方法。在没有锁竞争的情况,轻量级锁减少传统锁使用OS互斥量产生的性能损耗。在竞争非常激烈时(轻量级锁总是失败),轻量级锁会多做很多额外操作,导致性能下降。

2.3 自旋锁

当竞争存在时,因为轻量级锁尝试失败,之后有可能会直接升级成重量级锁动用操作系统层面的互斥。也有可能再尝试一下自旋锁。

如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋),并且不停地尝试拿到这个锁(类似tryLock),当然循环的次数是有限制的,当循环次数达到以后,仍然升级成重量级锁。所以在每个线程对于锁的持有时间很少时,自旋锁能够尽量避免线程在OS层被挂起。

JDK1.6中-XX:+UseSpinning开启

JDK1.7中,去掉此参数,改为内置实现

如果同步块很长,自旋失败,会降低系统性能。如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能。

2.4 偏向锁,轻量级锁,自旋锁总结

首先偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁。

而轻量级锁和自旋锁都是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作。

为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁。

可见偏向锁,轻量级锁,自旋锁都是乐观锁。

时间: 2024-10-02 07:49:44

锁的优化和注意事项的相关文章

锁的优化及注意事项

"锁"的竞争必然会导致程序的整体性能下降,以下就是为了降低这种辐作用的建议:     1.减小锁持有时间 如果线程持有锁的时间很长,那么相对地,锁的竞争程度也就越激烈.程序开发应该尽可能地减少对某个锁的占有时间,以减少线程间互斥的可能. public synchronized void syncMethod(){ othercode1(); mutextMethod(); othercode2();} 优化后: public void syncMethod2(){ othercode1

锁的优化及注意事项(读书笔记)

有助于提高锁性能的几点建议 减少锁的持有时间 真正需要同步加锁的时候在加锁,减少锁的持有时间有助于减低锁冲突的可能性,进而提升系统的并发能力, 减少颗粒度,所谓减少颗粒度就是指缩小锁定对象的范围,从而减少锁冲突的可能性,进而提升系统的并发能力,问题在于类似于size()获取全局信息的方法调用并不频繁时,这种减少颗粒度的方法才能真正意义上提高系统吞吐量.(分割数据结构实现) 读写分离锁替换独占锁,读写锁是对系统功能点的分割 ReadWriteLock 在读多写少的场合,读写锁对系统性能是很有好吃的

【Java多线程】锁的优化策略

锁的优化策略 编码过程中可采取的锁优化的思路有以下几种: 1:减少锁持有时间 例如:对一个方法加锁,不如对方法中需要同步的几行代码加锁: 2:减小锁粒度 例如:ConcurrentHashMap采取对segment加锁而不是整个map加锁,提高并发性: 3:锁分离 根据同步操作的性质,把锁划分为的读锁和写锁,读锁之间不互斥,提高了并发性. 4:锁粗化 这看起来与思路1有冲突,其实不然.思路1是针对一个线程中只有个别地方需要同步,所以把锁加在同步的语句上而不是更大的范围,减少线程持有锁的时间: 而

Synchronized锁性能优化偏向锁轻量级锁升级 多线程中篇(五)

不止一次的提到过,synchronized是Java内置的机制,是JVM层面的,而Lock则是接口,是JDK层面的 尽管最初synchronized的性能效率比较差,但是随着版本的升级,synchronized已经变得原来越强大了 这也是为什么官方建议使用synchronized的原因 毕竟,他是一个关键字啊,这才是亲儿子,Lock,终归差了一点 简单看下,synchronized大致都经过了哪些重要的变革 重量级锁 对于最原始的synchronized关键字,锁被称之为重量级锁 因为底层依赖监

002-多线程-锁-同步锁-synchronized几种加锁方式、Java对象头和Monitor、Mutex Lock、JDK1.6对synchronized锁的优化实现

一.synchronized概述基本使用 为确保共享变量不会出现并发问题,通常会对修改共享变量的代码块用synchronized加锁,确保同一时刻只有一个线程在修改共享变量,从而避免并发问题. synchronized结论: 1.java5.0之前,协调线程间对共享对象的访问的机制只有synchronized和volatile,但是内置锁在功能上存在一些局限性,jdk5增加了Lock以及ReentrantLock. 2.java5.0,增加了一种新的机制:显式锁ReentrantLock,注意它

Java性能之synchronized锁的优化

synchronized / Lock 1.JDK 1.5之前,Java通过synchronized关键字来实现锁功能 synchronized是JVM实现的内置锁,锁的获取和释放都是由JVM隐式实现的 2.JDK 1.5,并发包中新增了Lock接口来实现锁功能 提供了与synchronized类似的同步功能,但需要显式获取和释放锁 3. Lock同步锁是基于Java实现的,而synchronized是基于底层操作系统的Mutex Lock实现的 每次获取和释放锁都会带来用户态和内核态的切换,从

Jdk1.6之后对锁的优化

Jdk1.6之后对锁的优化 偏向锁 偏向锁的意思就是,偏向于第一个获取该资源的锁,如果在接下来的过程中没有其他的线程获取到该锁,那么该获取到锁的线程将不需要获取同步的操作 自旋锁 竞争锁的线程如果没有获取到锁将会被挂起,当持有锁的线程释放掉锁的话将会被唤醒,这样挂起唤醒的操作效率很低,所以出现了自旋锁.自旋锁就是当线程获取锁失败的时候不会被挂起,而是通过执行一个忙循环的操作进行自旋.自旋锁默认是关闭的,可以使用-XX:+UseSpinning参数开启 自适应自旋锁 自旋锁如果是在一个业务逻辑执行

mysql sql优化及注意事项

sql优化分析 通过slow_log等方式可以捕获慢查询sql,然后就是减少其对io和cpu的使用(不合理的索引.不必要的数据访问和排序)当我们面对具体的sql时,首先查看其执行计划A.看其是否使用索引B.查看其查询的记录数C.确定索引的代价是否过高D.是否可以使用复合索引E.是否有“using temporary”F.是否有“using filesort” 创建高效索引 mysql的innodb有自己特殊的聚集索引(数据是按聚集索引的顺序存储的并和索引存储在一起),索引访问效率较高,次要索引是

SQL优化及注意事项

1. 把数据.日志.索引放到不同的I/O设备上,增加读取速度.数据量(尺寸)越大,提高I/O越重要. 2. 纵向.横向分割表,减少表的尺寸,如:可以把大数据量的字段拆分表. 3. 根据查询条件,建立索引,优化索引.优化访问方式,限制结果集的数据量.注意填充因子要适当(最好是使用默认值0).索引应该尽量小,尽量使用字节数小的列建索引,不要对有限的几个值的列建单一索引. 4. 用OR的字句可以分解成多个查询,并且通过UNION链接多个查询.它们的速度只与是否使用索引有关,如果查询需要用到联合索引,用