深入理解Java线程安全——银行欠我400万!!!

在阅读这篇文章之前,你需要了解线程创建过程中经由的几个状态,如果对于这些概念有一些模糊,没有关系,你一样可以看懂并且会使用这些有趣的方法!如果你需要对它们有足够的认识和理解,请戳下面的链接;

在这里插入图片描述

线程执行的过程状态图解

在了解线程安全之前,需要了解线程中一些与状态相关的方法,进而更完善的理解线程执行的整个过程中所经历的几种状态,深入理解线程不安全造成的原因;

线程中与状态相关的方法

1.休眠

 该方法主要作用是使当前线程主动休眠millis毫秒

方法描述
static void sleep(long millis)使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
public class TestRunnable {
    public static void main(String[] args){
        System.out.println("程序开始");
        MyRunnable runnable = new MyRunnable();
        //分别创建两个线程,并传入runnable参数
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        //运行线程
        thread1.start();
        thread2.start();
        System.out.println("程序结束");
    }
}
class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            //打印当前线程的名字和值
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述

2.放弃

 该方法主要作用是当前线程主动放弃时间片,回到就绪状态,竞争下一次的时间片;(通俗地就是当前线程好不容易等到系统随机分配到了时间片,在即将拿到使用的时候,它突然放弃了…蛮可惜的QAQ)

方法描述
static void yield()对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。

 在下列实例中,Task1做了放弃时间片的操作,具体形式是:每当i为5的倍数的时候,若当前状态恰好拿到时间片,则放弃使用时间片,放弃之后,因为是随机的,很可能被分配给其他线程,基于这个原因,该实例能看懂微小的变化,但是因为中途Task1做了放弃时间片的操作,最终Task1最后执行完毕的肯能行最大。

public class TestYield {
    public static void main(String[] args){
        Thread thread1 = new Thread(new Task1());
        Thread thread2 = new Thread(new Task2());
        thread1.start();
        thread2.start();
    }
}
class Task1 implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i < 100; i++) {
            System.out.println("Task1:"+i);
            if (i % 5 == 0){
                Thread.yield();//主动放弃时间片
            }
        }
    }
}
class Task2 implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i < 100; i++) {
            System.out.println("=====Task2:"+i);
        }
    }
}

在这里插入图片描述

3.结合

 将其他线程加入到当前线程,一旦加入,必须等待加入线程执行完毕之后,该线程才可以继续执行;

方法描述
void join()等待这个线程终止。
void join(long millis)等待这个线程终止最多millis毫秒。
void join(long millis, int nanos)等待最多 millis毫秒加上 nanos纳秒这个线程终止。
public class TestYield {
    public static void main(String[] args){
        Thread thread1 = new Thread(new Task());
        Thread thread2 = new Thread(new Task());
        //thread1.start();
        thread2.start();

        for (int i = 0; i <= 50 ; i++) {
            System.out.println("main :" +i);
            if(i == 30){
                try {
                    thread2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
class Task implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+" : "+i);
        }
    }
}

  • 打印结果:
    在这里插入图片描述

总结线程中的状态

在这里插入图片描述

线程安全

 举一个简单的案例:现有两个线程A和B,同时需要将所含字符串传入s数组的首位,两个线程竞争OS分发的时间片,拿到时间片的可以执行将字符串存入数组的首位的操作;

在这里插入图片描述
 第一次竞争:现在A拿到时间片(绿色的方格),假设当A在拿到时间片,准备将HELLO赋给s[1],但还未执行完操作,时间片到期,导致未完成赋值;
 第二次竞争:再次竞争时间片,假设线程B那拿到了时间片,而B在时间片到期之前完成了赋值,即将WORLD赋给s[1],此时s[1]不为空,线程B执行完毕;
 最后:只剩下线程A还未执行完毕,此时A拿到时间片在原来停止的地方继续执行,显然线程A准备将HELLO赋给s[1],这是后就会导致原本已存入WORLDs[1],被HELLO覆盖掉;


为什么会这样?

这种情况出现的原因主要是因为这里访问了同一个线程共享的对象;因此就会出现这种类似“争抢”的情况发生;这就是所谓的线程不安全

线程不安全

  • 当线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致;

临界资源: 共享资源(同一对象),一次允许一个线程使用,才可以保证其正确性。
原子操作:: 不可分割的多步操作,被视为一个整体,其顺序和步骤不能打乱或缺省(比如此处,A在执行过程中因时间片到期,却被B“插了队”,这就破坏了原子操作)

线程不安全案例——银行欠我400万!

 现妻子和丈夫共用一个账户,且余额2000万元,此时两人同时一时间输入统一账户密码进行取款,取款金额为1200万元整;
在这里插入图片描述

代码设计:

public class TestSynchronized {
    public static void main(String[] args){
        //开户存入2000元
        Account account = new Account("65455656","123456",2000);

        Husband husband = new Husband(account);
        Wife wife = new Wife(account);

        Thread thread1 = new Thread(husband);
        Thread thread2 = new Thread(wife);

        thread1.start();
        thread2.start();

    }
}
//丈夫
class Husband implements Runnable{
    Account account;

    public Husband (Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        System.out.println("Husband 开始取款");
        this.account.withDraw("65455656","123456",1200);
    }
}
//妻子
class Wife implements Runnable{
    Account account;

    public Wife(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        System.out.println("Wife 开始取款");
        this.account.withDraw("65455656","123456",1200);
    }
}

//银行账户
class Account{
    String cardNo;
    String password;
    double balance;

    public Account(String cardNo, String password, double balance) {
        this.cardNo = cardNo;
        this.password = password;
        this.balance = balance;
    }

    //取款
    public void withDraw(String no,String pwd,double money){
        System.out.println("正在读取账户信息,请稍后......");
        if(this.cardNo.equals(no) && this.password.equals(pwd)){
            System.out.println("验证成功!");
            if (money < balance){
                balance -= money;
                System.out.println("取款成功,当前余额为:"+balance);
            }else {
                System.out.println("卡内余额不足!");
            }
        }else {
            System.out.println("卡号或密码错误!");
        }
    }
}

执行结果:

在这里插入图片描述

原因叙述:

 由于丈夫妻子两个人取钱的动作是两个线程同时访问临界资源,导致一个线程执行过程中被其他线程“插入”,因此出现了银行余额-400的奇怪现象;
 说明两个线程同时验证成功,即在丈夫取钱的线程没有执行完毕而时间片到期的时候,妻子取钱的线程拿到时间片并验证成功进入了取钱状态;因为丈夫取钱的动作还未执行完毕,此时两者都满足if (money < balance)的条件,因此导致银行亏损400万元;哈哈哈,如果现实中这样,银行亏大了!
在这里插入图片描述

修复线程不安全——加锁

 每个对象都有一个互斥标记锁,用来分配给线程的。只有拥有对象互斥锁标记的线程才能进入该对象加锁的同步代码块。线程退出同步代码块时会释放相应的互斥锁标记。

synchronized (临界资源对象){ //对临界资源加锁
	//代码(原子操作)
}

在本例中进行线程安全的修改:

class Husband implements Runnable{
    Account account;

    public Husband (Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        synchronized (account){
            System.out.println("Husband 开始取款");
            this.account.withDraw("65455656","123456",1200);
        }
    }
}
class Wife implements Runnable{
    Account account;

    public Wife(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        synchronized (account){
            System.out.println("Wife 开始取款");
            this.account.withDraw("65455656","123456",1200);
        }
    }
}

分析代码:

  • 此时,只有拿到从acount对象拿到锁标记的线程才能执行取款操作(原子操作),而在执行过程中,如果说先拿到时间片和锁标记的线程A的时间片到期了(限期等待),而线程B拿到OS分配的时间片,但是此时锁标记在线程A身上,因此线程B就无法进行取款操作(阻塞状态),只能等待线程A取款完毕,释放锁标记才可以执行取款操作;

相似地,我们也可以不分别在Husband 和Wife类下加锁,而直接将锁标记加在Account类里面,即定义一个原子操作模块;
在这里插入图片描述
打印结果:
在这里插入图片描述

银行再也不会欠我钱了…

总结

线程执行过程中的所有状态

线程状态。 线程可以处于以下状态之一:

  • NEW (初始状态)
    尚未启动的线程处于此状态。
  • RUNNABLE (运行状态)
    在Java虚拟机中执行的线程处于此状态。
  • BLOCKED (阻塞状态)
    被阻塞等待监视器锁定的线程处于此状态。
  • WAITING (无限期等待)
    正在等待另一个线程执行特定动作的线程处于此状态。
  • TIMED_WAITING (有限期等待)
    正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
  • TERMINATED (终止状态)
    已退出的线程处于此状态。
    在这里插入图片描述
  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lukey Alvin

谢谢鼓励!越努力越幸运!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值