乐观锁实现多线程抢红包算法-java

1,032 阅读2分钟

一.目标

多人抢红包时保证红包金额金和红包个数的准确,同时保证并发性。

二.方案

  1. 可以采用java中的锁机制来控制每个抢红包线程对红包的获取,保证同时只有一个线程在抢红包,同时利用volitate关键子保证红包金额和红包个数对所有线程可见性(java内存模型)。
  2. 通过乐观锁实现更高的并发性。抢红包过程中涉及到两个共享红包金额sum和红包金额sum和红包个数count两个变量,要同时控制两个共享变量的原子性。

三.实施方案

采用乐观锁实现多人抢红包算法,可以保证更高的并发性。由于要同时保证两个共享变量的原子性,有两种方案:

  • 可以将红包金额和红包个数包装在一个对象里,采用AtomicReference保证对象的原子性从而保证红包金额和红包个数的原子性。
  • 通过AtomicStampedReference来实现,版本号初始化为红包个数,在这个场景中,每次抢依次红包改变不是加1,而是减1。

第二中方案比较简单,因此我才用了第二种方案来实现。构造函数和红包中变量定义代码如下:

//每个线程可以抢当前红包余额的最大比例
private final double MAX_RATE=0.6;
private final double MIN_MONEY=0.1;
//控制红包金额和红包个数的原子性,红包个数是版本号 AtomicStampedReference<BigDecimal> pari;
 //红包个数
 private Integer count;
 //红包金额private BigDecimal sum;
 //测试用,红包总金额是否有错
 static BigDecimal  sumMoney=new BigDecimal(0.0);
 //红包的构造函数
 public RedPacket(int count,double sum)
 {    this.count=count;    
      this.sum=new BigDecimal(sum);    
      this.pari=new AtomicStampedReference<BigDecimal>(this.sum,count);
 }

一个抢红包过程的的实现代码:

 public BigDecimal getMoney()
    {   
        //当还有红包时线程不断去尝试抢红包,直到抢到红包或者被抢完退出
        while(true) {
            BigDecimal expectSum = this.pari.getReference();
            int expectCount = this.pari.getStamp();
            if (expectCount<1) {
                System.out.println("手速太慢,红包已经被抢完啦!");
                return new BigDecimal(0);
            } else if (expectCount== 1) {
                if (this.pari.compareAndSet(expectSum,new BigDecimal(0),expectCount,expectCount-1)) {
                    System.out.println("恭喜你抢到了最后一个红包" + expectSum+ "元");
                    return expectSum;
                }
            } else {
                BigDecimal rate = new BigDecimal(Math.random() * MAX_RATE).setScale(1,BigDecimal.ROUND_HALF_DOWN);
                BigDecimal result=expectSum.multiply(rate);
                if(result.doubleValue()<MIN_MONEY)
                {
                    result=new BigDecimal(0.1);
                }
                if (this.pari.compareAndSet(expectSum,expectSum.subtract(result),expectCount,expectCount-1)) {
                    System.out.println("恭喜你抢到了" + result+ "元");
                    return result;
                }
            }
        }
    }

测试:

public static void main(String[] args) throws InterruptedException {
        //20个人抢
        int people=20;
        //这个锁纯粹为了验证每轮红包结束后金额是否正确,不影响每一轮的抢红包过程
        Lock  lock=new ReentrantLock();
        //进行30轮测试
        for(int j=0;j<30;j++) {
            //红包个数5,红包金额100
            RedPacket redPacket=new RedPacket(5,100.0);
            //用于一轮抢红包结束前,不进行新一轮的抢红包。(测试才需要)
            CountDownLatch latch=new CountDownLatch(people);
            System.out.println("抢红包开始啦===================================");
            for (int i = 1; i <= people; i++) {
                new Thread(() -> {
                    BigDecimal num = redPacket.getMoney();
                    lock.lock();
                    sumMoney=sumMoney.add(num);
                    lock.unlock();
                    latch.countDown();
                }).start();
            }
            latch.wait(); 
            System.out.println("抢红包结束啦=========总金额 "+sumMoney.doubleValue()+"=============================");
            sumMoney=sumMoney.subtract(sumMoney);
        }
    }

结果:


完整代码见------github源码