- 锁的优化
1.减小锁的持有时间
此处略
2.减小锁粒度
这种技术的典型的使用场景就是ConcurrentHashMap类的实现。它内部细分了若干个小的HashMap,称之为段(SEGMENT),默认情况下,一个ConcurrentHashMap被进一步细分为16个段。如果需要在ConcurrentHashMap中增加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放到哪个段中,然后对该段加锁,并完成put()操作。在多线程环境中,如果多个线程同时进行put()操作,只要被加入的表项不存放在同一个段中,则线程便可以做到真正的并行。由于默认有16个段,因此,可以同时接受16个线程同时插入(如果都插入不同的段中),从而大大提高吞吐量。下面的代码显示put()操作的过程。
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;//根据key,获得对应的段的序号
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j); //得到段,然后将数据插入给定的段中
return s.put(key, hash, value, false);
}
减小锁粒度会引入一个新问题:当系统需要取得全局锁时,其消耗的资源会比较多。虽然ConcurrentHashMap的pull()方法很好的分离了锁,但是当试图访问ConcurrentHashMap全局信息时,就会需要同时取得所有段的锁方法能顺利实施。比如ConcurrentHashMap的size()方法,它将返回ConcurrentHashMap的全部有效表项之和。要获取这个信息需要取得所有子段的锁,size()代码如下:
sum=0;
for(int i=0;i<segments.length;i++) //对所有的段加锁
segments[i].lock();
for(int i=0;i<segments.length;i++) //统计总数
sum+=segments[i].count;
for(int i=0;i<segments.length;i++) //释放所有的锁
segments[i].unlock();
在高并发的场景下,ConcurrentHashMap的size()性能要差与同步的HashMap。所谓减少锁粒度,就是指缩小锁定范围,从而减少锁冲突的可能性,进而提高系统的并发能力。
3.读写锁分离
减少锁粒度是通过分割数据结构实现的,那么读写锁则是多系统功能点的分割。在读多写少的场景下,使用读写锁可以有效提升系统的并发能力。
例如LinkedBlockingQueue的实现中,take()和put()分别实现了从队列中取得数据和往队列中增加数据的功能,但是这两个操作分别作用于队列的前端和尾端,用锁分离并不冲突。
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);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly(); //不能有两个线程同时进行put()
try {
while (count.get() == capacity) { //如果队列满了
notFull.await(); //等待
}
enqueue(node); //插入数据
c = count.getAndIncrement();//更新总数,变量c是count加1前的值
if (c + 1 < capacity)
notFull.signal(); //有足够的空间,通知其他线程
} finally {
putLock.unlock();//释放锁
}
if (c == 0)
signalNotEmpty();//插入成功后,通知take()操作取数据
}
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) {//如果当前没有可用数据,一直等待
notEmpty.await();//等待,put()操作的通知
}
x = dequeue(); //取得第一个数据
c = count.getAndDecrement();//数量减1,原子操作,因为会和put()同时访问count.注意c是count减1前
//的值
if (c > 1)
notEmpty.signal();//通知其他take()操作
} finally {
takeLock.unlock();//锁释放
}
if (c == capacity)
signalNotFull();//通知put()操作,已有空余空间
return x;
}
4.锁粗化
public void Method(){
synchronized (lock) {
// do sth.
}
//做其他不需要的同步的工作,但能很快执行完毕
synchronized (lock) {
// do sth.
}
}
被整合成如下形式:
public void Method(){
//整合成一次锁请求
synchronized (lock) {
//do sth.
// 做其他不需要同步的工作,但能很快的执行完毕
}
}
for(int i=0;i<CIRCLE;i++){
synchronized (lock) {
}
}
//更加合理的方式是
synchronized {
for(int i=0;i<CIRCLE;i++){
}
}