Java高并发——使用Lock锁实现并发安全

前言

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

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

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

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

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

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

第二步,增加转入的钱

第三步,将新的余额存入

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

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

没有锁的并发实例

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

银行业务类——Bank.java

package com.shuixian.jianghao.utils;

import java.text.DecimalFormat;
import java.util.Arrays;


/**
 *  银行业务类
 * @author 秋枫艳梦
 * @date 2019-05-07
 * */
public class Bank {
    //账户数组
    private final double[] accounts;

    private DecimalFormat decimalFormat = new DecimalFormat("#.00");

    /**
     *  构造函数
     * @param n accounts数组的长度
     * @param initialBalance 每个账户的钱款数
     * */
    public Bank(int n,double initialBalance){
        accounts=new double[n];

        Arrays.fill(accounts,initialBalance);
    }

    /**
     *  从一个账户向另一个账户转账
     * @param from 转出账户,对应数组中的元素
     * @param to 转入账户
     * @param amount 转账金额
     * */
    public void transfer(int from,int to,double amount){

        try {
            //转出账户的钱不够,结束
            if (accounts[from]<amount){

                return;
            }

            //对转出账户进行扣钱
            accounts[from]-=amount;
            //对转入账户进行加钱
            accounts[to]+=amount;

            System.out.printf(Thread.currentThread()+"从用户 %d 转出 %10.2f 到用户 %d",from,amount,to);
            System.out.println("\t当前银行所有用户的总余额:"+getTotalBalance());

        }catch (Exception e){

        }finally {

        }
    }

    /**
     *  获取当前银行所有账户的余额之和
     * @return 银行的总余额
     * */
    public String getTotalBalance(){
        double sum=0;
        for (double a : accounts){
            sum+=a;
        }
        return decimalFormat.format(sum);
    }

    /**
     *  获取账户的数量,用于随机选取转入账户
     * @return 数组长度
     * */
    public int size(){
        return accounts.length;
    }
}

 测试类——BankTest.java

package com.shuixian.jianghao.utils;

/**
 *  没有锁的测试类,此时无法保证并发安全
 * @author 秋枫艳梦
 * @date 2019-05-07
 * */
public class BankTest {
    //模拟100个账户
    public static final int ACCOUNTS_SIZE=100;
    //假设每个账户1000元,那么并发安全的情况下,银行的总余额应该始终是100000元
    public static final double INIT_BALANCE=1000;
    //假设转账金额的上限是1000元
    public static final double MAX_AMOUNT=1000;
    //休眠时间
    public static final int DELAY=10;

    public static void main(String[] args) {
        //实例化一个有100个账户、每个账户初始余额为1000元的银行
        Bank bank=new Bank(ACCOUNTS_SIZE,INIT_BALANCE);

        //开启100个线程
        for (int i = 0; i < ACCOUNTS_SIZE; i++) {
            //转出账户
            int fromAccount=i;

            //构造线程
            Runnable runnable=() ->{
                try {
                    while (true){
                        //随机获取一个转入账户
                        int toAccount=(int)(bank.size()*Math.random());
                        //随机获取转账金额
                        double amount=MAX_AMOUNT*Math.random();
                        //执行转账
                        bank.transfer(fromAccount,toAccount,amount);
                        //模拟耗时
                        Thread.sleep((long)(DELAY*Math.random()));
                    }
                }catch (InterruptedException e){

                }
            };
            Thread thread=new Thread(runnable);
            thread.start();
        }
    }
}

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

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

Lock锁实现同步机制

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

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

改动一下我们的Bank类:

package com.shuixian.jianghao.utils;

import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 *  银行业务类
 * @author 秋枫艳梦
 * @date 2019-05-07
 * */
public class Bank {
    private final double[] accounts;
    private Lock myLock=new ReentrantLock();
    private DecimalFormat decimalFormat = new DecimalFormat("#.00");

    /**
     *  构造函数
     * @param n accounts数组的长度
     * @param initialBalance 每个账户的钱款数
     * */
    public Bank(int n,double initialBalance){
        accounts=new double[n];
        Arrays.fill(accounts,initialBalance);
    }

    /**
     *  从一个账户向另一个账户转账
     * @param from 转出账户,对应数组中的元素
     * @param to 转入账户
     * @param amount 转账金额
     * */
    public void transfer(int from,int to,double amount){
        //锁上
        myLock.lock();
        try {
            //转出账户的钱不够,结束
            if (accounts[from]<amount){
                
                return;
            }

            //对转出账户进行扣钱
            accounts[from]-=amount;
            //对转入账户进行加钱
            accounts[to]+=amount;

            System.out.printf(Thread.currentThread()+"从用户 %d 转出 %10.2f 到用户 %d",from,amount,to);
            System.out.println("\t当前银行所有用户的总余额:"+getTotalBalance());
        }catch (Exception e){

        }finally {
            //释放锁
            myLock.unlock();
        }
    }

    /**
     *  获取当前银行所有账户的余额之和
     * @return 银行的总余额
     * */
    public String getTotalBalance(){
        double sum=0;
        for (double a : accounts){
            sum+=a;
        }
        return decimalFormat.format(sum);
    }

    /**
     *  获取账户的数量,用于随机选取转入账户
     * @return 数组长度
     * */
    public int size(){
        return accounts.length;
    }
}

 

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

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

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

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

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的Lock是一种更高级别的线程同步机制,它比传统的synchronized关键字更加灵活,性能也更好。Java中的Lock要求显式地获取和释放,而synchronized则会自动获取和释放。下面介绍一下Lock使用及其常见的使用场景。 ### Lock使用 Java中的Lock接口定义了一组方法,用于获取、释放以及其他一些与相关的操作。Lock的常用实现类有ReentrantLock、ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock等。 下面是一个简单的使用ReentrantLock的示例: ```java import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockDemo { private Lock lock = new ReentrantLock(); public void method() { lock.lock(); // 获取 try { // 这里是需要同步的代码块 } finally { lock.unlock(); // 释放 } } } ``` 在上面的示例中,我们使用了ReentrantLock实现的功能。在需要同步的代码块前调用lock()方法获取,在同步代码块执行完后调用unlock()方法释放。 ### Lock使用场景 Lock使用场景与synchronized类似,都是在多线程环境下对共享资源进行同步。但是,由于Lock的灵活性更强,所以它的使用场景比synchronized更加广泛。 下面是一些常见的Lock使用场景: - 高并发情况下的线程同步:在高并发情况下,使用Lock可以提供更好的性能,因为它的实现比synchronized更加高效。 - 读写分离的情况下的线程同步:在读写分离的情况下,使用ReentrantReadWriteLock可以实现读写,使得读操作可以并发执行,而写操作需要独占,保证数据的一致性。 - 死避免:在使用synchronized时,如果由于某些原因没有及时释放,就可能导致死。而使用Lock时,可以在获取的时候设置超时时间,避免死的发生。 总之,LockJava中一种强大的线程同步机制,使用时需要注意的获取和释放,以及异常处理等问题,但它的灵活性和性能优势使得它成为Java并发编程中不可或缺的一部分。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值