锁的优化

对于单任务或者单线程的应用程序而言,主要资源消耗都花在任务本身,它既不需要维护并行数据结构间的一致性,也不需要为线程的切换和调度花费时间。但对于多线程的应用来说,系统出了处理功能需求外,还需要额外维护多线程环境的特有信息,如线程本身的元数据、线程的调度、线程上下文的切换等。在高并发环境下,激烈的锁今早会导致程序性能下降,自然有必要讨论一些有关锁的性能问题以及相关的一些注意事项

一,有助于提高锁性能的几点建议

1.减小锁持有时间

如果线程持有锁的时间很长,那么相对地,锁的竞争程度也就越激烈,eg:

	public synchronized void syncMethos(){
		othercode1();
		mutextMethos();
		othercode2();
	}

上述代码中,假设只有mutextMethos()方法是需要同步的,其他两个无需同步且是重量级的方法,则会花费较长的CPU时间,此时,如果并发量大,使用这种对整个方法做同步的方案,会导致等待线程大量增加,因为一个线程在进入该方法时获得内部锁,只有在所有任务都执行完后才会释放锁。so,较为优化的方案,只在必要时进行同步,这样就能明显减少线程持有锁的时间,提高系统吞吐量

	public void syncMethos(){
		othercode1();
		synchronized(this){
			mutextMethos();
		}
		othercode2();
	}

2.减小锁粒度

典型的使用场景就是ConcurrentHashMap

对于HashMap来说,最重要的两个方法即get和put,最自然的想法就是对整个HashMap加锁,必然可以得到一个线程安全的对象,但是这样的锁的粒度太大。ConcurrentHashMap,它内部进一步细分了若干个小的HashMap,称为段(segment),默认情况下,一个ConcurrentHashMap被进一步细分为16个段

如果需要在ConcurrentHashMap中增加一个表项,并不是将整个HashMap加锁,而是首先根据hashCode得到该表项应该被放在哪个段中,然后对该段加锁,并完成put操作,在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不放在同一个段中,则线程间便可以做到真正的并行

3.读写分离锁来替代独占锁

读写锁ReadWriteLock之前在写Lock的时候提到过,使用读写分离锁是减小锁粒度的一种特殊情况,是通过对系统功能点分割提高系统性能,应用在读多写少的场合

扫描二维码关注公众号,回复: 2232549 查看本文章

4.锁分离

典型的例子即LinkedBlockingQueue的实现,take函数和put函数分别实现了从队列中取数据和增加数据的功能,虽然两个函数都对队列进行修改操作,但由于LinkedBlockQueue基于链表,两个操作分别在队头和队尾,理论上说并不冲突

若使用独占锁,则take和put操作就不能真正并发,在运行时,它们会彼此等待释放锁资源,在JDK中,使用两把不同的锁:

    /** 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();

take操作时,如果队列为空,则让当前线程等待在notEmpty上,新元素入队列时,则进行一次notEmpty上的通知

    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();
            if (c > 1)
                notEmpty.signal();//通知其他take操作
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();//通知put操作,已有空余空间
        return x;
    }

相应的put

    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();
        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();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();//插入成功后,通知take操作取数据
    }

5.锁粗化

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

	public void demoMethod(){
		synchronized(lock){
			//do sth.
		}
		//做其他不需要的同步的工作,但很快能执行完毕
		synchronized(lock){
			//do sth.
		}
	}

优化整合

	public void demoMethod(){
		//整合为一次请求
		synchronized(lock){
			//do sth.
			//做其他不需要的同步的工作,但很快能执行完毕
		}
	}

二,锁优化

(本来想记个笔记再次翻的时候比较方便,结果昨天写了很久忘记保存。。。今天补上,今后希望不再犯这样的错误)

1.锁偏向

JDK1.6中引入的一项锁优化。当一个线程访问同步块并获得锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示当前线程已经获得了锁,若测试失败,需测试下Mark Word中偏向锁的标识是否设置为1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程

2.轻量级锁

JDK1.6之中加入的新型锁机制。如果偏向锁失败,虚拟机并不会立即挂起线程,使用一种称为轻量锁的优化手段,即将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁,如果线程获得轻量锁成功,则可顺利进入临界区,如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁

3.自旋锁

锁膨胀后,虚拟机为了避免线程真实的在操作系统层面挂起,还会做最后的努力,即自旋。系统会进行一次赌注:假设在不仅将来,线程可以得到这把锁,因此,虚拟机让当前线程做几个空循环(即自旋含义),在经过若干次循环后,如果可以得到锁,那么就进入临界区,如果还不能得到锁,才会真实地将线程在操作系统层面挂起

自旋锁在JDK1.4.2中就已经引入,不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK1.6中就已经改为默认开启了,自旋次数的默认值是10次,可以使用参数-XX:PreBlockSpin来更改

在JDK1.6中引入了自适应的自旋锁,自适应意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环

4.锁消除

即通过对运行上下文的扫描,取出不可能存在共享资源竞争的锁,so,可能会有个问题,如果不可能存在竞争,为什么程序员还要加上锁?看个程序

	public String [] createStrings(){
		Vector<String> v=new Vector<>();
		for(int i=0;i<100;i++){
			v.add(Integer.toString(i));
		}
		return v.toArray(new String[]{});
	}

代码中的Vector,由于变量v只在函数中使用,因此它只是一个单纯的局部变量,局部变量是在线程栈上分配的,属于线程私有的数据,因此不可能被其他线程访问,so在这种情况下加锁同步是没有必要的,如果虚拟机检测到这种情况,就会将这些无用的锁操作去除

锁消除涉及到的关键技术为逃逸分析,即观察某一个变量是否会逃出某一个作用于,在本例中,变量v显然没有逃出函数之外,以此为基础虚拟机才可以大胆地将v内部的加锁操作去除,如果函数返回的不是String数组而是v本身,则认为变量v逃逸出了当前函数,也就是说v有可能被其他线程访问,若是这样,虚拟机就不能消除v中的锁操作

逃逸分析必须在-server模式下运行,可以使用-XX:+DoEscapeAnalysis参数打开逃逸分析,使用-XX:+EliminateLocks参数可以打开锁消除

持续更新。。。

参考:

《深入理解java虚拟机》

《java高并发程序设计实战》

猜你喜欢

转载自blog.csdn.net/autumn03/article/details/80938383