Java多线程学习-II

参考了 《Java核心技术》 卷I 第14章 多线程 14.5.1-14.5.4

5.同步

  并发线程如果访问统一对象或者变量可能造成竞争。书中举了银行账户之间转账的例子,这是代码;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Runnable r = () -> {
try
{
while (true)
{
int toAccount = (int) (bank.size() * Math.random())
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
}
catch (InterruptedException e)
{
}
};

  transfer方法代码

1
2
3
4
5
6
7
8
9
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 from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}

  这里注意到accounts[to] += amount;不是原子操作,下面这个图也很清晰地显示出发生竞争,导致最后值不正确的过程。

  在Java中,关键字synchronized就是提供了锁的机制,当然java.util.concurrent包中也提供了分离出来的类实现锁的机制

使用ReentrantLock(注意在java.util.concurrent包中,Lock是接口,ReentrantLock是实现类 )的一个基本框架如下:

1
2
3
4
5
6
7
8
9
myLock.lock(); // a ReentrantLock object
try
{
// critical section
}
finally
{
myLock.unlock(); // make sure the lock is unlocked even if an exception is thrown
}

  下面这个例子使用锁来保护Bank类的transfer方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Bank
{
private Lock bankLock = new ReentrantLock(); // ReentrantLock implements the Lock interface
. . .
public void transfer(int from, int to, int amount)
{
bankLock.lock();
try
{
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}
finally
{
bankLock.unlock();
}
}
}

  可以看到如果一个线程最先调用了Bank对象的transfer方法,将获得锁,在锁不被释放之前,其它线程如果访问这一被锁保护的代码,将会被阻塞。线程锁还有一个hold count的变量,用来记录调用嵌套调用锁方法的次数。也就是说transfer方法中可以嵌套调用另一个方法同样锁住bankLock对象:

  这里transfer方法中调用了getTotalBalance方法,而getTotalBalance方法又要获得锁,在此种情境下,锁并不会冲突,因为是在同一线程下。如果是两个线程访问同一个Bank对象中的transfer或者getTotalBalance方法,锁就要串行化工作了。很显然,如果两个线程访问两个Bank对象的transfer方法,锁并不会冲突,因为是两把锁。

  下面这张图是同步和非同步访问transfer的示例:

  关于条件对象:仔细看刚刚Runnbale中的run方法,会发现在bank.transfer(from, to, amount)之前并没有做任何的条件判断,这会造成余额不够的情况,但是,不能用下面这样的条件判断:

1
2
if (bank.getBalance(from) >= amount)
bank.transfer(from, to, amount);

  因为如果当前线程有可能在下面的注释的地方中断:

1
2
3
if (bank.getBalance(from) >= amount)
// thread might be deactivated at this point
bank.transfer(from, to, amount);

  一旦中断,那么线程再次运行时,有可能判断条件又不满足了,通过使用锁的手段可以避免

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void transfer(int from, int to, int amount)
{
bankLock.lock();
try
{
while (accounts[from] < amount)
{
// wait
}
// transfer funds
}
finally
{
bankLock.unlock();
}
}

  但这就意味着当账户没有足够余额的时候,其他线程根本没有transfer的机会,这就是我们需要条件对象的原因。在此,我们设置了一个条件“余额充足”的条件:

1
2
3
4
5
6
7
8
9
10
class Bank
{
private Condition sufficientFunds; //条件对象
. . .
public Bank()
{
. . .
sufficientFunds = bankLock.newCondition();
}
}

  如果transfer方法发现余额不走,调用sufficientFundsawait 方法,当前线程阻塞,放弃锁,注意调用await方法与等待获得锁的线程有所不同,调用await方法,是进入该条件的等待集,锁可用时,也不能马上接触阻塞,要等待其它线程调用同意条件的signalAll方法才可以。signalAll方法不是激活线程,只是解除等待线程。