Java中如何提升锁性能

作者:张泽立(Java架构沉思录做了部分修改)
原文:https://my.oschina.net/u/3703858/blog/1791973

1、减少持有锁的时间

比如100个人去银行办理业务,要填一百张表,但是只有一支笔,那么很显然,每个人用笔的时间越短,效率也就越高。看代码:

/*
优化前:
othercode1和othercode2很耗时间,里面没有涉及资源同步,只有mutexMethod方法要对资源同步,
所有优化代码让持有锁时间尽量短
*/
public synchronized void syncMethod(){
        othercode1();
        mutexMethod();
        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的执行效率和可靠性

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

2、减小锁的粒度

ConcurrentHashMap的实现,他的内部被分为了若干个小的hashmap,称之为段(SEGMENT),默认是16段。

减小锁粒度会引入一个新的问题,当需要获取全局锁的时候,其消耗的资源会较多,比如ConcurrenthashMap的size()方法。可以看到计算size的时候需要计算全部有效的段的锁。

public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
}

final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

事实上计算size的时候会先使用无锁的方式计算,如果失败会采用这个方法,但是在高并发的场合concurrenthashmap的size依然要差于同步的hashmap.因此在类似于size获取全局信息方法调用不频繁的情况下,这种减小粒度的的方法才是真正意义上的提高系统并发量。

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

3、使用读写分离替代独占锁

在读多写少的情况下,使用读写锁可以有效的提高系统性能 ReadWriteLock可以提高系统性能。

package com.high.concurrency;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author: zhangzeli
 * @date 8:43 2018/4/10
 * <P></P>
 */
public class ReadWriteLockDemo {
    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock =readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private int value;

    public Object handleRead(Lock lock) throws InterruptedException{
        try {
            lock.lock();
            Thread.sleep(1000);
            return value;
        }finally {
            lock.unlock();
        }
    }

    public void handleWrite(Lock lock,int index) throws InterruptedException{
        try {
            lock.lock();
            Thread.sleep(1000);
            value =index;
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final ReadWriteLockDemo demo = new ReadWriteLockDemo();
        Runnable readRunnale = new Runnable() {
            @Override
            public void run() {
                try {
                    demo.handleRead(lock);
                    //demo.handleRead(readLock);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Runnable write = new Runnable() {
            @Override
            public void run() {
                try {
                    //demo.handleWrite(writeLock,new Random().nextInt());
                    demo.handleWrite(lock,new Random().nextInt());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        for(int i=0;i<18;i++){
            new Thread(readRunnale).start();
        }
        for(int i=18;i<20;i++){
            new Thread(write).start();
        }
    }
}

差异很明显。

4.锁分离

以LinkedBlockingQueue为例,take函数和put函数分别实现了从队列取和往队列加数据,虽然两个方法都对队列进项了修改,但是LinkedBlockingQueue是基于链表的所以一个操作的是头,一个是队列尾端,从理论情况下将并不冲突。

如果使用独占锁则take和put就不能完成真正的并发,所以jdk并没有采用这种方式,取而代之的是两把不同的锁分离了put和take的操作,下面看源码:

    /** Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();//take函数需要持有takeLock

    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();//put函数需要持有putLock

    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();
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) { //如果当前没有可用数据,一直等待
                notEmpty.await();      //等待,put操作的通知
            }
            x = dequeue();         //取得第一个数据
            c = count.getAndDecrement();//数量减一,原子操作因为回合put同时访问count.注意变量c是count减一
            if (c > 1)
                notEmpty.signal();  //通知其他take操作
        } finally {
            takeLock.unlock(); //释放锁
        }
        if (c == capacity)
            signalNotFull(); //通知put,已有空余空间
        return x;
    }

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();//不能有两个线程同时进行put
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) { //如果队列已满
                notFull.await();   //等待
            }
            enqueue(node);  //插入数据
            c = count.getAndIncrement(); //更新总数,变量c是count加1前的值
            if (c + 1 < capacity)
                notFull.signal();   //有足够的空间,通知其他线程
        } finally {
            putLock.unlock();  //释放锁
        }
        if (c == 0)
            signalNotEmpty();  //插入成功后,通知take操作
    }

5、锁粗化

虚拟机在遇到一连串地对同一锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减小对锁的请求同步次数,这个操作叫锁粗化,比如

//优化前
for (int i=0;i<20;i++){
     synchronized (lock){

     }
}

//优化后
synchronized (lock){
    for (int i=0;i<20;i++){


     }
}

注意:性能优化就是根据运行时的真实情况对各个资源点进行权衡折中的过程,锁粗话的思想和减少锁持有时间是相反的,但是在不同的场合,他们的效果并不相同,所以大家要根据实际情况,进行权衡

6、Java虚拟机锁优化

6.1锁偏向

偏向锁,简单的讲,就是在锁对象的对象头中有个ThreaddId字段,这个字段如果是空的,第一次获取锁的时候,就将自身的ThreadId写入到锁的ThreadId字段内,将锁头内的是否偏向锁的状态位置1。这样下次获取锁的时候,直接检查ThreadId是否和自身线程Id一致,如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段,提高了效率。

但是偏向锁也有一个问题,就是当锁有竞争关系的时候,需要解除偏向锁,使锁进入竞争的状态

参数-XX:+UseBiasedLocking

Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。它通过消除资源无竞争情况下的同步原语, 进一步提高了程序的运行性能。偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中, 该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。如果在运行过程中,遇到了其他线程抢占锁, 则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。(偏向锁只能在单线程下起作用) 因此 流程是这样的 偏向锁->轻量级锁->重量级锁

6.2轻量级锁

轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。

然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁解锁:轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。

如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

注:轻量级锁会一直保持,唤醒总是发生在轻量级锁解锁的时候,因为加锁的时候已经成功CAS操作;而CAS失败的线程,会立即锁膨胀,并阻塞等待唤醒。(详见下图)

下图是两个线程同时争夺锁,导致锁膨胀的流程图。

锁不会降级

自旋其实就是虚拟机为了避免线程真实的在操作系统层挂起,虚拟机让当前线程做空轮询或许是几个cpu时间周期,如果还没办法获取锁则在挂起.

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁, 就不会再恢复到轻量级锁状态。 当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程, 被唤醒的线程就会进行新一轮的夺锁之争。

6.3锁消除

锁消除是Java虚拟机在JIT编译是,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间

public class TestLockEliminate {
    public static String getString(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }

    public static void main(String[] args) {
        long tsStart = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            getString("TestLockEliminate ", "Suffix");
        }
        System.out.println("一共耗费:" + (System.currentTimeMillis() - tsStart) + " ms");
    }
}

getString()方法中的StringBuffer数以函数内部的局部变量,进作用于方法内部,不可能逃逸出该方法,因此他就不可能被多个线程同时访问,也就没有资源的竞争,但是StringBuffer的append操作却需要执行同步操作:

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

逃逸分析和锁消除分别可以使用参数-XX:+DoEscapeAnalysis和-XX:+EliminateLocks(锁消除必须在-server模式下)开启。使用如下参数运行上面的程序:

使用如下命令运行程序:-XX:+DoEscapeAnalysis -XX:+EliminateLocks

锁的优缺点对比

原文发布于微信公众号 - Java架构沉思录(code-thinker)

原文发表时间:2018-06-10

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏hotqin888的专栏

bootstrap treeview 增删改的正确姿势

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hotqin888/article/det...

4803
来自专栏犀利豆的技术空间

徒手撸框架--实现 RPC 远程调用

微服务已经是每个互联网开发者必须掌握的一项技术。而 RPC 框架,是构成微服务最重要的组成部分之一。趁最近有时间。又看了看 dubbo 的源码。dubbo 为了...

1532
来自专栏精讲JAVA

Netty入门学习系列--helloworld服务端(一)

public class NettyServerHandle extends ChannelInboundHandlerAdapter {

1282
来自专栏大内老A

这算是ASP.NET MVC的一个大BUG吗?

这是昨天一个同事遇到的问题,我觉得这是一个蛮大的问题,而且不像是ASP.NET MVC的设计者有意为之,换言之,这可能是ASP.NET MVC的一个Bug(不过...

2188
来自专栏Java 源码分析

NioEventLoopGroup 源码分析

NioEventLoopGroup 源码分析 1. 在阅读源码时做了一定的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限。为了方便 IDE 查看...

3496
来自专栏向治洪

动态补丁升级

一、概述 最新github上开源了很多热补丁动态修复框架,大致有: https://github.com/dodola/HotFix https://git...

3719
来自专栏java小记

java多线程学习(1)-锁的简介

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

980
来自专栏Golang语言社区

理解Go语言Web编程(上)

断断续续学Go语言很久了,一直没有涉及Web编程方面的东西。因为仅是凭兴趣去学习的,时间有限,每次去学,也只是弄个一知半解。不过这两天下定决心把Go语言Web编...

38412
来自专栏青玉伏案

iOS逆向工程之Hopper中的ARM指令

虽然前段时间ARM被日本软银收购了,但是科技是无国界的,所以呢ARM相关知识该学的学。现在看ARM指令集还是倍感亲切的,毕竟大学里开了ARM这门课,并且做了不少...

3397
来自专栏進无尽的文章

简述OC语言

对于一门语言的学习是需要时间领悟的,而对于一些原理性的问题,我们需要清楚其核心思想,知其然而知其所以然,这样才能有利于自己的后续发展。本文只是简述,没有面面具到...

2292

扫码关注云+社区

领取腾讯云代金券