高并下如何做变量的自增与自减

9 篇文章 1 订阅

变量的自增与自减

变量的自增自减相信大家都会,一般情况下直接++--就可以了。但是实际情况我们可能需要考虑并发问题,多线程情况下,如果我们直接计算。计算结果可能就会不准确。

public static int num = 0;

public static void increase() {
 num++;
}

public static void main(String[] args) throws InterruptedException {
    Thread[] threads = new Thread[10];
    for (int i = 0; i < threads.length; i++) {
        threads[i] = new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
                //increaseWithLock();
                increase();
            }
        });

        threads[i].start();
    }

    for (Thread thread : threads) {
     thread.join();
    }

    System.out.println(num);
}

我们直接这么操作,结果就会不准确。上述代码运算结果为:

9589

并不是我们预算的10000。

2加锁的自增与自减

这时我们就会给运算方法加锁,synchronized或者lock都行

public static synchronized void increaseWithSync() {
 num++;
}

//或者
public static void increaseWithLock() {
    try {
        lock.lock();
        num++;
    } finally {
        lock.unlock();
    }
} 

运行结果:

10000

但是用到了锁,这个东西可以说偏重量级的了,会引起线程上下文的切换和调用,线程之间的切换也会有性能成本的。这是我们就要使用JDK自带的原子类了。

3原子自增与自减

我们来看看java.util.concurrent.atomic包下面的原子类AtomicInteger。看下面的代码:

AtomicInteger atomicInteger = new AtomicInteger();

@Test
public void test() throws InterruptedException {
    Thread[] threads = new Thread[10];
    for (int i = 0; i < threads.length; i++) {
        threads[i] = new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
             num = atomicInteger.incrementAndGet();
            }
        });

        threads[i].start();
    }

    for (Thread thread : threads) {
     thread.join();
    }

    System.out.println(num);
}

代码运行结果:

10000

符合预期。

Java的原子类主要采用CAS + 自旋实现,但是在高并发情况下,还是存在一些性能问题的:

高并发量的情况下,由于真正更新成功的线程占少数,容易导致循环次数过多,浪费时间,并且浪费线程资源。

由于需要保证变量真正的共享,**「缓存一致性」**开销变大。

之前我写了一篇关于如何手写Atomic原子类的文章,有兴趣的同学可以看看:

没用过Java原子类?我来手写一个AtomicInteger

实际上Java还提供了性能更优越的LongAdder。我们来看看LongAdder怎么使用。

private static volatile LongAdder longAdder = new LongAdder();

@Test
public void test() throws InterruptedException {
    Thread[] threads = new Thread[10];
    for (int i = 0; i < threads.length; i++) {
        threads[i] = new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
                //num = atomicInteger.incrementAndGet();
                longAdder.increment();
            }
        });

        threads[i].start();
    }

    for (Thread thread : threads) {
        thread.join();
    }

    System.out.println(longAdder);
}

运行结果同样符合预期

10000

那么LongAdder性能为什么高呢?

Benchmark                  Mode  Cnt       Score      Error   Units
AtomicTest.atomicLongAdd  thrpt  200   52860.651 ± 1337.731  ops/ms
AtomicTest.longAdderAdd   thrpt  200  486609.475 ± 5204.630  ops/ms

采用JMH做Benchmark基准测试,分别使用10个线程测试两个方法的吞吐量。测试的性能结果如上。我们发现LongAdder吞吐量明显要高。

唯一会制约AtomicXXX高效的原因是高并发,高并发意味着CAS的失败几率更高, 重试次数更多,越多线程重试,CAS失败几率又越高,变成恶性循环,AtomicXXX效率降低。那怎么解决?

LongAdder的解决方案是:减少并发,将单一value的更新压力分担到多个value中去,降低单个value的 “热度”,分段更新。这样,线程数再多也会分担到多个value上去更新,只需要增加value就可以降低 value的 “热度” 。

简而言之,LongAdder采用空间换时间。

4分布式系统中的自增与自减

我们来看这样一个需求:

用户注册就会给用户分配一个编号,编号规则按用户先后注册顺序递增,比如第一位注册的用户编号为100,第二位就为101,依次类推。

这里我们就要考虑并发,不能创建重复的编号。你可能会说,这个简单,我就用上面的LongAdder,性能好,线程安全,不会出现重复编号的情况。

但是实际上我们的系统可能有多个实列,上面的LongAdder只是JVM级别的,在自己的实列中获取可以实现安全的自增。在有多个实例的系统中就不行了,为了实现上面的需求,我们可以使用数据库的特性来生成编号。

一般的数据库如MySQL可能会有性能问题。这里我推荐使用Redis来生成。由于Redis的主计算线程属于单线程,使用Redis安全又高效。

Java有个Redis的API RedissonClient可以用来实现原子自增与自减。

首先我们需要创建一个RedissonClient实例:

private RedissonClient getRedissonClient() {
    Config config = new Config();
    SingleServerConfig singleServerConfig = config.useSingleServer();
    singleServerConfig.setAddress("redis://127.0.0.1:6379");
    singleServerConfig.setPassword("lvshen");
    RedissonClient redissonClient = Redisson.create(config);
    return redissonClient;
}

然后我们就用这个实例做自增计算

public long getCode(String key) {
    RedissonClient redissonClient = getRedissonClient();

    RAtomicLong atomicVar = redissonClient.getAtomicLong(key);
    if (!atomicVar.isExists()) {
        atomicVar.set(100);
    }
    long value = atomicVar.incrementAndGet(); // 多线程调用该方法,不会造成数据丢失
    return value;
}

上面的代码就实现了在分布式系统中的原子自增。

以上就是今天的全部内容啦,如果对你有用,欢迎点赞+转发。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值