栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

并发编程基础知识

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

并发编程基础知识

1 什么是并发编程

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

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

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

2 线程安全问题

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

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

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

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

Java 内存模型(Java Memory Model) 规定, 将所有的变量保存在 主内存 中.
当线程使用变量时, 会把主内存中的变量 复制 到 自己的 工作内存 中, 线程读写变量时 操作的是 自己工作内存中的变量.

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 并发编程之美-翟陆续

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/287707.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号