线程通信
1、等待。
public final void wait() public final void wait(long timeout) 注: 必须在对obj加锁的同步代码块中。 在一个线程中,调用obj.wait()方法,此时线程会释放气所拥有的所有的锁标记,并且放弃CPU的使用权,进入等待队列中。
2、通知(唤醒)。
public final native void notify() 随机唤醒一个等待队列中的线程。可能会产生死锁问题。 public final native void notifyAll() 唤醒等待队列中的所有的线程。
使用线程通信来实现一个银行卡取钱的功能。我们定义余额为0时,可以进行存钱操作,不可以进行取钱操作;余额不为0时,可以进行取钱操作,但是不能进行存钱操作。
定义一个银行卡实体类:
public class BankCard { /** * 银行卡余额 */ private double money; /** * 存钱 */ public void save(double money) { this.money += money; System.out.println(Thread.currentThread().getName() + "存钱" + money + ",余额:" + this.money); } /** * 取钱 */ public void withdraw(double money) { this.money -= money; System.out.println(Thread.currentThread().getName() + "取钱" + money + ",余额:" + this.money); } }
定义取钱线程:
public class WithdrawMoney implements Runnable { BankCard bankCard; public WithdrawMoney(BankCard bankCard) { this.bankCard = bankCard; } @Override public void run() { for (int i = 0; i < 5; i++) { this.bankCard.withdraw(1000); } } }
定义存钱线程:
public class SaveMoney implements Runnable { BankCard bankCard; public SaveMoney(BankCard bankCard) { this.bankCard = bankCard; } @Override public void run() { for (int i = 0; i < 5; i++) { bankCard.save(1000); } } }
定义两个线程,一个进行存钱操作,一个进行取钱操作。
public static void main(String[] args) { //实例化银行卡示例 BankCard bankCard = new BankCard(); //定义存钱操作,存钱需传入对应的银行卡,才可以完成操作 SaveMoney saveMoney = new SaveMoney(bankCard); //定义取钱操作,取钱需传入对应的银行卡,才可以完成操作 WithdrawMoney withdrawMoney = new WithdrawMoney(bankCard); //定义两个线程,t1完成存钱操作;t2完成取钱操作 Thread t1 = new Thread(saveMoney, "t1"); Thread t2 = new Thread(withdrawMoney, "t2"); t1.start(); t2.start(); }
运行结果:
t1存钱1000.0,余额:0.0 t2取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0 t2取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0
这时我们就发现了问题,t1明明存钱了1000,但是为什么余额还是0呢,这个就是线程安全的问题,没有保证线程安全。代码优化:
public class SaveMoney implements Runnable { BankCard bankCard; public SaveMoney(BankCard bankCard) { this.bankCard = bankCard; } @Override public void run() { synchronized (bankCard) {//加入同步代码块,同一时间只能一个线程操作银行卡 for (int i = 0; i < 5; i++) { bankCard.save(1000); } } } } public class WithdrawMoney implements Runnable { BankCard bankCard; public WithdrawMoney(BankCard bankCard) { this.bankCard = bankCard; } @Override public void run() { synchronized (bankCard) {//加入同步代码块,同一时间只能一个线程操作银行卡 for (int i = 0; i < 5; i++) { this.bankCard.withdraw(1000); } } } }
运行结果:
t1存钱1000.0,余额:1000.0 t1存钱1000.0,余额:2000.0 t1存钱1000.0,余额:3000.0 t1存钱1000.0,余额:4000.0 t1存钱1000.0,余额:5000.0 t2取钱1000.0,余额:4000.0 t2取钱1000.0,余额:3000.0 t2取钱1000.0,余额:2000.0 t2取钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0
此时取钱、存钱的结果就成了正确的结果。但是这个时候我们却不能保证可以达到t1存一次,t2去一次,这样的效果。这时候我们就可以使用线程通讯来进行处理。
/** * 存钱 */ public void save(double money) { //这里我们要判断银行卡余额,余额为0时才只能进行存钱操作,余额不为0时不能进行存钱操作。 if (this.money != 0) { //这里表明银行卡余额不为0,也就是银行卡有钱,则将存钱线程加入到等待队列,等待银行卡余额为0时,我们再来唤醒线程,进行存钱操作。 try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //当余额为0时,进行存钱操作 this.money += money; System.out.println(Thread.currentThread().getName() + "存钱" + money + ",余额:" + this.money); //唤醒等待队列中的线程 this.notify(); } /** * 取钱 */ public void withdraw(double money) { //这里我们要判断银行卡余额,余额不为0时才能进行取钱操作,余额为0时不能进行取钱操作。 if (this.money == 0) { //这里表明银行卡余额为0,也就是银行卡没钱,则将取钱线程加入到等待队列,等待银行卡余额不为0时,我们再来唤醒线程,进行取钱操作。 try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //当余额不为0时,进行取钱操作 this.money -= money; System.out.println(Thread.currentThread().getName() + "取钱" + money + ",余额:" + this.money); //唤醒等待队列中的线程 this.notify(); }
运行结果:
t1存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0
这时我们发现,输出结果即为我们预期结果。这时我们只是实现了单存单取的效果,我们再来试一下多存多取。
//定义四个线程,t1、t3存钱,t2、t4取钱。 Thread t1 = new Thread(saveMoney, "t1"); Thread t2 = new Thread(withdrawMoney, "t2"); Thread t3 = new Thread(saveMoney, "t3"); Thread t4 = new Thread(withdrawMoney, "t4"); t1.start(); t2.start(); t3.start(); t4.start();
输出结果:
t1存钱1000.0,余额:1000.0 t4取钱1000.0,余额:0.0 t3存钱1000.0,余额:1000.0 t4取钱1000.0,余额:0.0 t2取钱1000.0,余额:-1000.0 t2取钱1000.0,余额:-2000.0 t2取钱1000.0,余额:-3000.0 t2取钱1000.0,余额:-4000.0 t2取钱1000.0,余额:-5000.0 t3存钱1000.0,余额:-4000.0 t4取钱1000.0,余额:-5000.0 t4取钱1000.0,余额:-6000.0 t4取钱1000.0,余额:-7000.0 t1存钱1000.0,余额:-6000.0 t3存钱1000.0,余额:-5000.0 t1存钱1000.0,余额:-4000.0 t3存钱1000.0,余额:-3000.0 t1存钱1000.0,余额:-2000.0 t3存钱1000.0,余额:-1000.0 t1存钱1000.0,余额:0.0
然后我们就发现出现了问题,输出结果出现了负数的问题,这是什么原因呢?我们来解读一下:
第一次t1获得了cpu,开始进行存钱操作,此时余额为0,可以存钱,然后释放cpu。 余额为1000;
第二次t4获取了cpu,开始进行取钱操作,此时余额为1000,可以取钱,然后释放cpu。 余额为0;
第三次t2获取了cpu,开始进行取钱操作,此时余额为0,不可以取钱,将t2加入到等待队列,释放锁,释放cpu。 余额为0;
第四次t3获取了cpu,开始进行存钱操作,此时余额为0,可以存钱,唤醒等待队列中的线程,这时t2被唤醒,然后释放cpu。 余额为1000;
第五次t4获取了cpu,开始进行取钱操作,此时余额为1000,可以取钱,然后释放cpu。 余额为0;
第六次t2获得了cpu,这时就出现了问题。我们预期这里应该是不能取钱,因为余额为0,但是却发生了余额为负数的情况。

所以说是因为唤醒时我们没有校验余额是否为0,所以可以优化代码为:
/** * 存钱 */ public void save(double money) { //这里我们要判断银行卡余额,余额为0时才只能进行存钱操作,余额不为0时不能进行存钱操作。 while (this.money != 0) { //这里表明银行卡余额不为0,也就是银行卡有钱,则将存钱线程加入到等待队列,等待银行卡余额为0时,我们再来唤醒线程,进行存钱操作。 try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //当余额为0时,进行存钱操作 this.money += money; System.out.println(Thread.currentThread().getName() + "存钱" + money + ",余额:" + this.money); //唤醒等待队列中的线程 this.notify(); } /** * 取钱 */ public void withdraw(double money) { //这里我们要判断银行卡余额,余额不为0时才能进行取钱操作,余额为0时不能进行取钱操作。 while (this.money == 0) { //这里表明银行卡余额为0,也就是银行卡没钱,则将取钱线程加入到等待队列,等待银行卡余额不为0时,我们再来唤醒线程,进行取钱操作。 try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //当余额不为0时,进行取钱操作 this.money -= money; System.out.println(Thread.currentThread().getName() + "取钱" + money + ",余额:" + this.money); //唤醒等待队列中的线程 this.notify(); }
我们使用while循环,保证唤醒后可以继续进入条件校验。
输出结果:
t1存钱1000.0,余额:1000.0 t4取钱1000.0,余额:0.0 t3存钱1000.0,余额:1000.0 t4取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0
这时我们会发现程序到这里就没有继续执行了,但是明显我们的程序还没有结束。这个是什么原因呢?我们来解读一下:
第一次t1获得了cpu,开始进行存钱操作,此时余额为0,可以存钱,然后释放cpu。 余额为1000;
第二次t3获取了cpu,开始进行存钱操作,此时余额为1000,不可以存钱,所以t3进入等待队列,释放锁,然后释放cpu。 余额为1000;等待队列:t3;
第三次t1获得了cpu,开始进行存钱操作,此时余额不为0,不可以存钱,所以t1进入等待队列,释放锁,然后释放cpu。 余额为1000;等待队列:t3、t1;
第四次t4获取了cpu,开始进行取钱操作,此时余额为1000,可以取钱,随机唤醒一个线程,这里假设唤醒t1,然后释放cpu。 余额为0;等待队列:t3;
第五次t2获取了cpu,开始进行取钱操作,此时余额为0,不可以取钱,将t2加入到等待队列,释放锁,释放cpu。 余额为0;等待队列:t3,t2;
第六次t4获取了cpu,开始进行取钱操作,此时余额为0,不可以取钱,将t4加入到等待队列,释放锁,释放cpu。 余额为0;等待队列:t3,t2,t4;
第七次t1获取了cpu,开始进行存钱操作,此时余额为0,可以存钱,唤醒等待队列中的线程,这时假设t3被唤醒,然后释放cpu。 余额为1000;等待队列:t2,t4;
第八次t3获得了cpu,开始进行存钱操作,此时余额不为0,不可以存钱,所以t3进入等待队列,释放锁,然后释放cpu。 余额为1000;等待队列:t2,t4,t3;
第九次t1获得了cpu,开始进行存钱操作,此时余额不为0,不可以存钱,所以t1进入等待队列,释放锁,然后释放cpu。 余额为1000;等待队列:t2,t4,t3,t1;
这时会发现所有的线程都进入了等待队列,以致于没有线程可以继续往下执行。所以可以优化代码为:
/** * 存钱 */ public void save(double money) { //这里我们要判断银行卡余额,余额为0时才只能进行存钱操作,余额不为0时不能进行存钱操作。 while (this.money != 0) { //这里表明银行卡余额不为0,也就是银行卡有钱,则将存钱线程加入到等待队列,等待银行卡余额为0时,我们再来唤醒线程,进行存钱操作。 try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //当余额为0时,进行存钱操作 this.money += money; System.out.println(Thread.currentThread().getName() + "存钱" + money + ",余额:" + this.money); //唤醒等待队列中的线程 this.notifyAll(); } /** * 取钱 */ public void withdraw(double money) { //这里我们要判断银行卡余额,余额不为0时才能进行取钱操作,余额为0时不能进行取钱操作。 while (this.money == 0) { //这里表明银行卡余额为0,也就是银行卡没钱,则将取钱线程加入到等待队列,等待银行卡余额不为0时,我们再来唤醒线程,进行取钱操作。 try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //当余额不为0时,进行取钱操作 this.money -= money; System.out.println(Thread.currentThread().getName() + "取钱" + money + ",余额:" + this.money); //唤醒等待队列中的线程 this.notifyAll(); }
这里我们不采用随机唤醒,而是唤醒全部线程,保证不会出现上述情况。
运行结果:
t1存钱1000.0,余额:1000.0 t4取钱1000.0,余额:0.0 t3存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0 t4取钱1000.0,余额:0.0 t3存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0 t4取钱1000.0,余额:0.0 t3存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0 t4取钱1000.0,余额:0.0 t3存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0 t1存钱1000.0,余额:1000.0 t4取钱1000.0,余额:0.0 t3存钱1000.0,余额:1000.0 t2取钱1000.0,余额:0.0
然后结果运行正常。这个功能就做好了!

浙公网安备 33010602011771号