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

2022-05-05 19:38:51 浏览数 (1)

1变量的自增与自减

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

代码语言:javascript复制
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);
}

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

代码语言:javascript复制
9589

并不是我们预算的10000。

2加锁的自增与自减

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

代码语言:javascript复制
public static synchronized void increaseWithSync() {
 num  ;
}

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

运行结果:

代码语言:javascript复制
10000

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

3原子自增与自减

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

代码语言:javascript复制
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);
}

代码运行结果:

代码语言:javascript复制
10000

符合预期。

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

高并发量的情况下,由于真正更新成功的线程占少数,容易导致循环次数过多,浪费时间,并且浪费线程资源。 由于需要保证变量真正的共享,**「缓存一致性」**开销变大。

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

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

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

代码语言:javascript复制
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);
}

运行结果同样符合预期

代码语言:javascript复制
10000

那么LongAdder性能为什么高呢?

代码语言:javascript复制
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实例:

代码语言:javascript复制
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;
}

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

代码语言:javascript复制
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 人点赞