多线程之间的通讯(仿真生产与消费)

源于蚂蚁课堂的学习,点击这里查看(老余很给力)

java内存模型(jmm)

java内存模型分为两大类型即,主内存和本地内存
1.主内存
    也就是主进程所占用的内存
2.本地内存
    线程中开辟的属于线程自己的内存,其中存放着全局变量的副本数据
    也就是全局共享的数据在主内存中的,线程中复制一份放入自己的本地内存,线程执行结束后将其变动刷新至主内存。
    这也就是为什么多线程会有线程安全的问题所在。
    多个线程同时做了修改,都去刷新主内存,会造成结果和实际不一致。

 volatile关键字

其功能为多线程可见。
怎么说呢?实际上就是当线程在本地内存中修改了被volatile修改的变量后,会立刻将其值刷新至主内存中,其他线程
也会马上得到这个修改的结果
实际看上去就像线程在直接修改主线程中的这个变量一样。

还有就是防止指令重新排序
代码自上而下执行,这是常识,但是,计算机执行时,可能会将,没有依赖关系的代码指令的执行顺序打乱,但一般不会
影响结果。
如:int a=1;int b=1;这时无论怎么变化这两条指令,都没有影响。
如果再来一条int c=a+b;就会相互之间有了依赖关系,计算机就不会将其重新排序
指令并不是代码行,指令是原子的,通过javap命令可以看到一行代码编译出来的指令,当然,像int i=1;这样的代码行也
是原子操作。

而对于那些比较隐晦的,指令重拍可能会引发问题得数据,volatile关键字可以指定其对应的代码不会参与重新排序

 案例

package live.yanxiaohui;

/**
 * @Description 仿真消息的生产和消费(多线程之间的通讯),即来一个消息,消费一条消息
 * @CSDN https://blog.csdn.net/yxh13521338301
 * @Author: yanxh<br>
 * @Date 2020-05-15 11:08<br>
 * @Version 1.0<br>
 */
public class Test2 {
    public static void main(String[] args) {
        Money money = new Money();
        new Task1(money).start();
        new Task2(money).start();
    }
}

class Task1 extends Thread{
    private Money money;

    private int count;
    public Task1(Money money) {
        this.money = money;
    }

    @Override
    public void run() {
        while (true){
            if(count % 2 ==0){
                money.name = "马云";
                money.componet = "阿里";
            }else {
                money.name = "马化腾";
                money.componet = "腾讯";
            }
            count++;
        }
    }
}

class Task2 extends Thread{
    private Money money;

    public Task2(Money money) {
        this.money = money;
    }

    @Override
    public void run() {
        while (true){
            System.out.println(money.name + "," + money.componet);
        }
    }
}

class Money{
    public String name;
    public String componet;
}

首先我们运行程序,会发现数据错乱的现象

这是由于多个线程访问全局共享数据造成的线程安全问题。
说人话:
写入的线程做了修改,将本地内存改动的数据要同步至主内存,刚好同步一半,这时被挂起,
读线程从主内存读取数据就是一个错误的数据。

 那么加锁如何?

对共享的money对象加锁。关键代码如下
写入操作:
           synchronized (money){
               if(count % 2 ==0){
                   money.name = "马云";
                   money.componet = "阿里";
               }else {
                   money.name = "马化腾";
                   money.componet = "腾讯";
               }
           }

读操作:
            synchronized (money){
                System.out.println(money.name + "," + money.componet);
            }

 一定会有读者有疑问:读为什么还要加锁?

此处对数据的修改和读取是两个变量,这两个变量需要保证其原子性,否则依旧是可能出现线程安全的
比如:如果读不加锁,那么其读到第一个属性值后,被挂起,写入的线程做了修改,读线程继续执行后
就会出现第二个属性和第一个属性不同步的现象

 运行程序

数据是保证一致性了,但依然不满足我们消费的机制,所以需要再优化下 

我们可以使用wait和notify配合进行多个线程之间对同一共享的全局数据进行通讯


package live.yanxiaohui;

/**
 * @Description 仿真消息的生产和消费(多线程之间的通讯),即来一个消息,消费一条消息
 * @CSDN https://blog.csdn.net/yxh13521338301
 * @Author: yanxh<br>
 * @Date 2020-05-15 11:08<br>
 * @Version 1.0<br>
 */
public class Test2 {
    public static void main(String[] args) {
        Money money = new Money();
        new Task1(money).start();
        new Task2(money).start();
    }
}

class Task1 extends Thread{
    private Money money;

    private int count;



    public Task1(Money money) {
        this.money = money;
    }

    @Override
    public void run() {
        try{
            while (true){
                synchronized (money){
                    // 方便查看效果,设置阻塞时间
                    Thread.sleep(100);
                    if(!money.success){
                        // 交出对象的使用权,等待消息消费
                        money.wait();
                    }
                    if(count % 2 ==0){
                        money.name = "马云";
                        money.componet = "阿里";
                    }else {
                        money.name = "马化腾";
                        money.componet = "腾讯";
                    }
                    money.success = false;
                    // 唤醒此对象所有等待的线程
                    money.notify();
                }
                count++;
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

class Task2 extends Thread{
    private Money money;

    public Task2(Money money) {
        this.money = money;
    }

    @Override
    public void run() {
        try {
            while (true){
                synchronized (money){
                    if(money.success){
                        // 交出对象的使用权,等待消息生产
                        money.wait();
                    }
                    System.out.println(money.name + "," + money.componet);
                    money.success = true;
                    // 唤醒此对象所有等待的线程
                    money.notify();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Money{
    public String name;
    public String componet;
    // true表示可以写,false表示可以读
    public boolean success;
}

 

 由此可以看出,线程执行是抢夺CPU分配权的,和主线程代码的执行顺序没太大关系

我们可以使用lock锁,去替代sync 

package live.yanxiaohui;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Description 仿真消息的生产和消费(多线程之间的通讯),即来一个消息,消费一条消息
 * @CSDN https://blog.csdn.net/yxh13521338301
 * @Author: yanxh<br>
 * @Date 2020-05-15 11:08<br>
 * @Version 1.0<br>
 */
public class Test3 {
    public static void main(String[] args) {
        Money money = new Money();
        new Task1(money).start();
        new Task2(money).start();
    }


}

class Task1 extends Thread {
    private Money money;

    private int count;


    public Task1(Money money) {
        this.money = money;
    }

    @Override
    public void run() {
        while (true) {
            try {
                // 对象上锁
                money.lock.lock();
                // 方便查看效果,设置阻塞时间
                Thread.sleep(100);
                if (!money.success) {
                    // 交出对象的使用权,等待消息消费
                    money.condition.await();
                }
                if (count % 2 == 0) {
                    money.name = "马云";
                    money.componet = "阿里";
                } else {
                    money.name = "马化腾";
                    money.componet = "腾讯";
                }
                money.success = false;
                // 唤醒此对象所有等待的线程
                money.condition.signal();
                count++;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 释放锁
                money.lock.unlock();
            }
        }
    }
}

class Task2 extends Thread {
    private Money money;

    public Task2(Money money) {
        this.money = money;
    }

    @Override
    public void run() {
        try {
            while (true) {
                // 对象上锁
                money.lock.lock();
                if (money.success) {
                    // 交出对象的使用权,等待消息生产
                    money.condition.await();
                }
                System.out.println(money.name + "," + money.componet);
                money.success = true;
                // 唤醒此对象所有等待的线程
                money.condition.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            money.lock.unlock();
        }
    }
}

class Money {
    public String name;
    public String componet;
    // true表示可以写,false表示可以读
    public boolean success;

    public Lock lock = new ReentrantLock();

    public Condition condition = lock.newCondition();
}

运行结果也是相同

总结

1.多线程之间实现通讯基于对全局共享数据的锁定,即多个线程都必须去争夺锁,获取锁之后依照业务进行对应的等待或运行。
2.wait和notify必须要配合synchronized使用, 且必须要在同步代码块中执行
3.lock锁因为是手动添加的,所以它可控,实际开发中可用到的最多
4.wait底层是将对象的锁释放掉,自身线程进入阻塞等待状态,直到被对象的notify唤醒
5.sleep只是线程的定时阻塞,不会释放锁
6.volatile指定全局共享变量在内存模型中线程间数据的可见性
7.volatile还可以防止指令重新排序
原创文章 148 获赞 257 访问量 11万+

猜你喜欢

转载自blog.csdn.net/yxh13521338301/article/details/106137976