并发编程基础知识

1 什么是并发编程

并行: 同一时刻, 多个任务同时执行.(一个 CPU 核 处理一个任务)
并发: 一个时间段内, 多个任务都在执行, 并且都没有执行结束. (一个 CPU 核处理多个任务)

在多线程编程中, 线程的个数往往多于 CPU 的个数, 所以一般称之为 并发编程.

并发编程可以提高系统性能, 提高吞吐量.

2 线程安全问题

共享资源: 多个线程都可以访问的资源. 或者说, 多个线程共享的资源.

线程安全问题是指:
当多个线程同时 读写 一个 共享资源 并且没有采取同步措施时, 导致出现 脏数据 的问题.

一个经典的线程安全问题的例子:
多线程 执行 count++ 操作, count++ 不是一个原子操作, 实际上 count++ 是 3 个原子操作(获取变量值-计算-写入变量中)组成的. 当两个线程同时执行 count++ 时, 本来期望是 count=count+2, 但可能实际结果是 count=count+1.

3 Java 共享变量的内存可见性问题

Java 内存模型(Java Memory Model) 规定, 将所有的变量保存在 主内存 中.
当线程使用变量时, 会把主内存中的变量 复制 到 自己的 工作内存 中, 线程读写变量时 操作的是 自己工作内存中的变量.
JMM_1
JMM 是一个抽象的概念, 实际上线程的 工作内存 可能是 CPU 的 一级缓存或二级缓存.

虽然 CPU 架构不同可能导致 线程工作内存 有所不同, 但是从 JMM 角度, 多线程操作 共享变量 的过程是一样的:
当一个线程操作共享变量时, 它首先从 主内存 复制 共享变量 到自己的 工作内存, 然后对 工作内存 中的变量进行处理, 处理后将变量更新到 主内存.

内存可见性问题示例:

  1. 线程A 获取 共享变量X 的值, 由于工作内存没有, 所以加载主内存中 X 的值. 假设主内存中 X=0, 线程A 将 X+1 后写回主内存, 此时 主内存 和 线程A 的工作内存里 X 的值都是 1;
  2. 线程B 获取 共享变量X 的值, 由于工作内存没有, 所以加载主内存中 X 的值. 此时 X=1, 线程B 将 X+1 后写回主内存, 此时 主内存 和 线程B 的工作内存里 X 的值都是 2;
  3. 线程A 又要修改 X 的值, 由于工作内存中已经有 X, 而且 X=1, 那么问题来了, 现在主内存的 X=2, 线程A 获取到的却是 X=1. 这就是共享变量的内存不可见问题, 也就是 线程B 写入的值 对 线程A 不可见.

可以通过 volatile 关键字 或者 加锁 解决共享变量 内存不可见 问题.

4 Java 中的原子性操作

所谓原子操作, 是指执行一些列操作, 要么全部执行, 要不全部不执行.

如下代码是线程不安全的:

public class Test {
    private long value;

    public long get() {
        return value;
    }

    public void increment() {
        value++;
    }
}

对于 increment 方法, value++ 不是原子操作, 我们可以通过 synchronized 关键字将其变成原子操作:

public class Test {
    private long value;

    public synchronized long get() {
        return value;
    }

    public synchronized void increment() {
        value++;
    }
}

注意:
synchronized 可以保证 内存可见性 和 原子性, 所以可以实现线程安全.

但是 synchronized 是独占锁(exclusive clock), 没有获取到监视器锁的线程会被阻塞.
对于像 get 这种读操作, 多线程读并不会存在线程安全问题, 所有直接加 synchronized 会降低并发性能. 如果直接去掉的话, 内存可见性无法保证. 也可以用 volatile 来保证内存可见性, 这样性能应该会有所提升.

其实, Java 内部已经 通过 CAS算法 实现了一些原子操作类, 比如 AtomicLong, 这些类可以保证线程安全, 而且没有加锁, 可以保证并发性能.

5 Java 中的 CAS 操作

使用 有一个不好的地方, 就是 当一个线程没有获取到 锁 时会被 阻塞挂起, 这会导致线程 上下文切换 和 重新调度 开销.

Java 提供了非阻塞的 volatile 关键字来解决 共享变量的 内存可见性问题, 这在一定的程度上弥补了锁带来的开销问题, 但是 volatile 只能保证内存可见性, 不能保证原子性.

CAS 即 Compare and Swap, 是 JDK 提供的 非阻塞原子性操作. 它通过 硬件保证了 比较-更新 操作的原子性.
CAS 算法通过 Unsafe 类来实现.

CAS 可能会存在 ABA 问题, JDK 中的 AtomicStampedReference 类给每个变量加了一个时间戳, 从而避免了 ABA 问题的产生.

备注:
所谓 ABA 问题, 就是 线程T1 对变量X 修改时, 先比较 X 的值是不是为 A, 如果为 A 就修改.
但 X=A 并不能保证 X的值没有改变过, 可能 X 开始等于A, 但后来被其他线程改成了 B, 最后又被改成了 A.
虽然最终 X=A, 但这个 A 已经不是最初那个 A 了.

Reference

[1]. Java 并发编程之美-翟陆续

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值