1、减小锁持有时间
它的核心思想就是不要在不必需要的代码上加锁,因为线程持有的锁时间过长,相对地,锁的竞争程度也就越激烈。
看下面的代码:
public synchronized void syncMethod() {
othercode1();
mutextMethod();
othercode2();
}
public void syncMethod() {
othercode1();
synchronized (this) {
mutextMethod();
}
othercode2();
}
2、减小锁锁粒度
减小锁粒度也是一种削弱多线程锁竞争的有效手段。典型的使用场景就是ConcurrentHashMap。
对于HashMap来说,最重要的两个方法就是get和put方法。一种最自然的想法就是对整个HashMap加锁,必然可以得到一个线程安全的对象。但是这样做,加锁的粒度太大,对于ConcurrentHashMap,它内部进一步细分了若干个小的HashMap,称之为段 segment。默认情况下ConcurrentHashMap,被进一步细分为16个段。
如果需要在ConcurentHashMap中增加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放到那个段中,然后对该段加锁,并完成put操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间便可以做到真正的并行。
@SuppressWarnings("unchecked")
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;
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);
}
3、 读写分离锁来替换独占锁
使用读写锁ReadWriteLock可以提供系统的性能。
4、 锁分离
如果将读写锁的思想做进一步的延伸,就是锁分离。典型的案例就是LinkedBlockingQueue的实现。
如果使用独占锁,则要求在两个操作进行时,获取当前队列的独占锁,那么take和put操作就不可能真正的并发,在运行时,它们会彼此等待对方释放锁资源。在这种情况下,锁竞争会相对比较激烈,从而影响程序在高并发的性能。
因此在JDK的实现中,并没有采用这样的方式,取而代之的是把两把不同的锁,分离了take和put操作。
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger(0);
下面来看下take的源码:
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();//只能有一个线程take元素。
try {
while (count.get() == 0) {// 没有数据时,线程进入等待状态。
notEmpty.await();
}
x = dequeue(); // 获取一个元素
c = count.getAndDecrement();//元素的数量减一
if (c > 1)
notEmpty.signal();// 大于1,唤醒其他线程进行take操作。
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
下面看 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();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {// 队列已经满了
notFull.await();
}
enqueue(node); // 存放一个元素
c = count.getAndIncrement();//数量加一
if (c + 1 < capacity)// 还有空间,唤醒其他的线程进行put操作。
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
、
5 、锁粗化
虚拟机在遇到一连串连续地对同一锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数,这个操作叫做锁的粗化。
看下面的代码就清楚了
public void syncMethod() {
synchronized (this) {
mutextMethod2();
}
synchronized (this) {
mutextMethod2();
}
}
public void syncMethod() {
synchronized (this) {
mutextMethod1();
mutextMethod2();
}
}