锁与并发

来源:互联网 发布:ucloud vs 阿里云 编辑:程序博客网 时间:2024/05/16 05:29

1.锁在Java虚拟机中的实现和优化

1.1 偏向锁

1.        偏向锁是JDK1.6提出的一种锁优化方式。其核心思想是,如果程序没有竞争,则取消之前已经取得锁的线程同步操作。也就是,若某一锁被线程获取后,便进入偏向模式,当线程再次请求锁时,无需进入相关的同步操作,从而节省了操作时间。如果在此之间有其他线程进行了锁请求,则锁退出偏向模式。在JVM中使用-XX:+UserBiasedLocking可以设置启用偏向锁。

2.        偏向锁在锁竞争激烈的场合没有太强的优化效果,因为大量的竞争会导致持有锁的线程不停地切换,锁也很难一直保持在偏向模式,此时,使用锁偏向不仅得不到性能的优化,反而有可能降低系统性能。因此,在激烈竞争的场合,可以尝试使用-XX:-UseBiasedLocking参数禁用偏向锁。

1.2轻量级锁

如果偏向锁失败,Java虚拟机会让线程申请轻量级锁。轻量级锁在虚拟机内部,使用一个称为BasicObjectLock的对象实现,这个对象内部由一个Basiclock对象和一个持有该锁的Java对象指针组成。BasicObjectLock对象放置在Java栈的线帧中。在BasicLock对象内部还维护着displaced_header字段,它用于备份对象头部的Mark Word。

1.3 锁膨胀

当轻量级锁失败,虚拟机就会使用重量级锁。

1.4 自旋锁

1.        在锁膨胀之后,虚拟机会做最后的被挂起,这样线程上下文切换的性能损失就比较大。因此,在锁膨胀之后,虚拟机会做最后的争取,希望线程可以尽快进入临界区而避免被操作系统挂起。一种较为有效的手段就是会用自旋锁。

2.        自旋倘可以使线程在没有取得锁时,不被挂起,成而转而去执行一个空循环(即所谓的自旋),在若干个空循环后,线程如果可以获得锁,则继续执行。若干依然不能获得锁,才会被挂起。

3.        使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用时间长的并发程序,自旋锁在自旋等待后,往往依然无法获得对应的锁,不仅仅白白浪费了CPU时间,最终还是免不了执行被挂起的操作,反而浪费了系统资源。

1.5锁消除

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

2.锁在应用层的优化思路

2.1 减少锁持有时间

对于使用锁进行并发控制的应用程序而言,在锁竞争过程中,单个线程对锁的持有时间与系统性能有着直接的关系。如果线程持有锁的时间很长,那么相对的,锁的竞争度也就越激烈。因此,在程序开发过程中,应该尽可能地减少对某个锁的占有时间,以减少线程间互斥的可能。

例:


2.2减少锁粒度

1.        减少锁粒度也是一种锁削弱多线程的有效手段。这种技术典型的使用场景就是ConcurrentHashMap类的实现。对一个普通的集合对象的多线程同步来说,最常使用的方式就是对get()和add()方法进行同步。每当对集合进行add()操作或者get()操作时,总是获得集合对象的锁。因此,事实上没有两个线程可以做到真正的并发,任何线程在执行这些同步方法时,总要等待前一个线程执行完毕。在高并发时,激烈的锁竞争会影响系统的吞吐量。

2.        默认情况下,ConcurrentHashMap拥有16个段,因此,如果够幸运的话,ConcurrentHashMap可以同时接受16个线程同时插入(如果都插入不同的段中),从而大大提高其吞吐量。如图,显示了6个线程同时访问对ConcurrentHashMap进行访问,此时,线程1、2、3分别访问段4、5、6也需要访问1、2、3,则必须等待前面的线程结束访问才能进入ConcurrentHashMap。


2.3锁分离

1.        锁分离是减小锁粒度的一个特例,它依据应用程序的功能特点,将一个独占锁分成多个锁。一个典型的案例就是java.util.current.LinkedBlockingQueue的实现。

2.        在LinkedBlockingQueue的实现中,take()函数和put()函数分别实现了从队列中取得数据和往队列中增加数据的功能。虽然两个函数都对当前队列进行了修改操作,但从理论上说,两者并不冲突。如果使用独占锁,则要求在两个操作进行时取当前队列的独占锁,那么take()和put()操作就不可能真正并发,在运行时,它们会彼此行等待对方释放锁资源。在这种情况下,锁竞争会相对比较激烈,从而影响程序在高并发时的性能。


2.4锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早地获得资源执行任务。但是凡事都有一个度,如果对同一个锁不停地进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。为此,虚拟机在遇到一连串连续地对同一锁不断进行请求和释放的操作,便会把所有的锁操作整合成对锁对一次请求,从而减少对锁的请求同步次数。这个操作叫做锁的粗化。

3. 无锁

3.1 理解CAS

1.        CAS(Compare AndSwap)算法的无锁并发控制方法。与锁的实现相比,无锁算法的设计和实现都要复杂得多,但由于其非阻塞性,它对死锁问题先天免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。

2.        CAS算法的过程是这样:它包含3个参数CAS(V,E,V)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试。当然允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

4. 理解Javam内存模型

4.1原子性

原子性中的原子代表不可分割的意思。原子操作是不可中断的,也不能被多线程干扰。比如,对int和byte等数据的赋值操作就具备基本的原子特性,而像“a++”这样的操作不具备原子性,因为它涉及读取a、计算新值和写入a三步操作,中间有可能被其他线程干扰,导致最终计算结果和实际值出现偏差。

4.2 有序性

现在的CPU都支持指令流水线执行。为了保证流水线的顺畅执行,在指令执行时,有可能会对目标指令进行重排。重排不会导致单线程中的语义修改,但会导致多线程中的语义出现不一致。即,在一个线程中观察另外一个线程的操作,我们会发现,被观察线程的指令顺序和预期情况不符。

4.3 可见性

可见性指当一个线程修改了一个变量的值,在另外一个线程中可以马上得知这个悠修改。

4.4 Happens-Before原则

a)        程序顺序原则:一个线程内保证语义的串行性。

b)        volatitle规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性。

c)        锁规则:解锁(unlock)必然发生在随后的加锁(lock)前。

d)        传递性:A先于B先于C,那么A必然等于C。

e)        线程的start()方法先于它的每一个动作。

f)         线程的所有操作先于它的每一个动作。

g)        线程的所有操作先于线程的终结(Thread.join())。

h)        线程的中断(interrupt())先于被中断线程的代码。

i)          对象的构造函数执行结束先于finalize()方法。




0 0