铁匠 铁匠
首页
收藏
java
架构之路
常用算法
  • Java
  • nginx
  • 系统运维
  • 系统安全
  • mysql
  • redis
参考文档
关于
链接
  • 分类
  • 标签
  • 归档

专注、不予评判地关注当下
首页
收藏
java
架构之路
常用算法
  • Java
  • nginx
  • 系统运维
  • 系统安全
  • mysql
  • redis
参考文档
关于
链接
  • 分类
  • 标签
  • 归档
  • java api 文档
  • 集合

  • 版本特性

  • jvm

  • 网络编程

  • 并发编程

    • 并发编程三大特性
      • 可见性
      • 顺序性
      • 原子性
      • 参考
    • 线程的基本概念
    • 如何正确停止一个线程
    • java线程池
    • 线程池大小设置多少合适
    • java 常用队列
    • 使用 Semaphore 实现一个简单限流器
  • java
  • 并发编程
FengJianxin
2019-03-10
目录

并发编程三大特性

# 可见性

我们编写的任何程序,最终都会转成机器码,交给 cpu 执行,由于 cpu 和内存的速度差异巨大,如果 cpu 每执行完一个指令都将数据写回内存,执行下一个指令又从内存读取数据,会严重降低 cpu 的执行效率,所以 cpu 每次都是读取一个缓存行(cache line, 64 字节)数据将数据放在 cpu 缓存中。

如下是 cpu 三级缓存架构示意图

(图片来自网络)

从 cpu 到各计算单元速度参考

计算单元 时钟周期 时间
寄存器 1 cycle < 1ns
L1 Cache 3 - 4 cycle 1 ns
L2 Cache 10 - 20 cycle 3 ns
L3 Cache 40 - 45 cycle 15 ns
内存 120 - 240 cycle 80 ns

扩展

因为有缓存行的存在,有时候可以使用缓存行对齐的写法提高程序性能,关于这方便内容可以自行 google

# 顺序性

为了提高 cpu 执行效率并发执行指令,当两个指令没有先后依赖关系时,会并发执行指令,就可能造成指令执行循序和代码不一致的情况,这种情况只能保证在单线程情况下的执行结果是一致的,而在多线程可能会得到错误的结果。

以下是复现指令重拍情况存在的代码

public class ReorderingDemo {

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {

        int i = 0;
        for (; ; ) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread one = new Thread(() -> {
                shortWait(100000);
                a = 1;
                x = b;
            });

            Thread other = new Thread(() -> {
                b = 1;
                y = a;
            });
            one.start();
            other.start();
            one.join();
            other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            // 只有线程中的代码出现指令重排,才可能出现这个结果
            if (x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }

    public static void shortWait(long interval) {
        long start = System.nanoTime();
        long end;
        do {
            end = System.nanoTime();
        } while (start + interval >= end);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

可见性和指令重拍问题的本质是为了提高 cpu 性能,但是引入一项技术的同时又带来了其他问题。java 内存模型(Java Memory Model,JMM)提供了解决可见性问题的规范:Happens-Before 规则。

具体规则包括:

  • 线程内执行的每个操作,都保证 happen-before 后面的操作,这就保证了基本的程序顺序规则,这是开发者在书写程序时的基本约定。
  • 对于 volatile 变量,对它的写操作,保证 happen-before 在随后对该变量的读取操作。
  • 对于一个锁的解锁操作,保证 happen-before 加锁操作。
  • 对象构建完成,保证 happen-before 于 finalizer 的开始动作。
  • 甚至是类似线程内部操作的完成,保证 happen-before 其他 Thread.join() 的线程等。

# 原子性

由于操作系统多线程的存在,导致了 cpu 切换带来的数据一致性问题。经典的count += 1举例,一行代码是拆分了多个 cpu 指令:

  1. 需要把变量 count 从内存加载到 CPU 的寄存器;
  2. 在寄存器中执行 +1 操作;
  3. 将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

cpu 在执行完任意一个指令时都可能切换到其他线程,假如以上count += 1执行完第二条指令后,切换大其他线程执行,此时的线程读取 count 的值就是一个没有更新完成的数据,用此时的数据进行计算,就会得到错误的记过。过程如下图:

# 参考

CPU多级缓存 (opens new window) Java内存访问重排序的研究 (opens new window)

#并发编程
netty介绍
线程的基本概念

← netty介绍 线程的基本概念→

最近更新
01
策略模式
01-09
02
模板方法
01-06
03
观察者模式
01-06
更多文章>
Theme by Vdoing | Copyright © 2016-2023 铁匠 | 粤ICP备15021633号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式