java中线程安全问题分析及解决

学完线程之后,我们可以知道在java中我们是可以通过线程来同时去操作运行的,其应用范围非常广,但是由此引发了一个问题:假如有两个人同时去银行取钱,余额有一万元,两人通过不同平台同时取一万元。试想一下,此时程序判定取钱余额都是满足取钱金额的,因此返回ture,两个人同时取出钱,这不就出现bug了吗?这样我们岂不是可以同时取好多的钱出来?银行不就倒闭了?因此在这基础上有了锁的概念,这里顺便练习一下锁的知识还有复习线程的知识

线程同步问题分析

首先我们需要创建一个账户对象,用来保存账户的余额,卡号信息,再定义一个线程类,创建两个线程,分别表示两个人同时取钱,再启动线程

Account类:

public class Account {

    private String cardId;//卡号
    private double money;//余额

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    public Account() {
    }

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }



}

接着我们创建进程类来创建进程,这里涉及到一个问题,我们要让两个人同时取一个账户的钱,那么需要传入这个账户的参数进去,因此我们在进程类里创建一个有参构造器,用来传输账户类,到时候好表示同一个账户

package com.ljx.thread_safe;

public class ThreadTest {
    public static void main(String[] args) {
        //创建账户对象,代表两个人共享账户
        Account acc = new Account("ICBC-110",10000);
        //创建两个线程,分别代表两个人,再同时去取钱
        //创建有参构造器,用来表示这个账户对象
        new DrawThread(acc,"no1").start();//no1人
        new DrawThread(acc,"no2").start();//no2人

    }
}
public class DrawThread extends Thread{

    //由于用有参构造器创建对象时,我们接收到账户对象需要去调用run函数,因此要创建一个成员变量接收
    private  Account acc;

    public DrawThread(Account acc,String name){
        super(name);//Thread提供有参构造器,将name传入进程中,可以得到名称,方便我们判断是谁取得钱
        this.acc = acc;

    }
    @Override
    public void run() {
        acc.drawMoney(10000.0);
    }
}

这里我们创建了drawthread方法,重新在账户类里创建:

   public void drawMoney(double i) {
        //判定谁先取钱----》进程对象中传入name参数,用no代表编号----》行程类中多传入一个参数,并用父类的构造器传入参数,通过threadcurrent获取当前线程的名称
        String name = Thread.currentThread().getName();
        //判断余额是否足够
        if(money>=i){
            System.out.println((name + "来取钱" + i + "成功"));
            //计算取完钱的余额
            money=money-i;
            System.out.println(("余额还有:" + money));
        }
        else{
            System.out.println((name + "来取钱,余额不足"));
        }

    }

最后结果出现冲突,两个人同时取钱,引发了线程同步问题

线程同步问题解决方法

从上面实现案例可以发现,两个人同时取钱,会引发线程同步问题,那有没有什么办法可以避免呢?当然有,我们要创建一个锁,只能允许一个人访问,当有一个人访问的时候,我们将进程锁住,不让别的人进来,因此规避了两个人同时访问的问题。这在操作系统中有提及——互斥锁

通俗的讲就是将异步转换为同步

同步代码块

同步代码块就是其中一个加锁的方法,其作用是把共享资源核心代码上锁,以此保证线程安全

synchronized(同步锁){
    访问共享资源核心代码
}

注意:对于当前执行的线程来说,同步锁必须是同一个对象

public void drawMoney(double i) {
        //判定谁先取钱----》进程对象中传入name参数,用no代表编号----》行程类中多传入一个参数,并用父类的构造器传入参数,通过threadcurrent获取当前线程的名称
        String name = Thread.currentThread().getName();
        //判断余额是否足够
        synchronized ("suo") {
            if (money >= i) {
                System.out.println((name + "来取钱" + i + "成功"));
                //计算取完钱的余额
                money = money - i;
                System.out.println(("余额还有:" + money));
            } else {
                System.out.println((name + "来取钱,余额不足"));
            }
        }
    }

更改如上;对于两个人对象来说。suo是一个常量对象,当成锁对象。

最后结果发现两个进程不会冲突了

但但但是!!!我们发现,这个锁对象,不单单可以锁住这两个人,假如我们再创建几个对象,从不同的账户中取钱,就也会被锁住!!这是因为我们没有区分其他账户对象。试想一下,如果这样的话你在你的账户里取钱,别人去自己的账户取钱还要等你取完才能取,是不是很不符合逻辑?

因此我们设立锁对象还要让两个人共享资源作为锁

java中有推荐的方案:

public void drawMoney(double i) {
        //判定谁先取钱----》进程对象中传入name参数,用no代表编号----》行程类中多传入一个参数,并用父类的构造器传入参数,通过threadcurrent获取当前线程的名称
        String name = Thread.currentThread().getName();
        //判断余额是否足够
        synchronized (this) {
            if (money >= i) {
                System.out.println((name + "来取钱" + i + "成功"));
                //计算取完钱的余额
                money = money - i;
                System.out.println(("余额还有:" + money));
            } else {
                System.out.println((name + "来取钱,余额不足"));
            }
        }
    }

应将锁对象改为this,我们再创建多个对象去调用别的账户

发现已经成功了

扩充:如果是静态方法的话,同步锁改为类名.class,静态方法是通过类名调用,所以用类名.class锁住所有线程

public static void test(){
        synchronized (Account.class){
            
        }
    }

同步方法

除了同步代码块之外,我们可以设立同步方法,在方法的修饰词里加上synchronized

public synchronized void drawMoney(double i) {
        //判定谁先取钱----》进程对象中传入name参数,用no代表编号----》行程类中多传入一个参数,并用父类的构造器传入参数,通过threadcurrent获取当前线程的名称
        String name = Thread.currentThread().getName();
        //判断余额是否足够
        
            if (money >= i) {
                System.out.println((name + "来取钱" + i + "成功"));
                //计算取完钱的余额
                money = money - i;
                System.out.println(("余额还有:" + money));
            } else {
                System.out.println((name + "来取钱,余额不足"));
            }
        }

总结:同步代码块和同步方法其实类似,但是(省略)代码块范围小,功能相对来说强一点,方法范围大,功能相对弱

Lock锁

创建一个Lock对象进行加锁和解锁,更灵活,更强大

//定义一个锁对象,reentrantlock为实现类,用final修饰,确保一个账户类只有一把锁
    private final Lock lk = new ReentrantLock();
public void drawMoney(double i) {
        //判定谁先取钱----》进程对象中传入name参数,用no代表编号----》行程类中多传入一个参数,并用父类的构造器传入参数,通过threadcurrent获取当前线程的名称
        String name = Thread.currentThread().getName();

        try {//如果中间出错,程序可能执行不到unlock,导致后续线程无法启动
            //用try catch可以确保出错后任然解锁
            lk.lock();//加锁

            //判断余额是否足够
            if (money >= i) {
                System.out.println((name + "来取钱" + i + "成功"));
                //计算取完钱的余额
                money = money - i;
                System.out.println(("余额还有:" + money));
            } else {
                System.out.println((name + "来取钱,余额不足"));
            }
        }catch(Exception e){
                e.printStackTrace();
            }finally {
            lk.unlock();//解锁
        }
    }

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值