可能我们在开发项目进行过程中,通常会冒出这样的困惑:应该选择效率,还是选择质量?会不会有偷懒的思维,觉得把一些摸不清头绪,不知道怎么写的代码片段去掉,可以节省许多时间,更早的完成项目计划,其实以前我也是这么想的,但最近我开始意识到,这个问题的纠结之处不在于选择困难,而在于问题本身是个伪命题。
什么是“质量”呢?对于我们程序员来说可能是测试通过率,变量命名,代码格式化,组件化,查找bug,程序测试等。也有可能是程序的可扩展性,服务延时,产品功能的完整程度。前一种围绕代码的问题可以看成“代码质量”问题,第二种可以看成“执行质量”。
从“代码质量”来看,走捷径的偷懒思维,其实是种十分短视的做法。含糊绕过某个问题,你可能一时觉得省事不少,但到头来,往往发现因此搅乱了系统而要花费更多的时间来一行行的检查代码,找出bug,甚至重新调整整理逻辑框架。所以牺牲代码质量换取速度通常是得不偿失的。
相反地,高质量的代码其实是可以帮助你节省时间的。统一代码规范和变量命名,不仅可以帮到别的程序员,还可以帮到以后的你,更好地理解你现在写下的代码,经过严密思考而设计出的轻量级代码架构,则可以让你在迭代产品的时候获得更高的效率,更清晰地了解该从何入手,而不是到数据库里找需要替代的地方,而高测试通过率还可以给你充足的自信去调整代码,减少bug数量,减少QA时间。
这是转账线程类中run方法(随机选择一个目标账户和一个随机账户调用转账方法,然后睡眠):
当它运行时,我们不清楚某一时间某一个账户有多少钱,但是,所有账户的总金额应该是保持不变的。
TransferRunnable类:
这一结构确保任何时刻只有一个线程进入。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
再次运行程序后,发现不会出现错误,线程之间不会互相影响了。
什么是“质量”呢?对于我们程序员来说可能是测试通过率,变量命名,代码格式化,组件化,查找bug,程序测试等。也有可能是程序的可扩展性,服务延时,产品功能的完整程度。前一种围绕代码的问题可以看成“代码质量”问题,第二种可以看成“执行质量”。
从“代码质量”来看,走捷径的偷懒思维,其实是种十分短视的做法。含糊绕过某个问题,你可能一时觉得省事不少,但到头来,往往发现因此搅乱了系统而要花费更多的时间来一行行的检查代码,找出bug,甚至重新调整整理逻辑框架。所以牺牲代码质量换取速度通常是得不偿失的。
相反地,高质量的代码其实是可以帮助你节省时间的。统一代码规范和变量命名,不仅可以帮到别的程序员,还可以帮到以后的你,更好地理解你现在写下的代码,经过严密思考而设计出的轻量级代码架构,则可以让你在迭代产品的时候获得更高的效率,更清晰地了解该从何入手,而不是到数据库里找需要替代的地方,而高测试通过率还可以给你充足的自信去调整代码,减少bug数量,减少QA时间。
我们现阶段还是注重好代码的质量,执行质量就不多说啦,当然这些都是题外话,想和各位分享!接下来还是进入正题。
什么是竞争条件
在大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,将会发生什么?可能就会产生错误的对象。这样的情况通常称为竞争条件。
我们来模拟一个有若干账户的银行,随机在这些账户之间转账,每一个账户一个线程。每一次交易,会从线程所服务的账户中随机转移一定的金额到另一个账户中。
这是Bank类中转账的方法:
public void transfer(int from, int to, double amount) {
if (accounts[from] < amount) {
return;
}
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("转账金额: %10.2f 转出账户: %d 转入账户: %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" 最后的金额: %10.2f%n", getTotalBalance());
}
这是转账线程类中run方法(随机选择一个目标账户和一个随机账户调用转账方法,然后睡眠):
@Override
public void run() {
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = maxAmount * Math.random();
bank.transfer(fromAccount, toAccount, amount);
try {
Thread.sleep((int) (DELAY * Math.random()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
当它运行时,我们不清楚某一时间某一个账户有多少钱,但是,所有账户的总金额应该是保持不变的。
下面是例子的完整代码:
Bank类:
/**
* @author XzZhao
*/
public class Bank {
private final double[] accounts;
public Bank(int n, double initialBalance) {
accounts = new double[n];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = initialBalance;
}
}
public void transfer(int from, int to, double amount) {
if (accounts[from] < amount) {
return;
}
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("转账金额: %10.2f 转出账户: %d 转入账户: %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" 最后的金额: %10.2f%n", getTotalBalance());
}
public double getTotalBalance() {
double sum = 0;
for (double a : accounts) {
sum += a;
}
return sum;
}
public int size() {
return accounts.length;
}
}
TransferRunnable类:
/**
* @author XzZhao
*/
public class TransferRunnable implements Runnable {
private final Bank bank;
private final int fromAccount;
private final double maxAmount;
private final int DELAY = 10;
public TransferRunnable(Bank bank, int fromAccount, double maxAmount) {
super();
this.bank = bank;
this.fromAccount = fromAccount;
this.maxAmount = maxAmount;
}
@Override
public void run() {
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = maxAmount * Math.random();
bank.transfer(fromAccount, toAccount, amount);
try {
Thread.sleep((int) (DELAY * Math.random()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
Test:
/**
* @author XzZhao
*/
public class UnsynchBankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static void main(String[] args) {
Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; i++) {
TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
Thread t = new Thread(r);
t.start();
}
}
}
程序出错:总金额发生变化
锁对象
用ReentrantLock保护代码块的结构:
Lock myLock = new ReentrantLock();
myLock.lock();
try {
// work code
} finally {
myLock.unlock();
}
这一结构确保任何时刻只有一个线程进入。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
使用一个锁来保护Bank类的transfer方法:
/**
* @author XzZhao
*/
public class Bank {
...
private final Lock bankLock = new ReentrantLock();
public void transfer(int from, int to, double amount) {
if (accounts[from] < amount) {
return;
}
bankLock.lock();
try {
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("转账金额: %10.2f 转出账户: %d 转入账户: %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" 最后的金额: %10.2f%n", getTotalBalance());
} finally {
bankLock.unlock();
}
}
...
}
再次运行程序后,发现不会出现错误,线程之间不会互相影响了。
锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数来跟踪lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。所以,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
条件对象
我们来修改一下例子,我们使用条件对象来避免选择没有足够资金的账户作为转出账户。
Bank类:
/**
* @author XzZhao
*/
public class Bank {
private final double[] accounts;
private final Lock bankLock;
private final Condition sufficientFunds;
public Bank(int n, double initialBalance) {
accounts = new double[n];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = initialBalance;
}
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition(); // 获取一个和该锁相关的条件对象
}
public void transfer(int from, int to, double amount) throws InterruptedException {
bankLock.lock();
try {
while (accounts[from] < amount) {
sufficientFunds.await(); // 将该线程放到条件的等待集中
}
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("转账金额: %10.2f 转出账户: %d 转入账户: %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" 最后的金额: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll(); // 解除该条件的等待集中的所有线程的阻塞状态
} finally {
bankLock.unlock();
}
}
public double getTotalBalance() {
bankLock.lock();
try {
double sum = 0;
for (double a : accounts) {
sum += a;
}
return sum;
} finally {
bankLock.unlock();
}
}
public int size() {
return accounts.length;
}
}
TransferRunnable类:
/**
* @author XzZhao
*/
public class TransferRunnable implements Runnable {
private final Bank bank;
private final int fromAccount;
private final double maxAmount;
private final int DELAY = 10;
public TransferRunnable(Bank bank, int fromAccount, double maxAmount) {
super();
this.bank = bank;
this.fromAccount = fromAccount;
this.maxAmount = maxAmount;
}
@Override
public void run() {
try {
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = maxAmount * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}