并发环境下进行程式设计时,需要使用锁机制来同步多执行绪间的操作,保证共享资源的互斥访问。
加锁会带来效能上的损坏,似乎是众所周知的事情。
然而,加锁本身不会带来多少的效能消耗,效能主要是线上程的获取锁的过程。
如果只有一个执行绪竞争锁,此时并不存在多执行绪竞争的情况,那么JVM会进行优化,那么这时加锁带来的效能消耗基本可以忽略。
因此,规范加锁的操作,优化锁的使用方法,避免不必要的执行绪竞争,不仅可以提高程式效能,也能避免不规范加锁可能造成执行绪死锁问题,提高程式健壮性。
下面阐述几种锁优化的思路。
01
尽量不要锁住方法
在普通成员函式上加锁时,执行绪获得的是该方法所在物件的物件锁。此时整个物件都会被锁住。
这也意味着,如果这个物件提供的多个同步方法是针对不同业务的,那么由于整个物件被锁住,一个业务业务在处理时,其他不相关的业务执行绪也必须wait。
下面的例子展示了这种情况:
LockMethod类包含两个同步方法,分别在两种业务处理中被呼叫:
public class LockMethod {
public synchronized void busyA {
for (int i = 0; i
System.out.println(Thread.currentThread.getName + deal with bussiness A:+i);
}
}
public synchronized void busyB {
for (int i = 0; i
System.out.println(Thread.currentThread.getName + deal with bussiness B:+i);
}
}
}
BusyA是执行绪类,用来处理A业务,呼叫的是LockMethod的busyA方法:
public class BusyA extends Thread {
LockMethod lockMethod;
void deal(LockMethod lockMethod){
this.lockMethod = lockMethod;
}
@Override
public void run {
super.run;
lockMethod.busyA;
}
}
BusyB是执行绪类,用来处理B业务,呼叫的是LockMethod的busyB方法:
public class BusyB extends Thread {
LockMethod lockMethod;
void deal(LockMethod lockMethod){
this.lockMethod = lockMethod;
}
@Override
public void run {
super.run;
lockMethod.busyB;
}
}
TestLockMethod类,使用执行绪BusyA与BusyB进行业务处理:
public class TestLockMethod
public static void main(String args) {
LockMethod lockMethod = new LockMethod;
BUSSA bussa = new BUSSA;
BUSSB bussb = new BUSSB;
bussa.deal(lockMethod);
bussb.deal(lockMethod);
bussa.start;
bussb.start;
}
}
执行程式,可以看到线上程bussa 执行的过程中,bussb是不能够进入函式 busyB的,因为此时lockMethod 的物件锁被执行绪bussa获取了。
02
缩小同步程式码块,只锁资料
有时候为了程式设计方便,有些人会synchnoized很大的一块程式码。
如果这个程式码块中的某些操作与共享资源并不相关,那么应当把它们放到同步块外部,避免长时间的持有锁,造成其他执行绪一直处于等待状态。
尤其是一些循环操作、同步I/O操作。不止是在程式码的行数范围上缩小同步块,在执行逻辑上,也应该缩小同步块。
例如多加一些条件判断,符合条件的再进行同步,而不是同步之后再进行条件判断,尽量减少不必要的进入同步块的逻辑。
03
锁中尽量不要再包含锁
这种情况经常发生,执行绪在得到了A锁之后,在同步方法块中呼叫了另外物件的同步方法,获得了第二个锁.
这样可能导致一个呼叫堆叠中有多把锁的请求,多执行绪情况下可能会出现很复杂、难以分析的异常情况,导致死锁的发生。
下面的程式码显示了这种情况:
synchronized(A){
synchronized(B){
}
}
或是在同步块中呼叫了同步方法:
synchronized(A){
B b = objArrayList.get(0);
b.method; //这是一个同步方法
}
解决的办法是跳出来加锁,不要包含加锁:
{
B b = null;
synchronized(A){
b = objArrayList.get(0);
}
b.method;
}
04
将锁私有化,在内部管理锁
把锁作为一个私有的物件,外部不能拿到这个物件,更安全一些。
物件可能被其他执行绪直接进行加锁操作,此时执行绪便持有了该物件的物件锁。
例如下面这种情况:
class A {
public void method1 {
}
}
class B {
public void method1 {
A a = new A;
synchronized (a) { //直接进行加锁
a.method1;
}
}
}
这种使用方式下,物件a的物件锁被外部所持有,让这把锁在外部多个地方被使用是比较危险的,对程式码的逻辑流程阅读也造成困扰。
一种更好的方式是在类的内部自己管理锁,外部需要同步方案时,也是通过界面方式来提供同步操作:
class A {
private Object lock = new Object;
public void method1 {
synchronized (lock){
}
}
}
class B {
public void method1 {
A a = new A;
a.method1;
}
}
05
进行适当的锁分解
考虑下面这段程式:
public class GameServer {
public Map> tables = new HashMap>;
public void join(Player player, Table table) {
if (player.getAccountBalance > table.getLimit) {
synchronized (tables) {
List tablePlayers = tables.get(table.getId);
if (tablePlayers.size
tablePlayers.add(player);
}
}
}
}
public void leave(Player player, Table table) {/*省略*/}
public void createTable {/*省略*/}