Java高并发下锁的优化详解
作者:爱coding的同学
简述
锁是最常用的同步方法之一。在高并发的环境下,激烈的锁竞争会导致程序的性能下降。
下面是一些关于锁的使用建议,可以把这种副作用降到最低。
减少锁持有时间
对于使用锁进行并发控制的应用程序而言,在锁竞争过程中,单个线程对锁的持有时间与系统性能有着直接的关系。
如果线程持有锁的时间很长,那么相对地,锁的竞争程序也就越激烈。
应该尽可能地减少对某个锁的占有时间,以减少程序间互斥的可能。
public synchronized void syncMethod() { otherCode1(); //无同步控制需要 needSynMethod(); //有同步控制需要 otherCode2(); //无同步控制需要 } public void syncMethod2() { otherCode1(); //无同步控制需要 synchronized(this) { needSynMethod(); //有同步控制需要 } otherCode2(); //无同步控制需要 }
说明:减少锁的持有时间有助于降低锁冲突的可能性,进而提升系统的并发能力。
减少锁粒度
减少锁粒度也是一种削弱多线程锁竞争的有效手段。这种技术典型的使用场景就是ConcurrentHashMap类的实现。ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁,可以简单理解成把一个大的HashTable分解成多个,形成了锁分离。而Hashtable的实现方式是—锁整个hash表。concurrentHashMap内部细分了若干个小的hashMap,称之为段(segment),默认情况下,被细分为16个段。新增的时候根据key的hashcode计算出应该存放到哪一个段中,然后对这个段枷锁,完成put()操作。就是说,最多可以同时接收16个线程同时插入(前提是16个不同段插入),从而大大提高吞吐量。但是,减少锁粒度会引入一个新的问题,即:当系统需要取得全局锁时,其消耗的资源会比较多。需要遍历每一个段,对每一个段进行加锁,最后还要对每一个段进行解锁。concurrentHashMap的size()方法会先使用无锁的方式求和,如果失败才会尝试这种加锁的方法。所以,在高并发场合,size()的性能差于同步的Hashmap,适用于size()调用少的场合。 说明:所谓减少锁粒度,就是指缩小锁定对象的范围,从而减少锁冲突的可能性,进行提供系统的并发能力。
读写分离锁来替换独占锁
ReadWriteLock读写分离锁替代独占锁是减少锁粒度的一种特殊情况。减少锁粒度是通过分割数据结构来实现的,而读写锁则是对系统功能点的分割。 读操作本身不会影响数据的完整性和一致性。因此,理论上讲,在大部分情况下,应该可以允许多想成同时读。 读写锁的访问约束情况:读-读(非阻塞)、读-写(阻塞)、写-读(阻塞)、写-写(阻塞)
public class ReadWriteLockTest2 { public static void main(String[] args) { //创建一个锁对象 ReadWriteLock lock = new ReentrantReadWriteLock(false); //创建一个线程池 ExecutorService pool = Executors.newFixedThreadPool(2); //创建一些并发访问 RWRun rw1 = new RWRun(lock,true); RWRun rw2 = new RWRun(lock,true); RWRun rw3 = new RWRun(lock,false); //在线程池中执行各个的操作 pool.execute(rw1); pool.execute(rw2); pool.execute(rw3); //关闭线程池 pool.shutdown(); } } class RWRun implements Runnable { private ReadWriteLock myLock; //执行操作所需的锁对象 private boolean ischeck; //是否查询 public RWRun(ReadWriteLock myLock, boolean ischeck) { super(); this.myLock = myLock; this.ischeck = ischeck; } public void run() { if (ischeck) { //获取读锁 myLock.readLock().lock(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //释放读锁 myLock.readLock().unlock(); } else { //获取写锁 myLock.writeLock().lock(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //释放写锁 myLock.writeLock().unlock(); } } }
上面执行完的时间为2秒钟,如果是独占锁 就需要3秒钟 结论:在读多写少的场合,使用读写锁可以有效提升系统的并发能力。
锁分离
在LinkedBlockingQueue的实现中,take()和put()分别实现了从队列中取得数据和往队列中增加数据的功能。
虽然两个函数都对当前队列进行了修改操作,但由于是基于链表的,因此,两个操作分别作用于队列的前端和尾端,从理论上来说,两者并不冲突。
所以在JDK中,采用了两把不同的锁,分离了toke()和put()的操作,实现了可并发的操作。
//take()函数需要持有的锁 private final ReentrantLock takeLock = new ReentrantLock(); private final Condition notEmpty = takeLock.newCondition(); //put()函数需要持有的锁 private final ReentrantLock putLock = new ReentrantLock(); private final Condition notFull = putLock.newCondition();
锁粗化
虚拟机在遇到一连串连续地对同一锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数,这个操作叫做锁的粗化。
例子:在循环内请求锁时。
for (int i =0 i < n; i++) { synchronized (lock) { doSomething(); } }
更加合理的做法应该是在外层只请求一次锁
到此这篇关于Java高并发下锁的优化详解的文章就介绍到这了,更多相关高并发下锁的优化内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!