多线程(二)线程交互之互斥与同步
程序员文章站
2022-05-04 17:10:12
...
首先我们通过一个有意思的案例来引入由于线程争用条件造成的一些严重的问题。
下面的代码简单来说是初始化多个能量盒子,每个盒子所含初始能量相同,这样总能量就固定了。开设多个线程将这些盒子的能量相互转移,在转移过程就出现了问题。
package disappearEnergy;
/**
* 宇宙的能量系统
* 遵循能量守恒定律:
* 能量不会凭空创生或消失,只会从一处转移到另一处
*/
public class EnergySystem {
//能量盒子,能量存贮的地方
private final double[] energyBoxes;
/**
*
* @param n 能量盒子的数量
* @param initialEnergy 每个能量盒子初始含有的能量值
*/
public EnergySystem(int n, double initialEnergy){
energyBoxes = new double[n];
for (int i = 0; i < energyBoxes.length; i++)
energyBoxes[i] = initialEnergy;
}
/**
* 能量的转移,从一个盒子到另一个盒子
* @param from 能量源
* @param to 能量终点
* @param amount 能量值
*/
public void transfer(int from, int to, double amount){
if (energyBoxes[from] < amount)
return;
System.out.print(Thread.currentThread().getName());
energyBoxes[from] -= amount;
System.out.printf("从%d转移%10.2f单位能量到%d", from, amount, to);
energyBoxes[to] += amount;
System.out.printf(" 能量总和:%10.2f%n", getTotalEnergies());
}
/**
* 获取能量世界的能量总和
*/
public double getTotalEnergies(){
double sum = 0;
for (double amount : energyBoxes)
sum += amount;
return sum;
}
/**
* 返回能量盒子的长度
*/
public int getBoxAmount(){
return energyBoxes.length;
}
}
package disappearEnergy;
public class EnergyTransferTask implements Runnable{
//共享的能量世界
private EnergySystem energySystem;
//能量转移的源能量盒子下标
private int fromBox;
//单次能量转移最大单元
private double maxAmount;
//最大休眠时间(毫秒)
private int DELAY = 10;
public EnergyTransferTask(EnergySystem energySystem, int from, double max){
this.energySystem = energySystem;
this.fromBox = from;
this.maxAmount = max;
}
public void run() {
try{
while (true){
int toBox = (int) (energySystem.getBoxAmount()* Math.random());
double amount = maxAmount * Math.random();
energySystem.transfer(fromBox, toBox, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
package disappearEnergy;
public class EnergySystemTest {
//将要构建的能量世界中能量盒子数量
public static final int BOX_AMOUNT = 100;
//每个盒子初始能量
public static final double INITIAL_ENERGY = 1000;
public static void main(String[] args){
EnergySystem eng = new EnergySystem(BOX_AMOUNT, INITIAL_ENERGY);
for (int i = 0; i < BOX_AMOUNT; i++){
EnergyTransferTask task = new EnergyTransferTask(eng, i, INITIAL_ENERGY);
Thread t = new Thread(task,"TransferThread_"+i);
t.start();
}
}
}
经过允许,程序出现了如下的异常结果:
如上图所示,能量总和会减少,并且出现某些语句未执行完下一条语句便开始执行的情况。
出现上述问题的原因:
当多个线程同时共享访问同一数据(内存区域)时,每个线程都尝试操作该数据,从而导致数据被破坏(corrupted),这种现象称为争用条件。
类似于上图所示的现象,每个线程在操作数据时,会先将数据初值读【取到自己获得的内存中】,然后在内存中进行运算后,重新赋值到数据。线程1在还【未重新将值赋回去时】,线程1阻塞,线程2开始访问该数据,然后进行了修改,之后被阻塞的线程1再获得资源,而将之前计算的值覆盖掉线程2所修改的值,就出现了数据丢失情况。
为解决上面的问题,就要实现线程的互斥与同步。
互斥:同一时间,只能有一个线程访问数据。
同步:一种通信机制,一个线程操作完成后,以某种方式通知其他线程。
线程之间的互斥:通过加锁实现:线程访问临界区的代码放在一个代码块中,加锁实现,即 synchronized(lockObj)
线程之间的同步:通过wait()+notify()的通信机制来实现
wait()和notifyAll()是在线程同步的时候使用的一对方法。据此思想,修改了EnergySystem的代码,在transfer函数中添加了对象锁。
package disappearEnergy;
/**
* 宇宙的能量系统
* 遵循能量守恒定律:
* 能量不会凭空创生或消失,只会从一处转移到另一处
*/
public class EnergySystem {
//能量盒子,能量存贮的地方
private final double[] energyBoxes;
private final Object lockObj = new Object();
/**
*
* @param n 能量盒子的数量
* @param initialEnergy 每个能量盒子初始含有的能量值
*/
public EnergySystem(int n, double initialEnergy){
energyBoxes = new double[n];
for (int i = 0; i < energyBoxes.length; i++)
energyBoxes[i] = initialEnergy;
}
/**
* 能量的转移,从一个盒子到另一个盒子
* @param from 能量源
* @param to 能量终点
* @param amount 能量值
*/
public void transfer(int from, int to, double amount){
synchronized(lockObj){
// if (energyBoxes[from] < amount)
// return;
//while循环,保证条件不满足时任务都会被条件阻挡
//而不是继续竞争CPU资源
while (energyBoxes[from] < amount){
try {
//条件不满足, 将当前线程放入Wait Set
lockObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(Thread.currentThread().getName());
energyBoxes[from] -= amount;
System.out.printf("从%d转移%10.2f单位能量到%d", from, amount, to);
energyBoxes[to] += amount;
System.out.printf(" 能量总和:%10.2f%n", getTotalEnergies());
//唤醒所有在lockObj对象上等待的线程
lockObj.notifyAll();
}
}
/**
* 获取能量世界的能量总和
*/
public double getTotalEnergies(){
double sum = 0;
for (double amount : energyBoxes)
sum += amount;
return sum;
}
/**
* 返回能量盒子的长度
*/
public int getBoxAmount(){
return energyBoxes.length;
}
}