为什么需要进程、线程?
首先,我们来回忆一下冯诺依曼计算机体系。当前计算机主要是基于冯诺依曼体系结构设计的,下面就简单分析一下冯诺依曼体系结构的计算机是如何工作的,首先下面的图就是冯诺依曼体系结构图。
冯诺依曼计算机体系结构,当前流程计算机组成如下:
引发的问题:如果单纯依靠上述体系来做计算机处理,性能不高。主要瓶颈在IO上,比如磁盘IO、网络IO等的读取、写入;而CPU性能非常高,往往是大部分时间是闲置的,就是没有充分利用了CPU的性能。如下图所示:
为了解决此问题,科学家、计算家们通过两种方式优化了计算性能不高问题:1、增加内存、缓存;2、优化软件结构
导入了内存、缓存(1级、2级、3级)、寄存器;需要数据各层进行复制
引入了进程、线程,让线程抢占式的利用CPU资源;需要接管计算机硬件资源,任务调度、进程管理【线程】;在Java 层面即是synchronized、volatile、CAS、Lock、并发工具类、CountDownLatch、CyclicBarrier、Future、线程池等。
程序运行的底层原理
- 程序是什么?QQ.exe,PowerPoint.exe
- 进程是什么?程序启动,进入内存,资源分配的基本单元
- 线程是什么?程序执行的基本单位
- 程序如何开始运行? CPU 读指令 - PC(存储指令地址),读数据到寄存器 Register,计算,回写到内存;然后指向下一条指令
- 线程如何进行调度?Linux 线程调度器(OS)操作系统
- 线程切换的概念是什么?Context Switch CPU 保存现场执行新线程,恢复现场,继续执行原线程这样的一个过程
缓存、寄存器
从CPU的计算单元(ALU)到:
Registers | < 1 ns |
L1 cache | 约 1ns |
L2 cache | 约 3ns |
L3 cache | 约 15ns |
main memory | 约 80ns |
根据空间局部性原理,按块读取,程序局部性原理,可以提高效率;充分发挥总线、CPU针脚等一次性读取更多数据的能力。
MESI Cache 缓存一致性协议
缓存一致协议是Modified、Exclusive、Shared、Invalid 四个单词的缩写,详细知识参考:https://www.cnblogs.com/z00377750/p/9180644.html
Intel 的缓存一致性协议叫MESI,其他处理器的缓存一致性协议不叫MESI,叫:MSI、MOSI、Synapse Firefly Dragon
缓存行Cache Line:
- 缓存行越大,局部性空间效率越高,但读取时间慢
- 缓存行越小,局部性空间效率越低,但读取时间快
- 取一个折中值,目前多用:64字节【工业实践得出的结论】
根据缓存行一致性协议,如果x的值被修改,在多线程环境下需要需要被通知到另外的其他线程。这个通知过程是需要花费时间的。
代码示例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原理图如下:
CAS 引发的问题
文章最后,给大家推荐一些受欢迎的技术博客链接:
- JAVA相关的深度技术博客链接
- Flink 相关技术博客链接
- Spark 核心技术链接
- 设计模式 —— 深度技术博客链接
- 机器学习 —— 深度技术博客链接
- Hadoop相关技术博客链接
- 超全干货--Flink思维导图,花了3周左右编写、校对
- 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
- 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
- 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂
- 深入聊聊Java 垃圾回收机制【附原理图及调优方法】
欢迎扫描下方的二维码或 搜索 公众号“大数据高级架构师”,我们会有更多、且及时的资料推送给您,欢迎多多交流!