(十九)线程安全问题及线程间通讯

文章讲述了多线程环境下可能出现的安全问题,如停车场的例子所示,可能导致数据不一致。线程安全是通过锁机制来保证的,包括同步代码块和同步方法。死锁是线程互相等待对方资源导致的僵局,避免方式是避免嵌套同步和确保锁对象的选择。此外,介绍了生产者-消费者模式,通过缓冲区降低生产者和消费者之间的耦合,提高系统效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        先简单描述一个问题,现在停车场还有一个空位,但是停车场的东西两个入口都正在有车要进入,这两个入口显示剩余车辆只有一个,所以是允许车进入的,所以这两辆车都进了停车场,最后的结果就是有一辆车没有车位。

        这个例子可能后果不是很严重,没车位而已,那如果换成银行呢,账户余额是10000块钱,你拿着卡去atm取钱,同时在手机银行给别人转账,同时进行,atm上查询到你的余额是10000,你要取10000,同时你在手机银行给别人发起了转账,由于你的余额是10000,所以钱是够得,可以转账。

        ATM上你取完10000之后你的余额还剩10000-10000=0元。但是这个结果还没有上传到服务器之前,服务器的余额还是10000,所以手机端转账也发起了,剩余余额还是10000,可以转账,转账之后结果是10000-10000也是0元,这是ATM将余额修改为0,并成功上传,这时手机银行也对余额进行了修改,要修改为0.所以你10000的余额成功的让银行亏损10000。这个例子的后果是不是很严重了。

线程安全问题

        经过上面的例子我们可以知道,多线程同时对同一数据进行操作就有可能产生问题。既然出现问题的原因是多线程对同一数据同时进行操作,所以我们解决这个问题的思路也很简单,就是多线程不能对同一数据同时操作,要让他们在对同一数据进行操作时必须保持一定的顺序先后进行。

解决思路

        在多线程并发时,我们对它们要修改的数据上一把锁,在其中一个线程对这个数据进行修改时,这把锁锁上,其他线程将不能对这个数据进行操作。

        这里要提出一个概念:锁对象。锁对象就是我们在操作数据时上的那一把锁。有几个注意事项:

        1.锁对象必须是针对这个数据进行操作的所有线程所共有的对象。

        2.锁对象可以是任何对象。

        总结:锁对象可以是这些线程共有的任何对象,就类似于大铁锁可以锁门,密码锁可以锁门,指纹锁也可以锁门,是什么对象不重要,只要是这些线程共有的就可以。

解决方案

同步代码块

        语法:

synchronized(锁对象){
    代码区
}

        代码区中的代码就是要被锁住的代码,在这一处的代码将只能被一个线程执行,其他线程想要执行这一处的代码,需要等正在执行此处代码的线程执行完毕之后才可以执行。

同步方法

        语法:

访问权限修饰符 synchronized 返回值类型 方法名(形参列表){
    方法体
}

        在同步方法中的代码就是被锁住的,谁调用同步方法,锁对象就代表谁。所以同步方法的锁对象就是this。

同步静态方法

语法:

访问权限修饰符 static synchronized 返回值类型 方法名(形参列表){
    方法体
}

        同步静态方法中的方法体就是要被锁住的代码。 同步静态方法的锁对象就是这个类的类对象。

        类对象,是一个对象在加载的时候jvm自动生成的对象,而不是通过new关键字加构造函数创建的对象。一个类只有一个类的对象,想要获取一个类的类对象,可以使用类名.class或者对象名.getClass()获取。

死锁

        死锁是指多个线程互相持有对方资源,使线程无法结束也无法从线程锁中出来的问题。

举例:

public class Test{
    public static void main(String[] args){
        Object obj1 = new Object();
        Object obj2 = new Object();

        Thread t01 = new Thread(){
            public void run(){
                synchronized(obj1){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("线程1的外锁,可以打印出来");
                    synchronized(obj2){
                        System.out.println("线程1的内锁,肯定打印不出来");
                    }
                }
            }
        };
        Thread t02 = new Thread(){
            public void run(){
                synchronized(obj2){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("线程2的外锁,可以打印出来");
                    synchronized(obj1){
                        System.out.println("线程2的内锁,肯定打印不出来");
                    }
                }
            }
        };
        t01.start();
        t02.start();
    }
}

         这里简单解释一下上面的代码以及运行的结果。

        线程1的外锁是用obj1锁住的,内锁是obj2锁住的,线程2恰好相反,外锁是用obj2锁住的,内锁是obj1锁住的.

        如果线程1抢到了时间片,开始执行,进入到了外锁之中,所以obj1这把锁就锁上了,然后线程1开始了休眠,线程2开始执行,线程2进入外层锁之后obj2就锁住了,然后线程2开始休眠。

        当线程1结束休眠之后开始尝试进入内锁,但是内锁是obj2,已经被线程2锁住了。线程2结束休眠之后开始尝试进入内锁,内锁是obj1,已经被线程1锁上了,所以无法进入内锁。

        因此,线程1 和 线程2 就一直在它们的内锁外面等待这个锁打开,所以这个程序一直无法结束,也无法继续运行下去。这就是死锁。

        要避免死锁只能是我们尽量不手动的在同步中套同步,如果同步嵌套,一定要分析代码逻辑是否会形成死锁,最后,锁对象一定要自定义一个对象充当锁对象,不要随便找个str或者其他对象。

线程间通讯

        线程间通讯的作用就是线程之间相互传递简单的消息。

        线程间通讯的所有方法都是Object提供的,而且线程间通讯的方法只能在同步代码块、同步方法或同步静态方法中调用。

常用方法

public final void wait();

        让当前线程开始无限期休眠。

public final native void wait(long timeout);

        让当前线程开始休眠一定的时间,传入的参数就是要休眠的时间,单位是毫秒

public final void wait(long timeout, int nanos)

        让当前线程开始休眠一定的时间,传入的参数就是要休眠的时间,一参是毫秒,二参是纳秒。最终要休眠的时间就是一参和二参经过单位换算之后的总时间。

public final native void notifyAll();

        唤醒以调用这个方法的对象为锁对象的所有线程。

public final native void notify();

        随机唤醒一个以调用这个方法的对象为锁对象的线程。

        在这里我们看到wait()的作用是休眠,上一篇文章也介绍了一个休眠的方法sleep();这里简单介绍一下它们的区别。

  1. wait方法让线程休眠之后会释放当前线程所占有的线程锁,sleep休眠之后不会释放当前线程占有的线程锁。就类似于在卫生间上厕所,wait是上厕所的人在睡着之前把锁打开,然后在卫生间睡着了。sleep就相当于上厕所的人在卫生间睡着了,锁还没打开,外面的人不管有多着急也只能等。
  2. wait只能在同步中调用,sleep没有要求
  3. wait是Object提供的,只能由锁对象调用,sleep是Thread提供的静态方法,没有这个要求。

        另外notify和notifyAll的唤醒机制也简单解释一下:notify在唤醒时只能唤醒由调用notify这个方法的锁对象中的wait方法睡眠的对象。例如:

        上面的死锁代码中线程1和线程2都有obj1和obj2这两个锁对象,如果线程1的休眠是obj1.wait()导致的休眠,那线程2只能用obj1.notify()或者obj1.notifyAll()唤醒。

        如果线程2调用obj2.notify()或者obj2.notifyAll(),由于线程1调用睡眠方法的锁对象和线程2调用唤醒方法的锁对象不是一个对象,所以线程2将不能唤醒线程1.

生产者与消费者模式

        在实际的软件开发中,我们将生产数据的模块称为生产者,将处理数据的模块称为消费者。生产者消费者模式就是在二者之间加入一个缓冲区,降低二者的耦合度。

        生产者不需要考虑消费者消耗数据的速度,只需要关注缓存区剩余的空间大小,如果生产的数据塞满了缓冲区,就释放一定的资源,提高消费者消费数据的速度。

        消费者不需要考虑生产者生产数据的速度,只需要关注缓存区剩余的空间大小,如果缓冲区中的数据空了,就释放一定的资源让生产者生产数据。

        用现实生活中工厂为例子,我们在工厂中设定一个仓库,用来存放工人生产的产品,工人负责生产,只需要考虑一下仓库是否被塞满了,当仓库塞满之后就可以放假,等仓库有空间可以存放货物之后再进行生产。

        市场部的销售人员只负责卖掉仓库中的产品,如果仓库中的产品全部空了之后就可以放假,等待工人生产出新的产品。

        用代码模拟一下这个过程。

在这个过程中有四个类

工人 销售 工厂 环境类

工厂

        仓库中剩余的产品数量

        仓库中最大能容纳的产品数量

        生产产品的方法

        出售商品的方法

工人

        去上班的工厂

        可以去工厂生产产品

销售

        去上班的工厂

        卖工厂的东西

环境类

       用代码实现

class Factory{
    private int num = 0;
    private final int MAX = 100;

    public synchronized void produce(){
        
        //如果产品的数量等于仓库的最大容量,就不生产了,开始放假
        if(num >= MAX){
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }else{
            //仓库没满,就赶快干活
            num++;
            //仓库中又生产出了产品,叫销售回来卖东西。
            this.notifyAll();
        }
    }

    public synchronized void sell(){
        //如果仓库中没有产品,销售人员可以放假。
        if(num == 0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }else{
            //仓库中还有产品,销售抓紧速度
            num--;
            //仓库中又卖出东西了,可以让工人开始生产了
            this.notifyAll();
        }
    }
}

class ProduceRunnable implements Runnable(){
    //工人上班的工厂,只有在一个工厂上班的员工才能对同一个仓库进行操作。
    private Factory factory;
    public ProduceRunnable(Factory factory){
        this.factory = factory;
    }
    public void run(){
        while(true){
            factory.produce();
        }
    }
}
class SellRunnable implements Runnable(){
    //销售上班的工厂,只有在一个工厂上班的员工才能对同一个仓库进行操作。
    private Factory factory;
    public ProduceRunnable(Factory factory){
        this.factory = factory;
    }
    public void run(){
        while(true){
            factory.sell();
        }
    }
}

        在环境类中创建几个工人对象和销售对象,创建一个工厂对象,并指定这些员工上班的工厂是同一个工厂。然后开始运行,就可以起到我们预期的效果,我们要做的就根据程序运行的情况,调整工人和销售的数量即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值