锁是最常用的同步方法之一。在高并发的环境下,激烈的锁竞争会导致程序的性能下降。
对于单任务或者单线程的应用而言,其主要资源消耗都花在任务本身,它既不需要维护并行数据结构间的一致性状态,也不需要为线程的切换和调度花费时间。对于多线程应用来说,系统除了处理功能需求外,还需要额外维护多线程环境的特有信息,如线程本身的元数据、线程的调度、线程上下文的切换等。并行计算之所以能提高系统的性能,并不是因为它"少干活"了,而是因为并行计算可以更合理地进行任务调度,充分利用各个CPU资源。
如何提高锁性能减少锁持有时间对于使用锁进行并发控制的应用程序而言,在锁竞争过程中,单个线程对锁的持有时间与系统性能有着直接的关系。如果锁的持有锁时间越长,那么锁的竞争程度也就越激烈。
简单来讲就是:要个人填写信息表,但是只有一根笔,每个人如果没想好怎么填,那么每个人持有笔的时间就会很长,那么总的时间就会变长。
因此减少对某个锁的持有时间,以减少线程间互斥。例如下面这段代码:
publicsynchronizedvoidsynMethod(){method1();mainMethod();method2();}复制代码
上面那段代码中,只有mainMethod()方法需要做同步控制,而method1()和method2()不需要做同步控制,那么上面那段在高并发的情况下对整个方法都进行了同步控制,如果method1()和method2()两个方法的耗时长,那么会导致整个程序的执行时间变长。因此我们可以选择下面这样优化:
publicvoidsynMethod(){method1();synchronized(this){mainMethod();}method2();}复制代码
这样做的好处就是,只针对mainMethod()方法做了同步控制,锁占用的时间相对较短,因此能够有较高的并发度。较少锁的持有时间有助于降低锁冲突的可能性,进而提升系统的并发能力。
减小锁粒度减小锁粒度也是一种削弱多线程锁竞争的有效手段。这种技术典型的使用场景就是ConcurrentHashMap类的实现。对ConcurrentHashMap有所了解的小伙伴应该知道,传统的HashTable之所以是线程安全的就是因为它是对整个方法加锁。而ConcurrentHashMap的性能比较高是因为它内部细分了若干个小的HashMap,称之为段(SEGMENT)。在默认情况下,一个ConcurrentHashMap类可以细分为16个端,性能相当于提升了16倍。
在ConcurrentHashMap中增加一个数据,并不是对整个HashMap加锁,而是首先根据hashcode得出应该被存放在哪个段中,然后对该段加锁,并完成put()操作。当多个线程进行put()操作的时候,如果锁的不是同一个段,那么就可以实现并行操作。
但是,减小锁粒度会带来一个新的问题:当系统需要取得全局锁时,其消耗的资源会比较多。例如:当ConcurrentHashMap调用size()方法时,需要或者所有子段的锁。虽然事实上,size()方法会先使用无锁的方式求和,如果失败才会尝试这种方式,但是在高并发的情况下,ConcurrentHashMap的性能依然要弱于同步的HashMap。
减小锁粒度,就是指缩小锁定对象的范围,从而降低锁冲突的可能性,进而提高系统的并发能力
用读写锁来替换独占锁读写分离锁可以有效地帮助减少锁竞争,提高系统性能。比如:A1、A2、A3三个线程进行写操作,B1、B2、B3三个线程进行读操作,如果使用重入锁或者内部锁,那么所有读之间,读与写之间,写之间都是串行操作。但是因为读操作并不会造成数据的完整性破坏,因此这种等待是不合理的。
因此可以使用读写分离锁ReadWriteLock来提高系统的性能。使用示例如下:
锁粗化通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,在使用完公共资源后,应该立即释放锁,只有这样,等待在这个锁上的其他线程才能尽早地获得资源执行任务。
错误示例:
publicvoidsynMethod(){synchronized(this){method1();}synchronized(this){method2();}}复制代码
优化后:
publicvoidsynMethod(){synchronized(this){method1();method2();}}复制代码
尤其是在循环中要注意锁的粗化
错误示例:
publicvoidsynMethod(){for(inti=1;in;i++){synchronized(lock){//dosth...}}}复制代码
优化后:
synchronized(lock){for(inti=1;in;i++){//dosth...}}复制代码JVM进行的锁优化偏向锁
锁偏向是一种针对加锁操作的优化手段。核心思想:如果一个线程获得了一个锁,那么这个锁就进入了***偏向模式***,当这个线程释放完这个锁后,下次同其他线程再次请求时,无须在做任何同步操作。这样就节省了大量的锁申请相关操作。
但是在锁竞争比较激烈的场合,效果不佳,因为在竞争激烈的场合,最有可能的情况就是每次都是不同的线程来请求,这样偏向模式会失效,因此还不如不启用偏向锁。可以通过JVM参数-XX:+UseBiasedLocking开启偏向锁。
轻量级锁如果偏向锁失败,那么虚拟机并不会立即挂起线程,它还会使用一种称为轻量级锁的优化手段。轻量级锁的操作也很方便,它只是简单地将对象头部作为指针指向持有锁的线程堆栈的头部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,则可以顺利进入临界区,如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁。
自旋锁锁膨胀后,为了避免线程真实地在操作系统层面挂起,虚拟机还会做最后的努力——自旋锁。当前线程暂时获取不到锁,但是如果简单粗暴地将这个线程挂起是一种得不偿失的操作,因此虚拟机会让当前线程做几个空循环,在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区。
重量级锁如果经过自旋还不能获得锁,才会真的将线程在操作系统层面挂起,升级为重量级锁**
锁消除Java虚拟机在JIT编译时,会通过对运行上下文进行扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
publicString[]createArrays(){VectorIntegervector=newVector();for(inti=1;i;i++){vector.add(i);}returnvector.toArray(newString[]{});}复制代码
上面一段代码中,因为vector这个变量是定义在createArrays()这个方法中,是一个局部变量,在线程栈中分配的,属于线程私有的数据,因此不存在资源竞争的情况。而Vector内部所有加锁同步都是没有必要的,如果虚拟机检测到这种情况,就会将这些无用的锁操作去除。
锁消除涉及的一项关键技术为逃逸分析,所谓逃逸分析就是观察某一个变量是否会逃出某一个作用域。在上面例子中,变量vector没有逃出createArrays()这个函数的方位,因此虚拟机才会就将这个变量的加锁操作去除。如果createArrays()返回的不是String数组,而是vector本身,那么就认为变量vector逃出了当前函数,会被其他线程所访问到。例如下面代码:
publicVectorIntegercreateList(){VectorIntegervector=newVector();for(inti=1;i;i++){vector.add(i);}returnvector;}复制代码ThreadLocal
除了控制资源的访问外,我们还可以通过增加资源来保证所有对象的线程安全。简单来讲就是:要个人填写信息表,我们可以分配根笔给他们填写,人手一根,那么填写的速度也将大大增加。
上面这个代码,如果没有同步控制则会出现java.lang.NumberFormatException:multiplepoints和java.lang.NumberFormatException:Forinputstring:""异常,因为SimpleDateFormat不是线程安全的,除非加锁控制。但是除了加锁我们还有没有其他方法呢,答案是有的,那就是使用ThreadLocal,每个线程分配一个SimpleDateFormat。
为每一个线程分配不同的对象,需要在应用层面保证ThreadLocal只起到了简单的容器作用
ThreadLocal的实现原理set()方法:
publicvoidset(Tvalue){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);}复制代码
先获取当前线程对象,然后通过getMap()方法拿到线程的ThreadLocalMap,并将值存入ThreadLocalMap中。可以简单把ThreadLocalMap理解为一个Map,其中key为当前线程对象,value便是我们所需要的值。
get()方法:
publicTget(){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){ThreadLocalMap.Entrye=map.getEntry(this);if(e!=null){
SuppressWarnings("unchecked")Tresult=(T)e.value;returnresult;}}returnsetInitialValue();}复制代码先获取到当前线程的ThreadLocalMap,然后通过将自己作为key取得内部的实际数据
如果希望及时回收对象,我们应该使用ThreadLocal.remove()方法将这个变量移除,否则如果将一些大的对象设置到ThreadLocal中,没有及时回收,会造成内存泄漏的可能。
无锁锁分为乐观锁和悲观锁,而无锁就是一种乐观的策略,它是使用一种叫比较并交换(CAS,CompareAndSwap)的技术来鉴别线程冲突,一旦检测到冲突发生,就重试当前操作直到没有冲突为止。
比较并交换CAS的算法过程是:包含三个参数CAS(V,E,N),其中V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V值设置为N值。最后返回当前V的真实值。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其他均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
线程安全整数(AtomicInteger)AtomicInteger是在JDK并发包中的atomic中的,可以把它看作一个整数,与Integer不同的是,它是可变的,并且是线程安全的。对其进行修改等任何操作都是用CAS指令进行的。下面是AtomicInteger的常用方法:
publicfinalintget()//取得当前值publicfinalvoidset(intnewValue)//设置当前值publicfinalintgetAndSet(intnewValue)//设置新值,并返回旧值publicfinalboolean