1.减少锁持有的时间
对于使用锁进行并发控制的应用程序而言,在锁的竞争过程中,单个线程对锁的持有时间与系统性能有着直接的关系。如果线程持有锁的时间越长,那么相对的,锁的竞争也就越激烈。比如,要求100个人填写自己的个人信息,但是只有1支笔,每个人拿着笔的时间都很长,那么总体花费的时间也就很长。如果真的只有一支笔,那么最好让每个人花尽可能少的时间持笔,务必做到想好了再拿笔写,而不能拿着笔思考该怎么想。程序开发也是类似的,应尽可能地减少对某个锁的持有时间,以减少线程间互斥的可能。比如在以下代码中
public synchronized void syncMethod(){
othercode1();
mutextMethod();
othercode2();
}
在上述方法中,othercode1和othercode2都是重量级方法,需要花费较长的CPU时间,但是只有mutextMethod方法是有同步需要的。在并发量较大的时候,使用这种对整个方法做同步的方法,则会导致等待线程大量增加。因为一个线程,在进入该方法时,需要获得内部锁,只有在整个任务执行完以后,才会释放锁。
一个较为优化的方法是,只在必要的时候进行同步,这样能够明显减少锁的持有时间,提高系统的吞吐量。
public void syncMethod(){
othercode1();
synchronized (this){
mutextMethod();
}
othercode2();
}
在上述代码中,只针对需要同步的方法使用内部锁加锁,锁占用的时间相对较短,因此有更高的并行度。
2.减少锁的粒度
将单个独占锁变为多个锁,从而将加速请求均分到多个锁上,有效降低对锁的竞争。但是,增加锁的前提是多线程访问的变量间相互独立,如果多线程需要同时访问多个变量,则很难进行锁分解,因为要维持原子性。
3.用读写分离锁来代替独占锁
使用读写分离锁ReadWriteLock可以提高系统的性能。使用读写分离锁来替代独占锁是减小锁粒度的一种特殊情况。如果说减小锁粒度是通过分割数据结构实现的,那么读写分离锁则是针对系统功能点的分割。
在读多写少的场合,使用读写分离锁可以有效提升系统的并发能力。
4.锁分离
如果将读写锁的思想进一步延申,就是锁分离。读写锁根据读写操作功能上的不同,进行了有效的锁分离。依据应用程序的特点,使用类似的分离思想,也可以对独占锁进行分离。典型的案例就是LinkedBlockingQueue的实现。
在LinkedBlockingQueue的实现中,take函数和put函数分别实现了从队列中取数据和往队列中增加数据的功能。虽然两个函数都是对当前队列进行了修改操作,但是LinkedBlockingQueue是基于链表的,因此两个操作分别用于队列的前端和尾端,从理论上讲,并不冲突。
如果使用独占锁,则要求在两个操作进行时分别获取当前队列的独占锁,那么take和put方法就不能真正的并发,在运行时,它们会彼此等待对方释放锁资源。这种情况下,锁的竞争会比较激烈,从而影响并发时的性能。
因此,在JDK实现中,并没有采用这种方式,取而代之的时用两把不同的锁分离了take和put方法。
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();
以上代码片段定义了takeLock和putLock,分别在take和put方法中使用。
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>(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方法可以取数据了
}
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();
}
x = dequeue(); //取数据
c = count.getAndDecrement(); //数量减1,原子操作,因为会和put函数同时访问count,变量c时cout减1前的值
if (c > 1)
notEmpty.signal(); //通知其他take方法
} finally {
takeLock.unlock(); //释放锁
}
if (c == capacity)
signalNotFull(); //通知put已有空余空间
return x;
}
5.锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早地获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停地进行请求,同步和释放,其本身也会消耗宝贵地资源,反而不利于优化。
为此,虚拟机在遇到一连串连续对同一个锁不断进行请求和释放操作时,便会把所有地锁操作整合成对锁地一次请求,从而减少对锁的请求同步次数,这个操作叫锁粗化。
public void syncMethod(){
synchronized (lock){
//do sth
}
//做其他不需要同步的工作,但不会消耗很长的cpu执行时间
synchronized (lock){
//do sth
}
}
上述代码会被整合成如下形式:
public void syncMethod(){
//整合成一次锁请求
synchronized (lock){
//do sth
//做其他不需要同步的工作,但不会消耗很长的cpu执行时间
}
}
在开发过程中,应有意识地在合理场合进行锁地粗化,尤其是在循环内请求锁的时候。