当前位置:   article > 正文

Java高并发——使用Lock锁实现并发安全_java 钱包 业务 锁

java 钱包 业务 锁

前言

并发,在一个成熟的系统中是必不可少的,这也是广大程序猿探讨的热点,高并发下的数据安全尤为重要。博主最近也在巩固这方面的知识,特此整理一下博客,做一下记录。

什么是并发?并发有哪些问题?

提到并发,就不得不提到线程,关于多线程想必大家都知道,如果一个程序开启多个线程,执行多个任务,那么我们就说这个程序存在并发。

并发场景下,最需要注意的问题就是数据安全性,即线程安全,那么什么是线程安全呢?

现在我们模拟一下银行转账的过程,假设要给转入账户增加金额:

第一步,读取转入账户的余额

第二步,增加转入的钱

第三步,将新的余额存入

如果两个线程同时在操作这一个账户,也就是说两个人同时向同一个账户转账的情况下,可能线程1执行完了第一步和第二步,但是还没有执行第三步的时候失去了CPU资源,然后线程2获得了运行权并且修改了转入账户的钱,然后线程1又被唤起,继续执行第三步……这样一来,总金额肯定是不正确的,压根儿就不是一个原子操作。

所以,这时候我们就需要采用锁机制来保证同步,即某些操作只允许一个线程操作,不允许多个线程同时进行的情况出现。

没有锁的并发实例

现在,我们用代码模拟银行转账的过程,假设一个银行有100个账户,每个账户有1000元的金额,创建多个线程随机转账,那么理想的情况下,银行的总金额应该是100x1000=100000元,以下是不加锁的情况:

银行业务类——Bank.java

  1. package com.shuixian.jianghao.utils;
  2. import java.text.DecimalFormat;
  3. import java.util.Arrays;
  4. /**
  5. * 银行业务类
  6. * @author 秋枫艳梦
  7. * @date 2019-05-07
  8. * */
  9. public class Bank {
  10. //账户数组
  11. private final double[] accounts;
  12. private DecimalFormat decimalFormat = new DecimalFormat("#.00");
  13. /**
  14. * 构造函数
  15. * @param n accounts数组的长度
  16. * @param initialBalance 每个账户的钱款数
  17. * */
  18. public Bank(int n,double initialBalance){
  19. accounts=new double[n];
  20. Arrays.fill(accounts,initialBalance);
  21. }
  22. /**
  23. * 从一个账户向另一个账户转账
  24. * @param from 转出账户,对应数组中的元素
  25. * @param to 转入账户
  26. * @param amount 转账金额
  27. * */
  28. public void transfer(int from,int to,double amount){
  29. try {
  30. //转出账户的钱不够,结束
  31. if (accounts[from]<amount){
  32. return;
  33. }
  34. //对转出账户进行扣钱
  35. accounts[from]-=amount;
  36. //对转入账户进行加钱
  37. accounts[to]+=amount;
  38. System.out.printf(Thread.currentThread()+"从用户 %d 转出 %10.2f 到用户 %d",from,amount,to);
  39. System.out.println("\t当前银行所有用户的总余额:"+getTotalBalance());
  40. }catch (Exception e){
  41. }finally {
  42. }
  43. }
  44. /**
  45. * 获取当前银行所有账户的余额之和
  46. * @return 银行的总余额
  47. * */
  48. public String getTotalBalance(){
  49. double sum=0;
  50. for (double a : accounts){
  51. sum+=a;
  52. }
  53. return decimalFormat.format(sum);
  54. }
  55. /**
  56. * 获取账户的数量,用于随机选取转入账户
  57. * @return 数组长度
  58. * */
  59. public int size(){
  60. return accounts.length;
  61. }
  62. }

 测试类——BankTest.java

  1. package com.shuixian.jianghao.utils;
  2. /**
  3. * 没有锁的测试类,此时无法保证并发安全
  4. * @author 秋枫艳梦
  5. * @date 2019-05-07
  6. * */
  7. public class BankTest {
  8. //模拟100个账户
  9. public static final int ACCOUNTS_SIZE=100;
  10. //假设每个账户1000元,那么并发安全的情况下,银行的总余额应该始终是100000元
  11. public static final double INIT_BALANCE=1000;
  12. //假设转账金额的上限是1000元
  13. public static final double MAX_AMOUNT=1000;
  14. //休眠时间
  15. public static final int DELAY=10;
  16. public static void main(String[] args) {
  17. //实例化一个有100个账户、每个账户初始余额为1000元的银行
  18. Bank bank=new Bank(ACCOUNTS_SIZE,INIT_BALANCE);
  19. //开启100个线程
  20. for (int i = 0; i < ACCOUNTS_SIZE; i++) {
  21. //转出账户
  22. int fromAccount=i;
  23. //构造线程
  24. Runnable runnable=() ->{
  25. try {
  26. while (true){
  27. //随机获取一个转入账户
  28. int toAccount=(int)(bank.size()*Math.random());
  29. //随机获取转账金额
  30. double amount=MAX_AMOUNT*Math.random();
  31. //执行转账
  32. bank.transfer(fromAccount,toAccount,amount);
  33. //模拟耗时
  34. Thread.sleep((long)(DELAY*Math.random()));
  35. }
  36. }catch (InterruptedException e){
  37. }
  38. };
  39. Thread thread=new Thread(runnable);
  40. thread.start();
  41. }
  42. }
  43. }

 代码如上,结合注释应该不难理解,我们开启100个线程,操作同一个Bank对象,不停的随机从数组中抽取转出账户和转入账户,进行转账,那么能不能保证总金额永远是100000呢?看一下运行结果:

 可以看到,没过多久就出现错误了,这显然是线程不安全的,在真正的系统中是要杜绝这种情况出现的,要不然你的用户就要跟你撕逼了,甚至把你告上法庭……

Lock锁实现同步机制

我们再通过锁机制来实现一下。

关于锁,Java中提供了好几种,最常见的是synchronized关键字,不过这种方式的锁不够灵活,锁粒度也比较大,所以这里我们先采用Java提供的Lock锁来实现,其他的锁在以后的博文介绍。

改动一下我们的Bank类:

  1. package com.shuixian.jianghao.utils;
  2. import java.text.DecimalFormat;
  3. import java.util.Arrays;
  4. import java.util.concurrent.locks.Condition;
  5. import java.util.concurrent.locks.Lock;
  6. import java.util.concurrent.locks.ReentrantLock;
  7. /**
  8. * 银行业务类
  9. * @author 秋枫艳梦
  10. * @date 2019-05-07
  11. * */
  12. public class Bank {
  13. private final double[] accounts;
  14. private Lock myLock=new ReentrantLock();
  15. private DecimalFormat decimalFormat = new DecimalFormat("#.00");
  16. /**
  17. * 构造函数
  18. * @param n accounts数组的长度
  19. * @param initialBalance 每个账户的钱款数
  20. * */
  21. public Bank(int n,double initialBalance){
  22. accounts=new double[n];
  23. Arrays.fill(accounts,initialBalance);
  24. }
  25. /**
  26. * 从一个账户向另一个账户转账
  27. * @param from 转出账户,对应数组中的元素
  28. * @param to 转入账户
  29. * @param amount 转账金额
  30. * */
  31. public void transfer(int from,int to,double amount){
  32. //锁上
  33. myLock.lock();
  34. try {
  35. //转出账户的钱不够,结束
  36. if (accounts[from]<amount){
  37. return;
  38. }
  39. //对转出账户进行扣钱
  40. accounts[from]-=amount;
  41. //对转入账户进行加钱
  42. accounts[to]+=amount;
  43. System.out.printf(Thread.currentThread()+"从用户 %d 转出 %10.2f 到用户 %d",from,amount,to);
  44. System.out.println("\t当前银行所有用户的总余额:"+getTotalBalance());
  45. }catch (Exception e){
  46. }finally {
  47. //释放锁
  48. myLock.unlock();
  49. }
  50. }
  51. /**
  52. * 获取当前银行所有账户的余额之和
  53. * @return 银行的总余额
  54. * */
  55. public String getTotalBalance(){
  56. double sum=0;
  57. for (double a : accounts){
  58. sum+=a;
  59. }
  60. return decimalFormat.format(sum);
  61. }
  62. /**
  63. * 获取账户的数量,用于随机选取转入账户
  64. * @return 数组长度
  65. * */
  66. public int size(){
  67. return accounts.length;
  68. }
  69. }

 

运行结果就不贴了,你可以发现,总金额永远都是100000,每个账户的流入、流出也都是正确的,这就实现了并发安全。

而且,这种情况下建议使用try-catch来进行处理,在finally块中使用unlock()方法释放锁,否则如果一个线程出现异常,并且它持有锁,那么就会造成死锁。

在以上的例子中,我们使用了Lock进行加锁,这样一来,如果一个线程正在执行transfer()方法,即使在执行的时候被剥夺了运行权,此时又来了一个线程执行transfer()方法,由于线程1还没有释放锁,所以新来的线程调用lock()方法时将会被阻塞,直到占用锁的线程释放锁之后它才能开始运行。

今天就先记录到这里,挖坑填坑,其乐融融!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/574626
推荐阅读
相关标签
  

闽ICP备14008679号