Java并发优化思路

一、并发优化

1.1、Java高并发包所采用的几个机制

(1)、CAS(乐观操作)
       jdk5以前采用synchronized,对共享区域进行同步操作,synchronized是重的操作,在高并发情况下,会引起线程频繁切换;而CAS是一种乐观锁机制,compare and swap,不加锁,而是假设没有冲突去完成,若有冲突会重试(非阻塞)。compare&swap是原子操作,基于CPU的原语操作。
       CAS仍然存在三大问题:ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作
(2)、Volatile(变量的可见性)
       VM阻止volatile变量的值放入处理器的寄存器,在写入值以后会被从处理器的cache中flush掉,写到内存中去,这样其他线程都可以立刻看到该变量的变化。
(3)、AQS,抽象队列同步器(原子性操作状态同步位、有序队列、阻塞唤醒进程)
       获取锁:首先判断当前状态是否允许获取锁,如果是就获取锁,否则就阻塞操作或者获取失败,也就是说如果是独占锁就可能阻塞,如果是共享锁就可能失败。另外如果是阻塞线程,那么线程就需要进入阻塞队列。当状态位允许获取锁时就修改状态,并且如果进了队列就从队列中移除。
       释放锁:这个过程就是修改状态位,如果有线程因为状态位阻塞的话就唤醒队列中的一个或者更多线程。

1.2、锁优化

       今天所说的锁优化,是指在阻塞式的情况下,如何让性能不要变得太差。但是再怎么优化,一般来说性能都会比无锁的情况差一点。
       锁优化的思路和方法总结一下,有以下几种:
(1)、减少锁持有时间

       减少其他线程等待的时间,只在有线程安全要求的程序上加锁

(2)、减小锁粒度

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

(3)、锁分离

       最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能;读写分离思想可以延伸,只要操作互不影响,锁就可以分离;比如LinkedBlockingQueue

(4)、锁粗化

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

(5)、锁消除

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

       锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。
       也许你会觉得奇怪,既然有些对象不可能被多线程访问,那为什么要加锁呢?写代码时直接不加锁不就好了?
       有时,这些锁并不是程序员所写的,有的是JDK实现中就有锁的,比如Vector和StringBuffer这样的类,它们中的很多方法都是有锁的。当我们在一些不会有线程安全的情况下使用这些类的方法时,达到某些条件时,编译器会将锁消除来提高性能
       开启锁消除是在JVM参数上设置的,当然需要在server模式下:

#开启锁消除
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

1.3、无锁操作

       与锁相比, 使用CAS操作, 由于其非阻塞性, 因此不存在死锁问题, 同时线程之间的相互影响, 也远小于锁的方式. 使用无锁的方案, 可以减少锁竞争以及线程频繁调度带来的系统开销.
       例如:生产消费者模型中, 可以使用BlockingQueue来作为内存缓冲区,但他是基于锁和阻塞实现的线程同步。如果想要在高并发场合下获取更好的性能,则可以使用基于CAS的ConcurrentLinkedQueue;同理,如果可以使用CAS方式实现整个生产消费者模型,那么也将获得可观的性能提升,如Disruptor框架.

1.4、虚拟机内的锁优化

(1)、 偏向锁

       所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程
       大部分情况是没有竞争的(某个同步块大多数情况都不会出现多线程同时竞争锁),所以可以通过偏向来提高性能。即在无竞争时,之前获得锁的线程再次获得锁时,会判断是否偏向锁指向我,那么该线程将不用再次获得锁,直接就可以进入同步块
       偏向锁的实施就是将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark
       当其他线程请求相同的锁时,偏向模式结束
       JVM默认启用偏向锁:-XX:+UseBiasedLocking
       在竞争激烈的场合,偏向锁会增加系统负担(每次都要加一次是否偏向的判断

(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互斥量产生的性能损耗。在竞争非常激烈时(轻量级锁总是失败),轻量级锁会多做很多额外操作,导致性能下降。

(3)、自旋锁

       当竞争存在时,因为轻量级锁尝试失败,之后有可能会直接升级成重量级锁动用操作系统层面的互斥。也有可能再尝试一下自旋锁。
       如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋),并且不停地尝试拿到这个锁(类似tryLock),当然循环的次数是有限制的,当循环次数达到以后,仍然升级成重量级锁。所以在每个线程对于锁的持有时间很少时,自旋锁能够尽量避免线程在OS层被挂起。
       JDK1.6中:-XX:+UseSpinning开启
       JDK1.7中:去掉此参数,改为内置实现
       如果同步块很长,自旋失败,会降低系统性能。如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能

(4)、偏向锁,轻量级锁,自旋锁总结

       上述的锁不是Java语言层面的锁优化方法,是内置在JVM当中的。
       偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁。
       轻量级锁和自旋锁是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作。
       为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁。偏向锁,轻量级锁,自旋锁都是乐观锁。

发布了12 篇原创文章 · 获赞 19 · 访问量 8724

猜你喜欢

转载自blog.csdn.net/liuhuiteng/article/details/88727509
今日推荐