2.使用synchronized关键字实现多线程的同步和互斥(不同线程同时读写同一数据)
利用能量守恒定律实现多线程的同步和互斥
- EnergySystem.java:能量类
- EnergySystemTest.java:测试Main类
- EnergyTransferTask.java:任务线程线程
synchronized
synchronized关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法和 synchronized块。
Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块。
不加锁(synchronized )的情况
/** * 宇宙的能量系统 遵循能量守恒定律: 能量不会凭空创生或消失,只会从一处转移到另一处 */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) { if (energyBoxes[from] > amount) { 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()); } else { return; } } /** * 获取能量世界的能量总和 */ public double getTotalEnergies() { double sum = 0; for (double amount : energyBoxes) sum += amount; return sum; } /** * 返回能量盒子的长度 */ public int getBoxAmount() { return energyBoxes.length; }}
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() { int c = 3; try{ while (c > 0){ c--; int toBox = (int) (energySystem.getBoxAmount()* Math.random()); double amount = maxAmount * Math.random(); energySystem.transfer(fromBox, toBox, amount); Thread.sleep((int) (DELAY * MansferTask(eng, i, INITIAL_ENERGY); Thread t = new Thread(task,"TransferThread_"+i); t.start(); } }}
public class EnergySystemTest { //将要构建的能量世界中能量盒子数量 public static final int BOX_AMOUNT = 10; //每个盒子初始能量 public static final double INITIAL_ENERGY = 100; 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(); } }}
代码功能
现在的代码描述的是有10个能量盒子,然后启动10个线程,每个线程会执行三次操作:将A盒子中能量转移到B盒子中,但是我们发现,能量总和减少啦,为什么呢?
举个例子:线程1读取盒子A的数据5000,将其加500,此时线程2读取盒子A的数据5000(因为还有写入,所以盒子A还是5000),将其加900,再将其写入,此时盒子A为5900,但此时线程A也要进行写操作,这时盒子A为5500,所以……
输出样例
TransferThread_3TransferThread_7TransferThread_2从2转移 53.56单位能量到3TransferThread_6从6转移 40.02单位能量到5 能量总和: 937.16
TransferThread_1从1转移 2.30单位能量到8 能量总和: 937.16
TransferThread_0从0转移 27.61单位能量到9TransferThread_9TransferThread_4从4转移 82.48单位能量到7 能量总和: 927.92
TransferThread_8从8转移 68.90单位能量到3 能量总和: 927.92
TransferThread_5从5转移 33.59单位能量到3 能量总和: 927.92
TransferThread_1从9转移 9.24单位能量到3 能量总和: 866.17
能量总和: 937.16
能量总和: 937.16
从7转移 43.20单位能量到3 能量总和: 909.37
从3转移 19.64单位能量到7 能量总和: 929.01
TransferThread_0从0转移 7.07单位能量到7 能量总和: 929.01
从1转移 70.99单位能量到2 能量总和: 1000.00
TransferThread_7从7转移 94.67单位能量到3 能量总和: 1000.00
TransferThread_5从5转移 66.16单位能量到3 能量总和: 1000.00
TransferThread_7从7转移 59.33单位能量到4 能量总和: 1000.00
TransferThread_4TransferThread_6从6转移 12.24单位能量到8 能量总和: 997.95
从4转移 2.05单位能量到4 能量总和: 1000.00
TransferThread_3从3转移 61.92单位能量到4 能量总和: 1000.00
TransferThread_2从2转移 65.65单位能量到5 能量总和: 1000.00
TransferThread_8TransferThread_9从9转移 44.34单位能量到3 能量总和: 977.95
从8转移 22.05单位能量到4 能量总和: 1000.00
TransferThread_1从1转移 1.77单位能量到8 能量总和: 1000.00
TransferThread_4从4转移 25.64单位能量到5 能量总和: 1000.00
TransferThread_3从3转移 83.27单位能量到6 能量总和: 1000.00
加锁操作,修改EnergySystem.java
/** * 宇宙的能量系统 遵循能量守恒定律: 能量不会凭空创生或消失,只会从一处转移到另一处 */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) { // 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; }}
锁的应用
当程序执行到transfer方法时,通过synchronized (lockObj)会将当前资源(盒子A)锁住,其他线程无法获取次资源,当energyBoxes[from]小于amount时,即当前数据不符合操作要求,执行lockObj.wait()方法,将资源放开,此线程进入Wait Set中等待其他线程唤醒(notifyAll(),notify()),当数据符合操作且操作结束后,会调用notifyAll方法释放线程,这是其他线程就可以获取当前资源了,达到总能量始终是一定的。
notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。两者的最大区别在于:
notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
notify则文明得多他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。
输出样例
TransferThread_6从6转移 62.69单位能量到1 能量总和: 1000.00
TransferThread_1从1转移 88.95单位能量到2 能量总和: 1000.00
TransferThread_3从3转移 15.16单位能量到1 能量总和: 1000.00
TransferThread_4从4转移 37.62单位能量到2 能量总和: 1000.00
TransferThread_5从5转移 88.83单位能量到6 能量总和: 1000.00
TransferThread_0从0转移 57.21单位能量到7 能量总和: 1000.00
TransferThread_2从2转移 67.80单位能量到0 能量总和: 1000.00
TransferThread_7从7转移 55.19单位能量到8 能量总和: 1000.00
TransferThread_8从8转移 75.56单位能量到8 能量总和: 1000.00
TransferThread_9从9转移 21.01单位能量到6 能量总和: 1000.00
TransferThread_3从3转移 75.10单位能量到8 能量总和: 1000.00
TransferThread_6从6转移 17.85单位能量到4 能量总和: 1000.00
TransferThread_7从7转移 54.41单位能量到4 能量总和: 1000.00
TransferThread_0从0转移 65.67单位能量到6 能量总和: 1000.00
TransferThread_9从9转移 11.83单位能量到7 能量总和: 1000.00
TransferThread_2从2转移 59.20单位能量到8 能量总和: 1000.00
TransferThread_9从9转移 32.79单位能量到3 能量总和: 1000.00
TransferThread_3从3转移 11.00单位能量到9 能量总和: 1000.00
TransferThread_4从4转移 41.82单位能量到4 能量总和: 1000.00
TransferThread_8从8转移 53.70单位能量到3 能量总和: 1000.00
TransferThread_6从6转移 81.85单位能量到8 能量总和: 1000.00
TransferThread_2从2转移 15.96单位能量到0 能量总和: 1000.00
TransferThread_4从4转移 32.41单位能量到9 能量总和: 1000.00
TransferThread_0从0转移 59.02单位能量到0 能量总和: 1000.00
TransferThread_8从8转移 67.92单位能量到2 能量总和: 1000.00