银行取款[多线程]{使用volatile修饰共享变量,但此场景并不保证线程同步}

简介: 经典例子:老婆(朱丽叶)老公(罗密欧),使用银行卡和存折,或者网银等,同时对同一账户操作的安全问题。 此处用多线程实现,同时取款的模拟实现,使用volatile修饰共享变量,但此场景并不保证线程同步,查看取款安全隐患问题,代码如下: 我学习地址(Thanks for auther): Java 理论与实践: 正确使用 Volatile 变量 java中volatile关键字的含义

经典例子:老婆(朱丽叶)老公(罗密欧),使用银行卡和存折,或者网银等,同时对同一账户操作的安全问题。

此处用多线程实现,同时取款的模拟实现,使用volatile修饰共享变量,但此场景并不保证线程同步,查看取款安全隐患问题,代码如下:

我学习地址(Thanks for auther):

Java 理论与实践: 正确使用 Volatile 变量

java中volatile关键字的含义

--------------------------------------------------------------------------------------------------------------------------------------

java语言内在两种同步机制:同步块(或方法)和 volatile 变量。都是为了实现代码线程的安全性。
在JVM运行内存中JVM栈中,对每个线程都分配一个线程栈(保存线程变量信息),当线程运行访问某个对象的值时,首先通过对象引用找到对应堆内存的变量的值,
然后把堆内存变量的具体值加载(load)到线程本地内存才能中,建立一个变量的副本。之后,线程就不再和对象在堆内存变量的值有关系,而直接修改副本变量的值,在修改修改副本变量的值0完的某一时刻(线程退出之前),
自动把线程变量副本的值回写到对象在堆中的变量中。这样堆中的对象的值就发生变化了。
普通变量:从主存读取(read)加载(load)变量值到当前工作内存,(执行代码,改变变量的值使用赋值写内存[use-asign-store])(可多次),最终工作内存数据(write)内存变量值,此并非原子性,如果主存的值发生修改,线程工作内存的值已经加载,不会产生改变,所以预期结果不一样。
volatile变量:JVM保证主内存加载到线程工作内存中的值时最新的。
线程1和线程2开始运行,读取加载volatile变量[如banlance],发现主存为banlance=500,则都加载最新这个值,
线程1修改banlance的值write到主存中,主存中的变为banlance=400,则线程2,由于已经进行read和load在进行运算后,也会更新主存banlance=400,即使用volatile修饰,还是会出现并发的问题。
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作。
锁两种主要特性:互斥(mutual exclusion)和可见性(visibility)。
互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,一次就只有一个线程能够使用该共享数据。
可见即它必须确保前线程释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的,如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
Volatile 变量具有synchronized的可见性特性,但是不具备原子特性。
Volatile 变量,出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。
volatile 变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。
在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。
volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值,volatile不会提供任何原子操作,它也不能用

--------------------------------------------------------------------------------------------------------------------------------------

银行账户:volatile int banlance

package com.tsxs.bank;
//银行账户
public class BankAccountVolatile {
	//volatile 余额
	private volatile int balance = 500;
	//查询
	public int getBalance(){
		return banlance;
	}
	//取款
	public void withdraw(int amount){
		banlance = banlance - amount;
	}
	//存款
	public void deposit(int amount){
		banlance = banlance + amount;
	}
}

线程方法:

package com.tsxs.syncmethods;

import com.tsxs.bank.BankAccountVolatile;

public class VolatileField implements Runnable{
	//所有Thread多线程线程都共享Runnable(接口对象)和account对象,volatile int banlance 
	private BankAccountVolatile account = new BankAccountVolatile();
	@Override
	public void run() {
		for(int i = 0; i< 5; i++){			//总共取款5次
			makeWithdraw(100);			//每次取款100
			if(account.getBalance() < 0){
				System.out.println(""+Thread.currentThread().getName()+"   透支了!");
			}
		}
	}
	
	/**
	 * makeWithdraw 账户取款
	 * @param amount 取款金额<br />
	 * 打印log记录取款过程
	 * */
	private void makeWithdraw(int amount){
		if(account.getBalance() >= amount){			//如果余额足够则取款
			System.out.println(""+Thread.currentThread().getName()+"   准备取款!");
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				System.out.println(Thread.currentThread().getName()+"   准备取款,等待0.5s线程中断!"+e.getMessage());
			}
			account.withdraw(amount);
			System.out.println(""+Thread.currentThread().getName()+"   完成"+amount+"取款!余额为"+account.getBalance());
		}else{			//余额不足则提示
			System.out.println(""+"余额不足以支付"+Thread.currentThread().getName()+amount+"   的取款,余额为"+account.getBalance());
		}
	}
}

测试代码:

package com.tsxs.test;

import org.junit.Test;

import com.tsxs.syncmethods.CodeLock;
import com.tsxs.syncmethods.NoSync;
import com.tsxs.syncmethods.SyncBlock;
import com.tsxs.syncmethods.SyncMethod;
import com.tsxs.syncmethods.VolatileField;

public class TreadSyncTest {

//	@Test
//	public void test() {
/*Junit不适合多线程并发测试。
    因为线程还在激活状态的时候,Junit已经执行完成。
	在Junit的TestRunner中,它没有被设计成搜寻Runnable实例,
	并且等待这些线程发出报告,它只是执行它们并且忽略了它们的存在。
	综上,不可能在Junit中编写和维护多线程的单元测试。
}*/	
	public static void main(String[] args) {
		//实现Runnable:所有Thread多线程线程都共享Runnable(接口对象)
//		NoSync target =new NoSync();
//		SyncMethod target = new SyncMethod();
//		SyncBlock target = new SyncBlock();
//		CodeLock target = new CodeLock();
		VolatileField target = new VolatileField();
		//创建李琦和他老婆两个线程实现取款(同时)
		Thread lq = new Thread(target);
		lq.setName("罗密欧");
		Thread lqwf = new Thread(target);
		lqwf.setName("朱丽叶");
		//调用Thread对象的start()方法,启动线程,执行run()方法(OS)
		lq.start();
		lqwf.start();
	}
}

 

测试结果:

罗密欧   准备取款!
朱丽叶   准备取款!
罗密欧   完成100取款!余额为300
朱丽叶   完成100取款!余额为300
罗密欧   准备取款!
朱丽叶   准备取款!
罗密欧   完成100取款!余额为100
朱丽叶   完成100取款!余额为100
罗密欧   准备取款!
朱丽叶   准备取款!
朱丽叶   完成100取款!余额为0
余额不足以支付朱丽叶100   的取款,余额为0
余额不足以支付朱丽叶100   的取款,余额为0
罗密欧   完成100取款!余额为0
余额不足以支付罗密欧100   的取款,余额为0
余额不足以支付罗密欧100   的取款,余额为0


分析结果,可见:

双线程总共取款10次,账户总额为500.

取款结果:取款成功600元,余额显示不对,取款流程不对。

现实中,此取款代码是有严重bug的,上边数据对于银行是不安全的,个人也会带来不必要的麻烦。

目录
相关文章
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
23 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
38 2
|
29天前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
62 0
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
58 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
27 3
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
44 1
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
49 1
|
2月前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
31 1
|
3月前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
2月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
79 6