天天看点

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念

为什么需要进程、线程?

首先,我们来回忆一下冯诺依曼计算机体系。当前计算机主要是基于冯诺依曼体系结构设计的,下面就简单分析一下冯诺依曼体系结构的计算机是如何工作的,首先下面的图就是冯诺依曼体系结构图。

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念

冯诺依曼计算机体系结构,当前流程计算机组成如下:

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念

引发的问题:如果单纯依靠上述体系来做计算机处理,性能不高。主要瓶颈在IO上,比如磁盘IO、网络IO等的读取、写入;而CPU性能非常高,往往是大部分时间是闲置的,就是没有充分利用了CPU的性能。如下图所示:

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念

为了解决此问题,科学家、计算家们通过两种方式优化了计算性能不高问题:1、增加内存、缓存;2、优化软件结构

导入了内存、缓存(1级、2级、3级)、寄存器;需要数据各层进行复制

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念

引入了进程、线程,让线程抢占式的利用CPU资源;需要接管计算机硬件资源,任务调度、进程管理【线程】;在Java 层面即是synchronized、volatile、CAS、Lock、并发工具类、CountDownLatch、CyclicBarrier、Future、线程池等。

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念

程序运行的底层原理

  • 程序是什么?QQ.exe,PowerPoint.exe
  • 进程是什么?程序启动,进入内存,资源分配的基本单元
  • 线程是什么?程序执行的基本单位
  • 程序如何开始运行? CPU 读指令 - PC(存储指令地址),读数据到寄存器 Register,计算,回写到内存;然后指向下一条指令
  • 线程如何进行调度?Linux 线程调度器(OS)操作系统
  • 线程切换的概念是什么?Context Switch CPU 保存现场执行新线程,恢复现场,继续执行原线程这样的一个过程

缓存、寄存器

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念

从CPU的计算单元(ALU)到:

Registers  < 1 ns
L1 cache 约 1ns
L2 cache 约 3ns
L3 cache 约 15ns
main memory 约 80ns

根据空间局部性原理,按块读取,程序局部性原理,可以提高效率;充分发挥总线、CPU针脚等一次性读取更多数据的能力。

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念

MESI Cache 缓存一致性协议

缓存一致协议是Modified、Exclusive、Shared、Invalid 四个单词的缩写,详细知识参考:https://www.cnblogs.com/z00377750/p/9180644.html

Intel 的缓存一致性协议叫MESI,其他处理器的缓存一致性协议不叫MESI,叫:MSI、MOSI、Synapse Firefly Dragon

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念

缓存行Cache Line:

  • 缓存行越大,局部性空间效率越高,但读取时间慢
  • 缓存行越小,局部性空间效率越低,但读取时间快
  • 取一个折中值,目前多用:64字节【工业实践得出的结论】

根据缓存行一致性协议,如果x的值被修改,在多线程环境下需要需要被通知到另外的其他线程。这个通知过程是需要花费时间的。

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念

代码示例1:数据在同一个缓存行,运行32728

/*
    两个线程操作在同一个缓存行的数据进行叠加计算,查看时间耗时。缓存行大小64字节,一个long类型8字节
    根据缓存一致性协议,当同一缓存行数据更改了,必须通知其他线程,所以时间都花在了通知上面去了
 */
public class T01_CacheLinePadding_Deprecated {
    public static long COUNT = 1_0000_10000L;

    private static class T {
        public volatile long x = 0L;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                arr[0].x = i;
            }
            latch.countDown();
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                arr[1].x = i;
            }
            latch.countDown();
        }, "t1");

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        latch.await();
        System.out.println((System.nanoTime() - start)/100_1000);
    }
}
           

代码示例2:数据不在同一个缓存行,运行16402,较示例1性能提升近1倍

/*
    两个线程操作在不在同一个缓存行的数据进行叠加计算,查看时间耗时。缓存行大小64字节,一个long类型8字节
    整体耗时较demo01 短,即快很多。
    因为数据作了填充,保证两个数据不在同一个缓存行中,所以不会触发缓存一致性问题,所以也就没有需要相互通知的问题。
    x,y肯定不在同一缓存行
    JDK 1.7
 */
public class T02_CacheLinePadding_Deprecated {
    public static long COUNT = 1_0000_0000;

    // 定义p1~p7,确保x在同个缓存行
    private static class T {
        private long p1,p2,p3,p4,p5,p6,p7;
        public volatile long x = 0L;
        private long p9,p10,p11,p12,p13,p14,p15;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                arr[0].x = i;
            }
            latch.countDown();
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                arr[1].x = i;
            }
            latch.countDown();
        }, "t2");

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        latch.await();
        System.out.println((System.nanoTime() - start) / 100_000);
    }
}
           

在项目中的应用有:disruptor、jdk中的

线程安全问题

因为CPU与外围IO存在较大的性能差异,需要提高计算机的性能

  • 硬件
    • 加了内存、多级缓存,多核CPU缓存之间是不共享数据,引发了可见性问题
    • 因为CPU 对线程任务的切换,引发了原子性问题
    • CPU 要更加高效地去执行线程中的任务,引发了有序性的问题
  • 软件
    • 增加了进程管理
      • 增加了线程切换
线程安全问题 描述 解决
1.原子性 表示不可分割 ---> 是因为多任务线程的切换 ---> 提高CPU利用率 ---> 提高并发性能 synchronized,不仅仅能够解决原子性不可分割,还保证可见性
2.可见性 其实本质上是CPU缓存  --->  多核CPU缓存之间是不共享 ---> 可见性的问题 volatile 保证多线程可见性,禁止CPU去缓存
3.有序性 解释了CPU为何会把我们写的Java代码做一个重排序的操作 volatile 禁止指令重排序

超线程:一个ALU对应多个PC Registers 所谓的四核八线程。注意:线程不是越多越好,线程切换需要时间。

锁的概念

1、为什么需要锁?

CPU是乱序执行,CPU在进行读等待的同时执行指令,是CPU乱序的根源;不是乱,而是提高效率。所以多线程环境下,对共享数据操作会引发产生数据线程安全的问题。

2、什么是锁?锁解决什么问题?

锁是逻辑概念,保证数据一致性。在同一个时刻,保证一个线程操作共享数据。在Java 开发中使用Synchronized 进行上锁。

持有锁的线程进行工作,那不持有锁的线程咋办?

  • 忙等待;轻量级,自旋锁,需要消耗CPU资源;适合竞争不激烈
  • 进队列等待;重量级,需要操作系统进行调度;适合竞争激烈

在JDK 早期,Synchronized 是直接向操作系统申请锁,整个操作非常重;在JDK后期,对Synchronized 关键字锁进行了升级,就是锁有个升级的过程

Synchronized知识较多,特别花费了较多时间进行整理,下载链接:Synchronized_思维导图(全面).xmind.zip

案例一:多线程操作共享数据m进行++操作,最终导致与理想结果不一致问题

// 多线程,并发情况下对int m变量进行累加,出现的数据被写覆盖。赋值不是原子性的
public class T00_CasAndUnsafe {

    private static int m = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    m++;
                }
                latch.countDown();
            });
        }

        Arrays.stream(threads).forEach((t) -> t.start());

        latch.await();

        System.out.println(m);
    }
}
           

解决方式一:为了解决数据共享问题,可以使用锁对共享数据加锁,如下示例:

// 多线程,并发情况下对 int m变量进行累加,数据回写使用synchronized进行同步加锁; 实现了数据的一致性

public class T03_CasAndUnsafe3 {

    private static /*volatile*/ int m = 0;

    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();

        Thread[] threads = new Thread[10];
        CountDownLatch latch = new CountDownLatch(threads.length);

        Object o = new Object();
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                synchronized (o) {
                    for (int j = 0; j < 10000; j++) {
                        m++;
                    }
                    latch.countDown();
                }
            });
        }

        Arrays.stream(threads).forEach((t) -> t.start());

        latch.await();

        System.out.println(m);

        long end = System.currentTimeMillis();

        long spend = end - start;
        System.out.println("spend:" + spend + " ms");
    }
}
           

解决方式二:为了解决数据共享问题,可以使用自旋锁CAS对共享数据加锁,如下示例:

// 多线程,并发情况下对 AtomicInteger m变量进行累加, 实现了数据的一致性
public class T04_AtomicInteger {

    private static AtomicInteger m = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();

        Thread[] threads = new Thread[10];
        CountDownLatch latch = new CountDownLatch(threads.length);

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    m.getAndIncrement(); // m++;
                }
                latch.countDown();
            });
        }

        Arrays.stream(threads).forEach((t) -> t.start());

        latch.await();

        System.out.println(m);

        long end = System.currentTimeMillis();

        long spend = end - start;
        System.out.println("spend:" + spend + " ms");
    }
}
           

CAS比较并交换,CAS原理图如下:

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念

CAS 引发的问题

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念

文章最后,给大家推荐一些受欢迎的技术博客链接:

  1. JAVA相关的深度技术博客链接
  2. Flink 相关技术博客链接
  3. Spark 核心技术链接
  4. 设计模式 —— 深度技术博客链接
  5. 机器学习 —— 深度技术博客链接
  6. Hadoop相关技术博客链接
  7. 超全干货--Flink思维导图,花了3周左右编写、校对
  8. 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
  9. 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
  10. 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂
  11. 深入聊聊Java 垃圾回收机制【附原理图及调优方法】

欢迎扫描下方的二维码或 搜索 公众号“大数据高级架构师”,我们会有更多、且及时的资料推送给您,欢迎多多交流!

多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念
多线程与高并发深入底层横向对比为什么需要进程、线程?程序运行的底层原理缓存、寄存器MESI Cache 缓存一致性协议线程安全问题锁的概念