Java并发编程实战第一部分学习记录

在这里插入图片描述

01 | 可见性、原子性和有序性问题:并发编程Bug的源头

并发程序幕后的故事

在这里插入图片描述

源头之一:缓存导致的可见性问题

源头之二:线程切换带来的原子性问题

在这里插入图片描述

源头之三:编译优化带来的有序性问题

在这里插入图片描述

总结

在这里插入图片描述

02 | Java内存模型:看Java如何解决可见性和有序性问题

什么是Java内存模型?

在这里插入图片描述

使用volatile的困惑

Happens-Before 规则

前面一个操作的结果对后续操作是可见的

  1. 程序的顺序性规则
    这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。

  2. volatile变量规则
    这条规则是指对一个volatile变量的写操作, Happens-Before 于后续对这个volatile变量的读操作。

  3. 传递性
    这条规则是指如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。

  4. 管程中锁的规则
    这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。

  5. 线程 start() 规则
    这条是关于线程启动的。它是指主线程A启动子线程B后,子线程B能够看到主线程在启动子线程B前的操作。

  6. 线程 join() 规则
    这条是关于线程等待的。它是指主线程A等待子线程B完成(主线程A通过调用子线程B的join()方法实现),当子线程B完成后(主线程A中join()方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。

总结

在Java语言里面,Happens-Before的语义本质上是一种可见性,A Happens-Before B 意味着A事件对B事件来说是可见的,无论A事件和B事件是否发生在同一个线程里。例如A事件发生在线程1上,B事件发生在线程2上,Happens-Before规则保证线程2上也能看到A事件的发生。

03 | 互斥锁(上):解决原子性问题

那原子性问题到底该如何解决呢?

你已经知道,原子性问题的源头是线程切换
“同一时刻只有一个线程执行”这个条件非常重要,我们称之为互斥。

简易锁模型

改进后的锁模型

Java语言提供的锁技术:synchronized

04 | 互斥锁(下):如何用一把锁保护多个资源?

保护没有关联关系的多个资源

保护有关联关系的多个资源

总结

“原子性”的本质是什么?其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。

05 | 一不小心就死锁了,怎么办?

如何预防死锁

在这里插入图片描述

  1. 破坏占用且等待条件
  2. 破坏不可抢占条件
  3. 破坏循环等待条件

06 | 用“等待-通知”机制优化循环等待

用synchronized实现等待-通知机制

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

那是不是所有的代码都需要认真分析一遍是否存在这三个问题呢?当然不是,其实只有一种情况需要:存在共享数据并且该数据会发生变化,通俗地讲就是有多个线程会同时读写同一数据。

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

安全性问题

当多个线程同时访问同一数据,并且至少有一个线程会写这个数据的时候,如果我们不采取防护措施,那么就会导致并发Bug,对此还有一个专业的术语,叫做数据竞争(Data Race)

竞态条件,指的是程序的执行结果依赖线程执行的顺序。

面对数据竞争竞态条件问题,又该如何保证线程的安全性呢?其实这两类问题,都可以用互斥这个技术方案,而实现互斥的方案有很多,CPU提供了相关的互斥指令,操作系统、编程语言也会提供相关的API。从逻辑上来看,我们可以统一归为:

活跃性问题

所谓活跃性问题,指的是某个操作无法执行下去。我们常见的“死锁”就是一种典型的活跃性问题,当然除了死锁外,还有两种情况,分别是“活锁”和“饥饿”

有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况,这就是所谓的“活锁”。
所谓“饥饿”指的是线程因无法访问所需资源而无法执行下去的情况。

性能问题

Java SDK并发包里之所以有那么多东西,有很大一部分原因就是要提升在某个特定领域的性能。
在这里插入图片描述

总结

总结

08 | 管程:并发编程的万能钥匙

什么是管程

在这里插入图片描述

MESA模型

在这里插入图片描述
在这里插入图片描述

wait()的正确姿势

notify()何时可以使用

总结

在这里插入图片描述

09 | Java线程(上):Java线程的生命周期

通用的线程生命周期

在这里插入图片描述

Java中线程的生命周期

在这里插入图片描述

  1. RUNNABLE与BLOCKED的状态转换
    只有一种场景会触发这种转换,就是线程等待synchronized的隐式锁

  2. RUNNABLE与WAITING的状态转换
    第一种场景,获得synchronized隐式锁的线程,调用无参数的Object.wait()方法
    第二种场景,调用无参数的Thread.join()方法。
    第三种场景,调用LockSupport.park()方法。调用LockSupport.park()方法,当前线程会阻塞,线程的状态会从RUNNABLE转换到WAITING。调用LockSupport.unpark(Thread thread)可唤醒目标线程,目标线程的状态又会从WAITING状态转换到RUNNABLE。

  3. RUNNABLE与TIMED_WAITING的状态转换
    在这里插入图片描述

  4. 从NEW到RUNNABLE状态,调用线程对象的start()方法

  5. 从RUNNABLE到TERMINATED状态

那stop()和interrupt()方法的主要区别是什么呢?

10 | Java线程(中):创建多少线程才是合适的?

为什么要使用多线程?

度量性能的指标有很多,但是有两个指标是最核心的,它们就是延迟和吞吐量

多线程的应用场景

创建多少线程合适?

CPU密集型的计算场景,理论上“线程的数量=CPU核数”就是最合适的。不过在工程上,线程的数量一般会设置为“CPU核数+1”

对于I/O密集型计算场景,最佳线程数=CPU核数 * [ 1 +(I/O耗时 / CPU耗时)]

其实只要把握住一条原则就可以了,这条原则就是将硬件的性能发挥到极致。

11 | Java线程(下):为什么局部变量是线程安全的?

方法是如何被执行的

局部变量存哪里?

调用栈与线程

线程封闭

12 | 如何用面向对象思想写好并发程序?

一、封装共享变量

对于这些不会发生变化的共享变量,建议你用final关键字来修饰

二、识别共享变量间的约束条件

三、制定并发访问策略

在这里插入图片描述

13 | 理论基础模块热点问题答疑

那这个“串行的故事”是怎样的呢?

在这里插入图片描述
在这里插入图片描述

1. 用锁的最佳实践

2. 锁的性能要看场景

3. 竞态条件需要格外关注

4. 方法调用是先计算参数

5. InterruptedException异常处理需小心

6. 理论值 or 经验值

发布了138 篇原创文章 · 获赞 3 · 访问量 7212

猜你喜欢

转载自blog.csdn.net/weixin_43719015/article/details/105665924
今日推荐