Java多线程---锁

“锁”是较为经常使用的同步方法之一。在高并发环境下,激励的锁竞争会致使程序的性能降低。因此咱们们将在这里讨论一些有关于锁问题以及一些注意事项。好比:前端

  • 避免死锁java

  • 减小锁粒度node

  • 锁分离程序员

在多核时代,使用多线程能够明显地提升系统的性能。但事实上,使用多线程的方式会额外增长系统的开销。算法

对于单任务或者单线程的应用而言,其主要资源消耗都有花在任务自己。它既不须要号维护并行数据结构间的一致性状态,也不须要为线程的切换和调度花费时间。但对于多线程应用来讲,系统除了处理功能需求外,还须要额外维护多线程环境的特有信息,如线程自己的元数据、线程的调度、线程上下文的切换等。安全

事实上,在单核CPU上,采用并行算法的效率通常要低于原始的串行算法的,其根本缘由也在于此。所以,并行计算之因此能提升系统的性能,并非由于它“少干活”了,而是由于并行计算能够更合理的进行任务调度,充分利用各个CPU资源。所以,合理的并发,才能将多核CPU的性能发挥到极致。性能优化

提升“锁”性能的几点建议

减小锁持有的时间

在锁的竞争中,单个线程对锁的持有时间与系统性能有直接的关系.应该尽量的减小锁的占有时间,以减小线程之间互斥的可能.减小锁的持有时间有助于下降锁冲突的可能性,进而提升系统的并发能力.数据结构

同步整个方法,若是在并发量较大时,使用这种对整个方法作同步的方案.会致使等待线程大量增长.多线程

public synchronized void method(){
    otherMethod();
    needSyncMethod();
    otherMethod();
}

优化方法之一是,只在必要时进行同步,这样就能明显的减小线程持有锁的时间,提升系统吞吐量.并发

public void method(){
    otherMethod();
    synchronized(this){
        needSyncMethod();
    }
    otherMethod();
}

减少锁粒度

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

对于HashMap来讲,最重要的两个方法是put()和get()。一种最天然的想法就是对整个HashMap加锁,必然能够获得一个线程安全的对象。但这样作,就会致使加锁颗粒度太大。对于ConcurrentHashMap,它的内部进一步分了若干个小的HashMap,称之为(SEGMENT)。

默认状况下,一个ConcurrentHashMap进一步细分为16个段.若是增长表项,并非将整个HashMap加锁,而是首先根据hashcode获得该表项应该被放在哪一个段中,而后对该段加锁,完成put()操做.只要被加入的数据不存放在同一个表项,则多个线程的put()操做能够作到真正的并行.

因为默认16个段,因此ConcurrentHashMap最多能够同时接受16个线程同时插入(若是都不插入到不一样的段中),从而大大提供其吞吐量。

所谓减小锁粒度,就是指缩小锁定对象范围,从而减小锁冲突的可能性,进而提升系统的并发能力.

读写分离锁替换独占锁

使用读写锁ReadWriteLock能够提升系统性能.若是说减小锁粒度是经过分割数据结构实现的,那么读写锁则是对系统功能点的分割。在读多写少的场合使用读写锁能够有效替身系统的并发能力。 由于读操做自己是不会影响数据的完整性和一致性。因此讲道理应该能够容许多线同时读。

锁分离

将读写锁思想进一步延伸就是锁分离.读写锁依据读写操做功能上的不一样,进行了有效的锁分离。依据应用程序的功能特色,使用相似的分离思想,也能够对独占锁进行分离.一个典型的案例就是LinkedBlockingQueue的实现。take()和put()方法虽然都对队列进行了修改操做,但因为是链表,所以,两个操做分别做用于队列的前端和末尾,理论上二者并不冲突。使用独占锁,则要求在进行take和put操做时获取当前队列的独占锁,那么take和put酒不可能真正的并发,他们会彼此等待对方释放锁。在JDK的实现中,取而代之的是两把不一样的锁,分离了take和put操做.削弱了竞争的可能性.实现类取数据和写数据的分离,实现了真正意义上成为并发操做。

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    //take和put之间不存在锁竞争关系,只须要take和take之间,put和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();

    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 {
             while (count.get() == capacity) { //当队列满时,等待拿走数据后唤醒.
                    notFull.await();
                }
                enqueue(node);
                c = count.getAndIncrement(); //更新总数,count是加(getAndIncrement先获取当前值,再给当前值加1,返回旧值)
                if (c + 1 < capacity) //若是旧值+1 小于 队列长度
                    notFull.signal(); //唤醒等待的写入线程.继续写入.
        } finally {
            putLock.unlock();  //释放锁
        }
        if (c == 0) //take操做拿完数据后就一直在notEmpty等待,这个时候的count为0,而当put操做后,成功后就能够唤醒take操做继续执行了.而当队列中count不少时,这一步是不须要执行的.
            signalNotEmpty(); //唤醒在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) { //若是队列为0,等待
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement(); //先取原值,再减1
            if (c > 1) //若是队列大于1,本身继续执行.
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity) //当长度等于设定的队列长度,就唤醒take操做.
            signalNotFull();
        return x;
    }

    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

}

锁粗化

若是对一个锁不停地进行请求,同步和释放,其自己也会消耗系统宝贵的资源,反而不利于性能优化.虚拟机在遇到须要一连串对同一把锁不断进行请求和释放操做的状况时,便会把全部的锁操做整合成对锁的一次请求,从而减小对锁的请求同步次数,这就是锁的粗化.

public void demoMethod(){
    synchronized(lock){
        //doSth...
    }

    //其余不须要同步但很快完成的事情
    .....

    synchronized(lock){
        //doSth...
    }
}

整合以下:

public void demoMethod(){
    synchronized(lock){
        //doSth...

        //其余不须要同步但很快完成的事情
        .....
    }
}

在开发过程当中,你们也应该有意识地在合理的场合进行锁的粗化,尤为当在循环内请求锁时。如下是一个循环内的请求锁的例子,在这种状况下,意味着每次循环都有申请锁和释放锁的操做。但在这种状况下,显然是没有必要的。

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

因此,一种更合理的作法应该是在外层只请求一次锁:

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

  }
}

性能优化就是根据运行时的真实状况对各个资源点进行权衡折中的过程.锁粗化的思想和减小锁持有时间是相反的,但在不一样场合,他们的效果并不相同.因此你们要根据实际状况进行权衡.

Java虚拟机对锁优化所作的努力

锁偏向

偏向锁是一种针对加锁操做的优化手段,他的核心思想是:若是一个线程得到了锁,那么锁就进行偏向模式.当这个线程再次请求锁时,无需再作任何同步操做.这样就节省了大量操做锁的动做,从而提升程序性能.

所以,对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果.由于极有可能连续屡次是同一个线程请求相同的锁.而对于锁竞争激烈的程序,其效果不佳.

使用Java虚拟机参数:-XX:+UseBiasedLocking 能够开启偏向锁.

轻量级锁

若是偏向锁失败,虚拟机并不会当即挂起线程.它还会使用一种称为轻量级的锁的优化手段.轻量级锁只是简单的将对象头部做为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁.若是线程得到轻量锁成功,则能够顺利进入临界区.若是失败,则表示其余线程争抢到了锁,那么当前线程的锁请求就会膨胀为重量级锁.

自旋锁

锁膨胀后,虚拟机为了不线程真实的在操做系统层面挂起,虚拟机还作了最后的努力就是自旋锁.若是一个线程暂时没法得到索,有可能在几个CPU时钟周期后就能够获得锁,
那么简单粗暴的挂起线程多是得不偿失的操做.虚拟机会假设在很短期内线程是能够得到锁的,因此会让线程本身空循环(这即是自旋的含义),若是尝试若干次后,能够获得锁,那么久能够顺利进入临界区,
若是还得不到,才会真实地讲线程在操做系统层面挂起.

锁消除

锁消除是一种更完全的锁优化,Java虚拟机在JIT编译时,经过对运用上下文的扫描,去除不可能存在的共享资源竞争锁,节省毫无心义的资源开销.

咱们可能会问:若是不可能存在竞争,为何程序员还要加上锁呢?

在Java软件开发过程当中,咱们必然会用上一些JDK的内置API,好比StringBuffer、Vector等。你在使用这些类的时候,也许根本不会考虑这些对象到底内部是如何实现的。好比,你颇有可能在一个不可能存在并发竞争的场合使用Vector。而周所众知,Vector内部使用了synchronized请求锁,以下代码:

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

上述代码中的Vector,因为变量v只在createString()函数中使用,所以,它只是一个单纯的局部变量。局部变量是在线程栈上分配的,属于线程私有的数据,所以不可能被其余线程访问。因此,在这种状况下,Vector内部全部加锁同步都是没有必要的。若是虚拟机检测到这种状况,就会将这些无用的锁操做去除。

锁消除设计的一项关键技术是逃逸分析,就是观察某个变量是否会跳出某个做用域(好比对Vector的一些操做).在本例中,变量v显然没有逃出createString()函数以外。以次为基础,虚拟机才能够大胆将v内部逃逸出当前函数,也就是说v有可能被其余线程访问。若是是这样,虚拟机就不能消除v中的锁操做。

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

相关文章
相关标签/搜索