关于线程/多线程(2)
同步
在很多多线程程序的应用中,两个和两个以上线程需要共享同一份数据的存取,如果这两个线程同时存取同一个对象,而这两个线程又同时调用了修改该对象的方法时,可以预见,线程彼此产生了竞争。根据线程访问次序的不同,可能会产生讹误的对线,这种情况被称为竞争条件
所以,为了避免多线程引起的对共享数据的讹误,必须了解同步存取。
条件竞争
比如说
在一个银行程序中:
有一个bank类的transfer方法,不断地从一个账户中,转移一定金额到另一个账户(不考虑账户余额的情况)。
有一个Runnable类的run方法,在每一次迭代中,他都会随机选择一个账户和一个目标账户,同时调用transfer方法。
假设我们再开始的时候有100个账户,分别有1000元钱。那么在上面这个程序不断执行下去的时候,银行里的总金额应该是不变的(一共为100,000元)。
但是,在实际操作中,会发现在最初的交易中,银行的余额保持在100,00元,但是够了一段时间,余额的总量发生了变化,这就是因为发生了竞争条件
这其中的问题在于更新银行账户的操作不是原子操作,指令可以被分解为如下:
1)将余额加载到寄存器
2)余额=余额+转账金额
3)返回余额
如果说,有一个线程1,执行了步骤1和2,然后就剥夺了运行权,现在线程2,被唤醒修改了余额的然后,线程1被唤醒并完成了步骤3,擦除了线程2所做的更新,导致总金额发生变化。
lock方法
由上面可以看到,我们需要一个锁来保护transfer方法。那就是——ReenteantLock方法!!
伪代码如下:
.....
private Lock bankLock = new ReentrantLock;
public void transfer(int from,int to ,int amount)
{
bankLock.lock();
try
{
步骤1,2,3;
}
finally
{
bankLock.unlock();
}
}
这下余额就永远不会出错啦
当然,这里的每个Bank都有自己的ReentrantLock类,如果两个线程同时调用同一个对象,那么锁是以串行的方式运行,如果是同一个类的不同对象,那么每个线程都会得到一个不同的锁,互不影响。
条件对象
条件对象的使用情况通常是在,当线程进入临界区时,却发现要满足某一条件后他才能执行。这就需要用一个条件对象来管理已经获得了锁却不能开始工作的线程。
还是之前那个例子:在上一个例子中,当一个线程调用transfer方法时,却发现该账户的转出金额大于账户的余额时,这个线程就被挂起,直到另一个线程向该账户中转账。但是由于锁的排它性,别的线程没有机会对该账户进行操作。这也就是为什么我们需要条件对象的原因。
我们可以用newCondition方法来获得一个条件对象,例如一个“余额冲充足”的条件对象
private Condition sufficientFunds; ... ... public Bank() { ... sufficientFunds = bankLock.newCondition; }
当条件不足时,调用
sufficientFunds.await();
注意:调用了await方法的线程和等待锁的线程有本质的不同,当线程调用await之后,它进入该条件的等待集,需要一直等到有其他线程调用同一条件上的signalAll为止
当另一个线程满足同一条件时,他应该调用
sufficientFunds.signalAll();
最终伪代码变成
.....
private Lock bankLock = new ReentrantLock;
public void transfer(int from,int to ,int amount)
{
bankLock.lock();
try
{
while(账户余额<转账金额)
sufficientFunds.await();
步骤1,2,3;
sufficientFunds.signalAll();
}
finally
{
bankLock.unlock();
}
}
总结一下就是:
- 锁用来保护代码片段,任何时刻都只能有一个线程执行被保护的代码块
- 锁可以管理试图进入被保护代码块的线程
- 锁可以拥有一个或者多个相关的条件对象
- 每个条件对象管理那些已经进入被保护的代码块的但还不能运行的线程
以上全部来自博主对于《java核心技术 卷一》的理解思考