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


    变量的自增与自减

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

    1. public static int num = 0;
    2. public static void increase() {
    3.  num++;
    4. }
    5. public static void main(String[] args) throws InterruptedException {
    6.     Thread[] threads = new Thread[10];
    7.     for (int i = 0; i < threads.length; i++) {
    8.         threads[i] = new Thread(() -> {
    9.             for (int j = 0; j < 1000; j++) {
    10.                 //increaseWithLock();
    11.                 increase();
    12.             }
    13.         });
    14.         threads[i].start();
    15.     }
    16.     for (Thread thread : threads) {
    17.      thread.join();
    18.     }
    19.     System.out.println(num);
    20. }

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

    9589
    

    并不是我们预算的10000。

    2加锁的自增与自减

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

    1. public static synchronized void increaseWithSync() {
    2.  num++;
    3. }
    4. //或者
    5. public static void increaseWithLock() {
    6.     try {
    7.         lock.lock();
    8.         num++;
    9.     } finally {
    10.         lock.unlock();
    11.     }

    运行结果:

    10000
    

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

    3原子自增与自减

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

    1. AtomicInteger atomicInteger = new AtomicInteger();
    2. @Test
    3. public void test() throws InterruptedException {
    4.     Thread[] threads = new Thread[10];
    5.     for (int i = 0; i < threads.length; i++) {
    6.         threads[i] = new Thread(() -> {
    7.             for (int j = 0; j < 1000; j++) {
    8.              num = atomicInteger.incrementAndGet();
    9.             }
    10.         });
    11.         threads[i].start();
    12.     }
    13.     for (Thread thread : threads) {
    14.      thread.join();
    15.     }
    16.     System.out.println(num);
    17. }

    代码运行结果:

    10000
    

    符合预期。

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

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

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

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

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

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

    1. private static volatile LongAdder longAdder = new LongAdder();
    2. @Test
    3. public void test() throws InterruptedException {
    4.     Thread[] threads = new Thread[10];
    5.     for (int i = 0; i < threads.length; i++) {
    6.         threads[i] = new Thread(() -> {
    7.             for (int j = 0; j < 1000; j++) {
    8.                 //num = atomicInteger.incrementAndGet();
    9.                 longAdder.increment();
    10.             }
    11.         });
    12.         threads[i].start();
    13.     }
    14.     for (Thread thread : threads) {
    15.         thread.join();
    16.     }
    17.     System.out.println(longAdder);
    18. }

    运行结果同样符合预期

    10000
    

    那么LongAdder性能为什么高呢?

    1. Benchmark                  Mode  Cnt       Score      Error   Units
    2. AtomicTest.atomicLongAdd  thrpt  200   52860.651 ± 1337.731  ops/ms
    3. 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实例:

    1. private RedissonClient getRedissonClient() {
    2.     Config config = new Config();
    3.     SingleServerConfig singleServerConfig = config.useSingleServer();
    4.     singleServerConfig.setAddress("redis://127.0.0.1:6379");
    5.     singleServerConfig.setPassword("lvshen");
    6.     RedissonClient redissonClient = Redisson.create(config);
    7.     return redissonClient;
    8. }

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

    1. public long getCode(String key) {
    2.     RedissonClient redissonClient = getRedissonClient();
    3.     RAtomicLong atomicVar = redissonClient.getAtomicLong(key);
    4.     if (!atomicVar.isExists()) {
    5.         atomicVar.set(100);
    6.     }
    7.     long value = atomicVar.incrementAndGet(); // 多线程调用该方法,不会造成数据丢失
    8.     return value;
    9. }

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

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

  • 相关阅读:
    第K位数字
    Centos7使用nginx搭建rtmp流媒体服务器
    【LeetCode】10、正则表达式匹配
    print输出
    Unity学习——平台发布过程(windows)
    JavaScript Web APIs第三天笔记
    pytest(13)-多线程、多进程执行用例
    Vulnhub:Os-Bytesec靶机渗透
    LeetCode //C - 98. Validate Binary Search Tree
    【开源WebGIS】06-Openlayers+Vue 画点、线、面
  • 原文地址:https://blog.csdn.net/wujialv/article/details/126437228