Java并发编程实战
王宝令
资深架构师
立即订阅
15060 人已学习
课程目录
已完结 50 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 你为什么需要学习并发编程?
免费
学习攻略 (1讲)
学习攻略 | 如何才能学好并发编程?
第一部分:并发理论基础 (13讲)
01 | 可见性、原子性和有序性问题:并发编程Bug的源头
02 | Java内存模型:看Java如何解决可见性和有序性问题
03 | 互斥锁(上):解决原子性问题
04 | 互斥锁(下):如何用一把锁保护多个资源?
05 | 一不小心就死锁了,怎么办?
06 | 用“等待-通知”机制优化循环等待
07 | 安全性、活跃性以及性能问题
08 | 管程:并发编程的万能钥匙
09 | Java线程(上):Java线程的生命周期
10 | Java线程(中):创建多少线程才是合适的?
11 | Java线程(下):为什么局部变量是线程安全的?
12 | 如何用面向对象思想写好并发程序?
13 | 理论基础模块热点问题答疑
第二部分:并发工具类 (14讲)
14 | Lock和Condition(上):隐藏在并发包中的管程
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
16 | Semaphore:如何快速实现一个限流器?
17 | ReadWriteLock:如何快速实现一个完备的缓存?
18 | StampedLock:有没有比读写锁更快的锁?
19 | CountDownLatch和CyclicBarrier:如何让多线程步调一致?
20 | 并发容器:都有哪些“坑”需要我们填?
21 | 原子类:无锁工具类的典范
22 | Executor与线程池:如何创建正确的线程池?
23 | Future:如何用多线程实现最优的“烧水泡茶”程序?
24 | CompletableFuture:异步编程没那么难
25 | CompletionService:如何批量执行异步任务?
26 | Fork/Join:单机版的MapReduce
27 | 并发工具类模块热点问题答疑
第三部分:并发设计模式 (10讲)
28 | Immutability模式:如何利用不变性解决并发问题?
29 | Copy-on-Write模式:不是延时策略的COW
30 | 线程本地存储模式:没有共享,就没有伤害
31 | Guarded Suspension模式:等待唤醒机制的规范实现
32 | Balking模式:再谈线程安全的单例模式
33 | Thread-Per-Message模式:最简单实用的分工方法
34 | Worker Thread模式:如何避免重复创建线程?
35 | 两阶段终止模式:如何优雅地终止线程?
36 | 生产者-消费者模式:用流水线思想提高效率
37 | 设计模式模块热点问题答疑
第四部分:案例分析 (4讲)
38 | 案例分析(一):高性能限流器Guava RateLimiter
39 | 案例分析(二):高性能网络应用框架Netty
40 | 案例分析(三):高性能队列Disruptor
41 | 案例分析(四):高性能数据库连接池HiKariCP
第五部分:其他并发模型 (4讲)
42 | Actor模型:面向对象原生的并发模型
43 | 软件事务内存:借鉴数据库的并发经验
44 | 协程:更轻量级的线程
45 | CSP模型:Golang的主力队员
结束语 (1讲)
结束语 | 十年之后,初心依旧
用户故事 (2讲)
用户来信 | 真好,面试考到这些并发编程,我都答对了!
3 个用户来信 | 打开一个新的并发世界
Java并发编程实战
登录|注册

21 | 原子类:无锁工具类的典范

王宝令 2019-04-16
前面我们多次提到一个累加器的例子,示例代码如下。在这个例子中,add10K() 这个方法不是线程安全的,问题就出在变量 count 的可见性和 count+=1 的原子性上。可见性问题可以用 volatile 来解决,而原子性问题我们前面一直都是采用的互斥锁方案。
public class Test {
long count = 0;
void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
}
其实对于简单的原子性问题,还有一种无锁方案。Java SDK 并发包将这种无锁方案封装提炼之后,实现了一系列的原子类。不过,在深入介绍原子类的实现之前,我们先看看如何利用原子类解决累加器问题,这样你会对原子类有个初步的认识。
在下面的代码中,我们将原来的 long 型变量 count 替换为了原子类 AtomicLong,原来的 count +=1 替换成了 count.getAndIncrement(),仅需要这两处简单的改动就能使 add10K() 方法变成线程安全的,原子类的使用还是挺简单的。
public class Test {
AtomicLong count =
new AtomicLong(0);
void add10K() {
int idx = 0;
while(idx++ < 10000) {
count.getAndIncrement();
}
}
}
无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题,可谓绝佳方案。那它是如何做到的呢?
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java并发编程实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(49)

  • 天涯煮酒
    or = rf.get(); 应该放到do{}内
    2019-04-16
    28
  • Geek_ebda96
    老师你举的这个例子,自己实现CAS是不是有点不对
    class SimulatedCAS{
      volatile int count;
      // 实现 count+=1
      addOne(){
        do {
          newValue = count+1; //①
        }while(count !=
          cas(count,newValue) //②
      }
      // 模拟实现 CAS,仅用来帮助理解
      synchronized int cas(
        int expect, int newValue){
        // 读目前 count 的值
        int curValue = count;
        // 比较目前 count 值是否 == 期望值
        if(curValue == expect){
          // 如果是,则更新 count 的值
          count= newValue;
        }
        // 返回写入前的值
        return curValue;
      }
    }

    2 这里是不是应该用oldValue来比较,在do里面的时候先把count的值用oldValue保存下来,传入的参数expected为oldValue,newValue为oldValue+1

    do{
        oldValue = count;
        newValue = oldValue + 1;
    }while(oldValue != cas(oldValue, newValue))

    望指正

    作者回复: 你的这个写法是对的👍

    2019-04-18
    5
    14
  • 张天屹
    如果线程1 运行到WMRange or = rf.get();停止,切换到线程2 更新了值,切换回到线程1,进入循环将永远比较失败死循环,解决方案是将读取的那一句放入循环里,CAS每次自旋必须要重新检查新的值才有意义

    作者回复: 👍

    2019-04-16
    12
  • 郑晨Cc
    or是原始的 nr是new出来的 指向不同的内存地址 compareandset的结果永远返回false 结果是死循环?是不是应该用atomicfieldreference?

    作者回复: 👍,不过我觉得没必要用atomicfieldreference

    2019-04-16
    10
  • andy
    public class SafeWM {
      class WMRange{
        final int upper;
        final int lower;
        WMRange(int upper,int lower){
        // 省略构造函数实现
        }
      }
      final AtomicReference<WMRange>
        rf = new AtomicReference<>(
          new WMRange(0,0)
        );
      // 设置库存上限
      void setUpper(int v){
        WMRange nr;
        WMRange or;
        do{
    or = rf.get();
          // 检查参数合法性
          if(v < or.lower){
            throw new IllegalArgumentException();
          }
          nr = new
            WMRange(v, or.lower);
        }while(!rf.compareAndSet(or, nr));
      }
    }

     这样子对吗?

    作者回复: 对👍

    2019-04-16
    2
    6
  • 榣山樵客™
    首先,or=rf.get()需要放到do{},每次需要重新获取,以防其他线程更新过导致死循环;

    然后,nr是new的,我觉得应该不会发生ABA的问题(reference的compareAndSet比较的是内存地址)。另外ABA问题应该容易发生在值类型上吧,引用类型的应该几乎不会发生?对于引用类型,几乎不会发生经过至少两次new对象,最后对象放在了同一块or之前使用的内存区块上吧?

    作者回复: 我也觉得没有ABA问题

    2019-04-16
    5
  • 密码123456
    我觉得可能会出现死循环。WMRange or = rf.get(); 应该放在do里面。每次比较交换失败后,重新获取一次。

    作者回复: 👍

    2019-04-16
    4
  • xuery
    例子中的模拟CAS,cas函数是加了锁的,保证整个操作的原子性;我的理解是这个只是一个模拟,实际中肯定不会加上锁的

    作者回复: 👍

    2019-05-02
    3
  • 刘志兵
    老师,compareAndSwapLong方法是一个native方法,比较共享变量和expect值是否相等,相等才设置新的值x, 不明白这里的对比是怎么保证原子性的,对比也是要再读一次共享变量,然后对比吧,如果先读出来之后对比的时候被其他线程修改了,那还是会有问题

    作者回复: 最终依赖的是cpu提供的原子指令,不用我们操心。

    2019-04-17
    3
  • Vincent
    第一个例子也不是线程安全的吧?i++这个操作不是线程安全的,会导致判断错误吧?

    作者回复: 局部变量不存在线程安全问题

    2019-06-25
    2
  • Sean
    设置上限为什么是WMRange(v, or.lower);? 是笔误还是我理解错了?

    作者回复: v是上限,下限不变,没问题

    2019-05-24
    2
  • 刘育飞
    不明白 synchronized int cas() 这不是已经用了 同步synchronized 关键字 吗怎么会 无锁 无堵塞呢

    作者回复: cas大部分都是CPU提供的指令,这里只是模拟它的原理

    2019-10-17
    1
  • linqw
    课后习题:如果在do{}while()第一次没设置成功,即对象已经被其他线程修改,or已经是过期的对象,导致死循环,可以写成如:
    public class SafeWM {
      class WMRange{
        final int upper;
        final int lower;
        WMRange(int upper,int lower){
    if(upper < lower){
             throw new IllegalArgumentException();
           }
        // 省略构造函数实现
        }
      }
      final AtomicReference<WMRange>
        rf = new AtomicReference<>(
          new WMRange(0,0)
        );
      // 设置库存上限
      void setUpper(int v){
        WMRange nr;
        WMRange or;
        do{
          or = = rf.get();
          nr = new
            WMRange(v, or.lower);
        }while(!rf.compareAndSet(or, nr));
      }
    }

    作者回复: 👍

    2019-04-21
    1
  • Mark
    请教一个问题
    compareAndSwapLong 更新完后返回的是true false,再return v。这是两步操作,return v之前,内存值有可能已经被该了,不是v了。
    有这种可能吗?

    作者回复: 有这种可能

    2019-04-18
    1
  • 小萝卜
    第16行应该放在循环体内
    2019-04-16
    1
  • 朕爱吾妃
    WMRange or = rf.get();这句话不放入do-while里这个or值一直不会发生变化,也就是如果在获取or这个之后,修改这个值之前,有其他线程进行值的修改,这样比较的时候就会出现与期望值不一样的情况,然后循环,这时候or还是没有修改,还是与期望值不一样,然后再次循环... ...,这样这个线程就会进行死循环,如果是实际项目中的话,应该会触发超时
    2019-11-21
  • kaiux
    // 比较目前count值是否==期望值
    if(curValue == expect){
    // 如果是,则更新count的值
    count= newValue;
    }
    ==========
    这段代码老师说是仅用来帮助理解,实际上这个是存在竞态条件的,如果在if执行完之后,count的值被别的变量修改,那么结果就不正确了。想找老师确认一下我理解的对不对?

    作者回复: 这段代码所在的方法是synchronized😂

    2019-11-18
  • 赤城
    老师,为什么我用synchronized实现的add10k方法执行要比使用AtomicInteger的执行更快呢,不应该采用AtomicInteger更快才对吗?

    synchronized:
    public synchronized void add10k() {
                int idx = 0;
                while (idx < 100000000) {
                    count++;
                    idx++;
          }
    }

    AtomicInteger:

    AtomicInteger count = new AtomicInteger(0);
        public void add10k() {
            int idx = 0;
            while (idx < 100000000) {
                count.getAndIncrement();
                idx++;
            }
        }

    作者回复: 你比较的是getandincrement和++的性能😂

    2019-11-06
    1
  • DFighting
    两个问题:
    1、compareAndSet每次和new 出来的object比较,永远不会相等,会出现死循环,这里比较的应该是oldValue和newValue,并使用cas方法对符合条件的值做更新。
    2、cas的完整过程应该是取出oldValue->比较->符合条件则用cas操作更新,也就是代码中的rf.get应该放在循环里。也就是说不能割裂对共享资源的CAS操作,不然并发时会有ABA问题
    2019-09-26
  • .
    此处原意应该是想考察or变量再CAS后如果不匹配,需要重新再次获取新的值,即WMRange or = rf.get()这条语句应该放到do{}while();的第一句。但问题的关键其实这里由于后面的nr是new出来的,所以rf.compareAndSet(or, nr)按地址进行比较无论如何都是不等的,死循环。
    2019-09-10
收起评论
49
返回
顶部