Java并发编程实战
王宝令
资深架构师
立即订阅
14231 人已学习
课程目录
已完结 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并发编程实战
登录|注册

07 | 安全性、活跃性以及性能问题

王宝令 2019-03-14

通过前面六篇文章,我们开启了一个简单的并发旅程,相信现在你对并发编程需要注意的问题已经有了更深入的理解,这是一个很大的进步,正所谓只有发现问题,才能解决问题。但是前面六篇文章的知识点可能还是有点分散,所以是时候将其总结一下了。

并发编程中我们需要注意的问题有很多,很庆幸前人已经帮我们总结过了,主要有三个方面,分别是:安全性问题、活跃性问题和性能问题。下面我就来一一介绍这些问题。

安全性问题

相信你一定听说过类似这样的描述:这个方法不是线程安全的,这个类不是线程安全的,等等。

那什么是线程安全呢?其实本质上就是正确性,而正确性的含义就是程序按照我们期望的执行,不要让我们感到意外。在第一篇《可见性、原子性和有序性问题:并发编程 Bug 的源头》中,我们已经见识过很多诡异的 Bug,都是出乎我们预料的,它们都没有按照我们期望的执行。

那如何才能写出线程安全的程序呢?第一篇文章中已经介绍了并发 Bug 的三个主要源头:原子性问题、可见性问题和有序性问题。也就是说,理论上线程安全的程序,就要避免出现原子性问题、可见性问题和有序性问题。

那是不是所有的代码都需要认真分析一遍是否存在这三个问题呢?当然不是,其实只有一种情况需要:存在共享数据并且该数据会发生变化,通俗地讲就是有多个线程会同时读写同一数据。那如果能够做到不共享数据或者数据状态不发生变化,不就能够保证线程的安全性了嘛。有不少技术方案都是基于这个理论的,例如线程本地存储(Thread Local Storage,TLS)、不变模式等等,后面我会详细介绍相关的技术方案是如何在 Java 语言中实现的。

© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java并发编程实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(106)

  • vector是线程安全,指的是它方法单独执行的时候没有并发正确性问题,并不代表把它的操作组合在一起问木有,而这个程序显然有老师讲的竞态条件问题。

    作者回复: 👍

    2019-03-14
    1
    57
  • 虎虎❤️
    老师讲的太好了。我没有并发的编程经验,但是可以看懂每一篇文章,也可以正确回答每节课后的习题。我觉得这次跟对了人,觉得很有希望跟着老师学好并发。

    但是,这样跟着学完课程就能学好并发编程吗?老师可以给些建议吗?除了跟着课程,我还需要做些什么来巩固战果?老师能不能给加餐一篇学习方法,谢谢!

    本节课总结:
    安全性:
    数据竞争: 多个线程同时访问一个数据,并且至少有一个线程会写这个数据。
    竞态条件: 程序的执行结果依赖程序执行的顺序。
    也可以按照以下的方式理解竞态条件: 程序的执行依赖于某个状态变量,在判断满足条件的时候执行,但是在执行时其他变量同时修改了状态变量。
    if (状态变量 满足 执行条件) {
      执行操作
    }
    问题: 数据竞争一定会导致程序存在竞态条件吗?有没有什么相关性?

    活跃性:
    死锁:破坏造成死锁的条件,1,使用等待-通知机制的Allocator; 2主动释放占有的资源;3,按顺序获取资源。
    活锁:虽然没有发生阻塞,但仍会存在执行不下去的情况。我感觉像进入了某种怪圈。解决办法,等待随机的时间,例如Raft算法中重新选举leader。
    饥饿:我想到了没有引入时间片概念时,cpu处理作业。如果遇到长作业,会导致短作业饥饿。如果优先处理短作业,则会饿死长作业。长作业就可以类比持有锁的时间过长,而时间片可以让cpu资源公平地分配给各个作业。当然,如果有无穷多的cpu,就可以让每个作业得以执行,就不存在饥饿了。

    性能:
    核心就是在保证安全性和活跃性的前提下,根据实际情况,尽量降低锁的粒度。即尽量减少持有锁的时间。JDK的并发包里,有很多特定场景针对并发性能的设计。还有很多无锁化的设计,例如MVCC,TLS,COW等,可以根据不同的场景选用不同的数据结构或设计。

    最后,在程序设计时,要从宏观出发,也就是关注安全性,活跃性和性能。遇到问题的时候,可以从微观去分析,让看似诡异的bug无所遁形。

    作者回复: 能看懂说明基本功很扎实啊。你的建议我会考虑的。

    2019-03-14
    23
  • kaixiao7
    老师,串行百分比一般怎么得出来呢(依据是什么)?

    作者回复: 你可以这么理解:临界区都是串行的,非临界区都是并行的,用单线程执行临界区的时间/用单线程执行(临界区+非临界区)的时间就是串行百分比

    2019-03-29
    20
  • 易水南风
    add10k的例子不明白,因为两个方法都已经加上锁了,同一个test对象应该不可能两个线程同时执行吧?

    作者回复: 同时执行,指的是同时被调用。被锁串行后,还是有问题

    2019-03-15
    3
    13
  • 飘呀飘的小叶子
    Vector实现线程安全是通过给主要的写方法加了synchronized,类似contains这样的读方法并没有synchronized,该题的问题就出在不是线程安全的contains方法,两个线程如果同时执行到if(!v.contains(o)) 是可以都通过的,这时就会执行两次add方法,重复添加。也就是老师说的竞态条件。

    作者回复: 👍

    2019-03-14
    1
    12
  • 亮亮
    void addIfNotExist(Vector v,
        Object o){
    synchronized(v) {
      if(!v.contains(o)) {
        v.add(o);
      }
    }
    }
    这样不知道对不对

    作者回复: 对的

    2019-03-14
    12
  • 代码搬运工
    contains和add之间不是原子操作,有可能重复添加。
    2019-03-14
    12
  • 寒铁
    add10K() 如果用synchronized修饰 应该就没有问题了吧? get和set是synchronized不能保证调用get和set之间的没有其他线程进入get和set,所以这是导致出错的根本原因。

    作者回复: 👍

    2019-04-03
    7
  • Demter
    老师说两个线程同时访问get(),所以可能返回1.但是两个线程不可能同时访问get(),get()上面有互斥锁啊,所以这个不是很懂啊

    作者回复: 同时访问,被串行化后,一先一后,结果两个线程都得到1

    2019-03-14
    1
    7
  • iron_man
    关于活锁,看了老师举的例子还是不太明白。
    死锁是多个线程互相持有彼此需要的资源,形成依赖循环。
    活锁是多个线程类似死锁的情况下,同时释放掉自己已经获取的资源,然后同时获取另外一种资源,又形成依赖循环,导致都不能执行下去?不知道总结的对不对,老师可否点评一下?

    作者回复: 总结的对。就是同时放弃,然后又重试竞争,最后死循环在里面了。

    2019-03-16
    6
  • hanmshashou
    ConcurrentHashMap 1.8后没有分段锁 syn + cas

    作者回复: 是这样,高手!

    2019-03-14
    6
  • ken
    实例不是线程安全的,Vector容器虽然是安全的单这个安全的原子性范围紧紧是每个成员方法。当需要调用多个方法来完成一个操作时Vector容器的原子性就适用了需要收到控制原子性,可以通过在方法上加synchronize保证安全性原子性。

    作者回复: 方法上加还不行

    2019-03-14
    5
  • 0928
    老师我在补充一下我之前的提问:
    流程是,服务器上存了2000万个电话号码相关的数据,要做的是把这批号码从服务器上请求下来写入到本地的文件中,为了将数据打散到多个文件中,这里通过 电话号码%1024 得到的余数来确定这个号码需要存入到哪个文件中取,比如13888888888 % 1024 =56,那么这个号码会被存入到 56.txt的文件中,写入时是一行一个号码。
    为了效率这里使用了多线程来请求数据并将请求下来的数据写入到文件,也就是每个线程包含向服务器请求数据,然后在将数据写入到电话号码对1024取余的那个文件中去,如果这么做目前会有一个隐患,多线程时如果 电话号码%1024 后定位的是同一个文件,那么就会出现多线程同时写这个文件的操作,一定程度上会造成最终结果错误。

    作者回复: 写一个文件只需要一个线程就够了。
    你可以用生产者-消费者模式试一下。
    可以创建64个线程,每个线程负责16个文件,
    同时创建64个阻塞队列,64个线程消费这76个阻塞队列,
     电话号码%1024 % 64 进入目标阻塞队列。

    其余的就是优化一下写文件的效率了

    2019-03-27
    4
  • 王玉坤
    老师,add10K()那块不是很懂,就算两个线程get()方法都读到0,他们在s调set()方法时因为是同步方法,总会一前一后的,根据hapens-before原则,前面修改的值应该对后面可见,为什么这个地方会出错呢?

    作者回复: 两个线程同时执行set(1){count=1},即便有同步,写到内存里的值也是1

    2019-03-16
    4
  • 探索无止境
    吞吐量和并发量从文中描述的概念上来看,总觉得很像,具体该怎么区分?期待指点!

    作者回复: 对于一台webserver,吞吐量一般指的是server每秒钟能处理多少请求;并发量指的是有多少个客户端同时访问。

    2019-03-14
    4
  • 师琳博
    add10k问题很多不明白,会问get有锁,怎么会同时执行。get虽然有锁,只能保证多个线程不能同一时刻执行。但是出现不安全的可能是线程a调用get后线程b调用get,这时两个get返回的值是一样的。然后都加一后再分别set.这样两个线程就出现并发问题了。问题在于同时执行get,而在于get和set是两个方法,这两个方法组合不是原子的,就可能两个方法中间的时间也有其它线程分别调用,出现并发问题。不知道这样解释对不对?

    作者回复: 对的

    2019-05-06
    3
  • Nevermore
    编写并发程序的初衷是为了提升性能,但在追求性能的同时由于多线程操作共享资源而出现了安全性问题,所以才用到了锁技术,一旦用到了锁技术就会出现了死锁,活锁等活跃性问题,而且不恰当的使用锁,导致了串行百分比的增加,由此又产生了性能问题,所以这就是并发程序与锁的因果关系。

    作者回复: 👍

    2019-03-14
    3
  • 拯救地球好累
    ---总结---
    1. 并发编程的三大问题:安全性问题;活跃性问题;性能问题
    2. 安全性问题的源头:原子性;可见性;有序性
    3. 安全性问题出现的根本原因:数据竞争(多个线程读写共享数据);竞态条件(程序的执行结果依赖线程执行的顺序)
    4. 活跃性问题的三种情况:死锁;活锁;饥饿
    5. 性能问题的衡量:阿姆达尔定律
    6. 性能问题的几种思路:无锁方案(TLS、COW、乐观锁);减少锁粒度
    2019-07-22
    2
  • duff
    「临界区串行,非临界区并行」 ,就很好理解,set(get()) 符合操作时在并发场景下的安全性问题了。
    2019-05-18
    2
  • 你只是看起来很努力
    void addIfNotExist(Vector v,
        Object o){
    synchronized(v) {
      if(!v.contains(o)) {
        v.add(o);
      }
    }
    }
    老师关于亮亮这个改动我有个问题:如果两个线程读到的是一个满的vector,那么线程1先加锁执行,这时候会进行扩容,vector的地址就改变了,线程2再来执行的时候,它之前读取到的vector地址是已经释放掉的,那么程序不会出问题吗?

    作者回复: vector的地址不会变,只是个指针而已

    2019-03-19
    2
收起评论
99+
返回
顶部