线程通信

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

然后结果运行正常。这个功能就做好了!

posted @ 2022-12-27 20:32  Amireux-126  阅读(34)  评论(0)    收藏  举报