目录
java----多线程线程通信
1.线程通信
1.1引言
线程通信的目的:是为了控制线程执行的顺序。
比如说,向同一张银行卡里存钱和取钱的例子,存一次钱然后取一次,不能多存也不能多取。
这个案例和之前的案例一样,只不过之前的案例没有考虑线程安全的问题。(详见此篇 3.2.2)。
其实如果单纯为了线程安全的话,只需要对临界资源bankCard
加锁(也就是利用线程的同步就可以解决)。
利用线程同步的代码如下所示。
BankCard类
public class BankCard {
private double money;
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
AddMoney 和SubMoney 类
public class AddMoney implements Runnable{
private BankCard bankCard;
public AddMoney(BankCard bankCard){
this.bankCard = bankCard;
}
@Override
public void run() {
for(int i = 0;i < 10;i ++){
synchronized (bankCard){
this.bankCard.setMoney(this.bankCard.getMoney() + 1000);
System.out.println(Thread.currentThread().getName() + "存了1000,余额是" + this.bankCard.getMoney());
}
}
}
}
public class SubMoney implements Runnable{
private BankCard bankCard;
public SubMoney(BankCard bankCard){
this.bankCard = bankCard;
}
@Override
public void run() {
for(int i = 0;i < 10;i ++){
synchronized (bankCard){
if (this.bankCard.getMoney() >= 1000){
this.bankCard.setMoney(this.bankCard.getMoney() - 1000);
System.out.println(Thread.currentThread().getName() + "取了1000,余额是" + this.bankCard.getMoney());
}
else{
System.out.println("余额不足");
i--;
}
}
}
}
}
main函数
public static void main(String[] args) {
BankCard card = new BankCard();
AddMoney addMoney = new AddMoney(card);
SubMoney subMoney = new SubMoney(card);
Thread thread1 = new Thread(addMoney);
Thread thread2 = new Thread(subMoney);
thread1.start();
thread2.start();
}
执行结果。
利用线程的同步也就是加锁,不会出现以下情况(还没显示存钱成功就打印出了取钱成功的字样):这种情况出现的原因在于,你存了钱(money
的值已经改了),但是存钱线程还没有执行打印语句就被取钱线程抢走了CPU的时间片,然后取钱线程就顺势取钱然后打印。
虽然依靠线程的同步,可以保证线程的基本安全,但是,却不能保证线程之间的执行顺序。我现在,就是想要你先存钱之后,我再进行取钱,并且是每存一次就要取一次,这就要依靠线程之间的通信来实现了。
1.2线程通信主要用到的方法
这些方法都必须在对obj加锁的同步代码块中使用。
1. public final void wait() 释放锁标记,调用了之后,线程会阻塞在锁的等待队列之中。
2. public final void notify() 从等待队列中随机唤醒一个
3. public final void notifyAll() 唤醒所有
下述代码。
针对这个例子,代码的结构进行了少许的调整,我们把是实现银行卡的钱数更改的两个函数,放到了BankCard类里面,依旧是10次循环。也就是持续的操作十次存钱取钱、存钱取钱、存钱取钱······
主函数的代码较上述来说是没有变化的。
首先是BankCard类。
注意使用wait的时候,使用的方法是 – 锁.wait()
public class BankCard {
private double money;
// 用flag来表征卡里是不是有钱,true的时候表示是有钱的,false的时候表示是没钱的
private boolean flag;
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
/**
*
* 注意看这两个函数的时候,不要一起看,一个一个的看,理清逻辑就好。
* 建议结合操作系统的进程线程一起看
*/
public synchronized void save(double money){ // 这个时候加锁是对this加锁
if(this.flag){ //如果flag为真,也就是说 现在卡里是有钱的,只能取钱不能再继续存钱了
try {
//注意使用wait的时候,使用的方法是 -- 锁.wait()
this.wait(); //这个时候我们要存钱的线程等待。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.money += money;
System.out.println(Thread.currentThread().getName() + "存钱成功,当前余额是" + this.money );
this.flag = true; //存完钱之后修改标记为true
this.notify(); //叫醒取钱线程。
}
public synchronized void withDraw(double money){
if(!this.flag){ //如果flag为假,也就是说 现在卡里是没有钱的,只能存钱不能再继续取钱了
try {
this.wait(); //这个时候我们要取钱的线程等待。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.money -= money;
System.out.println(Thread.currentThread().getName() + "取钱成功,当前余额是" + this.money );
this.flag = false; //取完钱之后修改标记为false
this.notify(); //叫醒存钱线程。
}
}
AddMoney 和 SubMoney类如下所示
public class AddMoney implements Runnable{
private BankCard bankCard;
public AddMoney(BankCard bankCard){
this.bankCard = bankCard;
}
@Override
public void run() {
for(int i = 0;i < 10;i ++){
// synchronized (bankCard){
// this.bankCard.setMoney(this.bankCard.getMoney() + 1000);
// System.out.println(Thread.currentThread().getName() + "存了1000,余额是" + this.bankCard.getMoney());
// }
bankCard.save(1000);
}
}
}
public class SubMoney implements Runnable{
private BankCard bankCard;
public SubMoney(BankCard bankCard){
this.bankCard = bankCard;
}
@Override
public void run() {
for(int i = 0;i < 10;i ++){
// synchronized (bankCard){
// if (this.bankCard.getMoney() >= 1000){
// this.bankCard.setMoney(this.bankCard.getMoney() - 1000);
// System.out.println(Thread.currentThread().getName() + "取了1000,余额是" + this.bankCard.getMoney());
// }
// else{
// System.out.println("余额不足");
// i--;
// }
this.bankCard.withDraw(1000);
}
}
}
主函数实质上和上述是一样的,还是给出来吧。
public static void main(String[] args) {
BankCard card = new BankCard();
AddMoney addMoney = new AddMoney(card);
SubMoney subMoney = new SubMoney(card);
Thread thread1 = new Thread(addMoney);
Thread thread2 = new Thread(subMoney);
thread1.start();
thread2.start();
}
ok,看这部分语法的时候,我发现自己操作系统关于进程和线程的知识,都忘得差不多了,属于是毫无保留的还给老师了。然后我是有重温了一遍,感觉收获不少,也许是因为学过一遍了,又或者是现在是自发的去阅读而不是为了应付考试。
总之建议 去重温一下操作系统吧,会有用的。
1.3 案例升级,假设是四个人用一张卡。两人存钱,两个人取钱,是否还能存一次取一次。
如果,我们在这个案例的基础上直接再添加两个线程(一个存钱一个取钱),是否可以达到我们想要的效果。
public static void main(String[] args) {
BankCard card = new BankCard();
AddMoney addMoney = new AddMoney(card);
SubMoney subMoney = new SubMoney(card);
Thread thread1 = new Thread(addMoney, "存钱一号机");
Thread thread2 = new Thread(subMoney,"取钱一号机");
//直接添加两个线程
Thread thread3 = new Thread(addMoney, "存钱二号机");
Thread thread4 = new Thread(subMoney,"取钱二号机");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
执行结果:
很明显,出现了连续存两次的情况。显然这不是我们想要看见的结果。
那么,为什么会出现,连续存两次的情况的呢,明明我们的银行卡也就是card
用的都是一个。
分析。
现在来逐步分析一下执行过程中可能出现的情况。
初始的flag
标记为false
从第一次有线程抢CPU开始分析。
1. 存钱一号抢到了CPU,存钱成功,修改标记flag为true,执行唤醒(但是其实这个时候谁也没有唤醒,因为等待的队列里面没有线程)
2. 存钱二号抢到了CPU,等待,释放刚抢到的CPU和锁。
3. 存钱一号抢到了CPU,等待,释放刚抢到的CPU和锁。
4. 取钱一号抢到了CPU,取钱成功,修改标记为false,执行唤醒(唤醒的可能是存钱一号也可能是存钱二号,这里假设唤醒了二号)。
5. 存钱二号抢到了CPU,存钱成功,修改标记为true,执行唤醒(唤醒的是存钱一号)。**当前的余额是1000**
6. 存钱一号抢到了CPU,**这个时候能不能存钱?????????** 答案是:可以
可以的原因还是来自于,线程的执行规则,线程在哪里休眠的,就会在哪里醒来。
第2步和第3步的时候,存钱一号
和存钱二号
被阻塞从而进入等待的队列是从下面的位置。
所以在后续的执行过程中,存钱一号
和存钱二号
被唤醒也是在这个位置,被唤醒之后,它们会继续抢占CPU,继续从this.wait()
往下执行,也就是说,这个时候,它越过了对flag
值得判断。直接执行了存钱的代码。
所以这个时候的问题实质上是出现在:唤醒之后没有进行对标记的判断,从而出现多存多取的情况。
那么,要解决这个问题,也变得简单了起来,只要我们在每次唤醒线程之后,重新进行一次判断不就可以完美解决了么?
只需要把if(flag)
换成while(flag)
代码:
public synchronized void save(double money){ // 这个时候加锁是对this加锁
while(this.flag){ //如果flag为真,也就是说 现在卡里是有钱的,只能取钱不能再继续存钱了
try {
this.wait(); //这个时候我们要存钱的线程等待。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.money += money;
System.out.println(Thread.currentThread().getName() + "存钱成功,当前余额是" + this.money );
this.flag = true; //存完钱之后修改标记为true
this.notify(); //叫醒取钱线程。
}
public synchronized void withDraw(double money){
while (!this.flag){ //如果flag为假,也就是说 现在卡里是没有钱的,只能存钱不能再继续取钱了
try {
this.wait(); //这个时候我们要取钱的线程等待。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.money -= money;
System.out.println(Thread.currentThread().getName() + "取钱成功,当前余额是" + this.money );
this.flag = false; //取完钱之后修改标记为false
this.notify(); //叫醒存钱线程。
}
再次执行之后:
满足我们要求他们存一次取一次的结果了。
但是!!!这么改完之后,多执行几次,我们就会发现一个新的问题!!!!
死锁了。。。。。。它死锁了。。。。。。。
四个线程都进去了(进去等待队列了)。
ok,来分析一下,是怎么死锁的。
1. 存钱一号抢到CPU,存钱成功,唤醒(无人唤醒),余额是1000
2. 存钱二号抢到CPU,存钱失败,等待。
3. 存钱一号抢到CPU,存钱失败,等待
4. 取钱一号抢到CPU,取钱成功,唤醒存钱一号,余额是0
5. 取钱二号抢到CPU,取钱失败,等待
6. 取钱一号抢到CPU,取钱失败,等待。
7. 存钱一号抢到CPU,存钱成功,唤醒了存钱二号,余额是1000;
8. 存钱一号抢到CPU,存钱失败,等待。
9. 存钱二号抢到CPU,存钱失败,等待。
全进去了
ok,所以当前现在问题是,在最紧要的关头,马上就要死锁的关头,存钱一号
唤醒错了人啊。。。。
因为this.notify()
只是随机唤醒一个,所以要解决当前出现的死锁问题,只需要全部唤醒,即this.notifyAll()
,要注意的是,这么做牺牲了性能。
1.4 案例,生产者消费者模式
上述案例在看的时候,生产者消费者模式这个操作系统的典型案例是不是会时不时的出现在思绪中呢。现在它来了。
描述:
若干个生产者生产产品,给若干个消费者去消费。为了使二者能够并发执行,将在两者之间设置一个可以存储多个产品的缓冲区,生产者往里面放,消费者从里面拿,不允许消费者到一个空的缓冲区取产品,也不允许生产者往满的缓冲区放数据。
具体一下,假设就来生产面包吧。
两个生产者分别生产5个面包,连个消费者,分别消费五个面包。
缓冲区暂定的容量也是五个吧。
接下来看一下面包Bread
类
public class Bread {
//面包的ID
private int ID;
//面包的名字
private String name;
//面包生产者的名字
private String produceName;
public Bread(int ID, String name, String produceName){
this.ID = ID;
this.name = name;
this.produceName = produceName;
}
public String getProduceName() {
return produceName;
}
public void setProduceName(String produc) {
this.produceName = produc;
}
public int getID() {
return ID;
}
public void setID(int ID) {
this.ID = ID;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "美味的" + name + ID + "号";
}
}
接下来是面包容器BreadColection
类
public class BreadColection {
//定义了面包的容器
private Bread[] con = new Bread[5];
//标记面包容器内当前的面包数量
private int breadNum = 0;
//存放面包得方法
public synchronized void storeBread(Bread bread){
while (breadNum >= 5){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
con[breadNum] = bread;
System.out.println(Thread.currentThread().getName() + "正在存储" + bread.toString());
breadNum++;
//唤醒所有线程
this.notifyAll();
}
//获取面包
public synchronized void takeBread(){
while (breadNum <= 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
breadNum --;
System.out.println(Thread.currentThread().getName() + "正在获取" + con[breadNum].toString() + "生产者是:" + con[breadNum].getProduceName());
con[breadNum] = null;
this.notifyAll();
}
}
主函数调用
public static void main(String[] args) {
//定义面包容器对象
BreadColection breadColection = new BreadColection();
//定义存储面包和获取面包的行为
Runnable store = new Runnable() {
@Override
public void run() {
for (int i = 0;i < 10;i++){
breadColection.storeBread(new Bread(i, "巧克力面包", Thread.currentThread().getName()));
}
}
};
Runnable take = new Runnable() {
@Override
public void run() {
for(int i = 0; i < 10; i++){
breadColection.takeBread();
}
}
};
//创建线程实现行为
Thread thread1 = new Thread(store, "生产者一号");
Thread thread2 = new Thread(store, "生产者二号");
Thread thread3 = new Thread(take, "消费者一号");
Thread thread4 = new Thread(take, "消费者二号");
//线程启动
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
执行结果:
2.总结
线程通信到现在就差不多结束了,这一部分,实现起来其实并不复杂,但是关键的是其背后所表达的思想,操作系统真的是绝绝子,下一篇可能 就是线程的高级应用了,诸如线程池之类的,都会进行介绍。