锁的优化

在高并发的环境下,锁是最常用的方法之一,激烈的锁竞争会导致系统出现性能方面的问题,这时就需要我们做一些锁的优化。

一.减小锁的持有时间

对于使用锁进行并发控制的应用程序而言,在锁竞争中,单个线程对锁持有时间与系统性能有直接的关系。如果线程持有时锁的时间很长那么相对的,锁的竞争也就越来越激烈。

举个例子: 如果要求20个人填写自我介绍,但是只有一支笔,如果有一个人抢到了笔,但是这个人没有想好怎么写,拿着笔去想,那么其他人只能等待,如果每个人都如此,无形中增加了许多时间。但是,如果没个人都想好了 再去拿笔去写,这样就能节省很长时间。

代码例子:

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

syncMethod方法中,假设只有mutextMethod()方法是要做同步控制,其他两个都不需要。如果其他两个是重量级方法,业务处理需要一些时间,那么这样就会加大CPU的消耗。

一个较为优化的解决方案是,只对mutextMethod()方法做同步控制。

 public void syncMethod(){

        othercode1();
        synchronized(this){
            mutextMethod();
        }
        othercode2();

    }

在改进的代码中,只对mutextMethod()方法做同步控制,锁占用的时间相对较短,这样在高并发的情况下,能提高效率。

在JDK的源码中,也能找到类似的解决方案。比如,处理正则表达式的Patten类

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

二.减小锁的粒度

减小锁的粒度,也是一种削弱多线程竞争的手段。这种技术典型的使用场景就是ConcurrentHashMap类的实现。

synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }

这是 put时候,源码里并没有全局加锁,而是首先根据hashcode得到该表项应该存放到哪个字段中,然后对该段加锁,并完成put操作。

但是,减小锁的粒度会引入一个新的问题,就是当系统需要取得全局锁的时候,其消耗的资源会更多。还是以concurrentHashMap为例子,concurrentHashMap的size()方法,他将返回concurrentHashMap的所有有效表项的数量,即,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()方法会先使用无锁的方式求和,执行失败了才会尝试这种加锁的方式。但是在高并发场合,concurrentHashMap的size()方法性能肯定要差于同步的HashMa()。

因此,只有在类似于size()获取全局信息的方法调用并不频繁时,这种减小锁粒度的方法才能真正意义上提高系统吞吐量。

三.锁分离

将读写锁的思想做一些延伸就是锁分离。 读写锁根据读写操作功能上的不同,进行了有效的锁分离。典型的案例就是 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()和take()间,put()和put()间进行锁的竞争,从而削弱了锁竞争的可能性。

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly(); //锁住,不能有两个线程同时take操作
    try {
        while (count.get() == 0) {
            notEmpty.await(); //如果当前没有数据,一直等待put的操作通知
        }
        x = dequeue(); //取得第一个数据
        c = count.getAndDecrement();//数量减1,原子操作,因为会和put()同时访问count。
        if (c > 1)
            notEmpty.signal();//通知其他tabke操作
    } finally {
        takeLock.unlock();//释放锁
    }
    if (c == capacity)
        signalNotFull();//通知put操作已有空间。
    return x;
}

同样,函数put()的操作也类似,就不在讲解。

四.锁粗化

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

public void demoMethod() {

       synchronized (lock){
           //do.sh
       }
       //做其他不必要的工作很快能执行完毕
       synchronized (lock){
           
       }
   }

会被整合成如下形式

public void demoMethod() {

        synchronized (lock){
            //do.sh
            //做其他不必要的工作很快能执行完毕
        }

    }

在开发中我们合理进行锁的粗化,避免频繁的请求与释放锁,尤其是在循环内,应该将锁放到循环外面。在这就不一一举例子了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值