并发编程
为什么要使用并发编程?
- 充分利用CPU的计算能力。
- 方便进行业务拆分,提升应用性能。
并发编程特性:
- 原子性:是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
。可以通过对于32位系统的来说,long和double数据类型的读写是非原子性的
和synchronized
实现原子性。Lock
- 可见性:是当一个线程修改了某个共享变量的值,其它线程能够马上得知这个修改的值。
关键字可以保证可见性。volatile
和synchronized
也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。Lock
- 有序性:指对于单线程来说,代码的执行是按顺序依次执行。对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致。
关键字可以保证一定的有序性,volatile
(synchronized代码块内部的有序性无法保证)和synchronized
也可以保证有序性,synchronized和Lock保证每个时刻只有一个线程执行同步代码。Lock
并发场景下存在哪些问题?
- 上下文频繁切换。
- 临界区线程安全问题,容易出现死锁,使用
可以排查死锁。jstack
/**
* @author alex
* @description : 并发死锁示例
* @date 2021年04月26日17:53
*/
public class DeadLockTest {
public static final String A = "a";
public static final String B = "b";
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (A) {
System.out.println("get A from t1");
try {
Thread.sleep(2000);
synchronized (B) {
System.out.println("get B from t1");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (B) {
System.out.println("get B from t2");
synchronized (A) {
System.out.println("get A from t2");
}
}
});
t1.start();
t2.start();
}
}
CPU多核缓存架构

电脑缓存是当cpu在读取数据的时候,先是从缓存文件中查找,然后找到之后会自动读取,再输入到cpu进行处理,当然如果没有在缓存中找到对应的缓存文件的话,那么就会从内存中读取并且传输给cpu来处理。当然这样的话需要一定的时间所以会很慢。等cpu处理之后,就很快把这个数据所在的数据块保存在缓存文件中,这样的话在以后读取这项数据的时候就直接在缓存中进行,不要重复在内存中调用并读取数据了。
- 一级缓存都内置在CPU内部并与CPU同速运行,可以有效的提高CPU的运行效率。一级缓存越大,CPU的运行效率越高,但受到CPU内部结构的限制,一级缓存的容量都很小。
- 二级缓存,它是为了协调一级缓存和内存之间的速度。cpu调用缓存首先是一级缓存,当处理器的速度逐渐提升,会导致一级缓存就供不应求,这样就得提升到二级缓存了。二级缓存它比一级缓存的速度相对来说会慢,但是它比一级缓存的空间容量要大。主要就是做一级缓存和内存之间数据临时交换的地方用。
- 三级缓存是为读取二级缓存后未命中的数据设计的—种缓存,在拥有三级缓存的CPU中,只有约5%的数据需要从内存中调用,这进一步提高了CPU的效率。其运作原理在于使用较快速的储存装置保留一份从慢速储存装置中所读取数据并进行拷贝,当有需要再从较慢的储存体中读写数据时,缓存(cache)能够使得读写的动作先在快速的装置上完成,如此会使系统的响应较为快速。
当多个CPU对同一个变量进行操作时,会出现数据不一致的情况,此时该如何处理?
-
阻塞其他CPU,使该处理器可以独享此共享内存。总线加锁
-
缓存一致性协议
缓存一致性协议
这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等,其中 MESI
是缓存一致性协议最为流行的一种实现方式。
MESI工作原理:
- M: Modified 修改
- E: Exclusive 独享、互斥
- S: Share 共享
- I: Invalid 无效
- 当一块内存被某一个CPU(假设为CPU1)读取到缓存中时,会将此内存标记为
,表示内存被独享,此时该CPU也会同时监听其它CPU对这个内存的操作。E
- 当其它CPU(假设为CPU2)读取同一块内存到缓存中时,CPU1会监听到这个动作,此时CPU1和CPU2都会将这块内存的状态标记为
。S
- 当CPU1对这块内存进行了修改,并且需要将修改结果写入主内存中时,此时CPU1将这块数据标记为
,并且对缓存行进行加锁(汇编指令#Lock),告知缓存一致性协议需要将结果写入主内存中,CPU2监听到这一操作时会将CPU2缓存中对这块数据的标记从M
改为S
。I
- 当CPU1写入完成时,CPU1缓存中对这块数据的标记从
改为M
,若CPU2还需要读取这块内存数据,则需要重新从内存中加载,CPU2加载到缓存时,CPU1和CPU2会将这块内存的状态标记为E
。S
- 若CPU1和CPU2需要同时更改,那么在同一个指令周期内会进行裁决,决定由谁进行修改,另一个CPU修改变成无效。
什么情况下缓存一致性协议会失效?
- 如果读取的内存存储长度大于一个缓存行时,可以使用总线加锁的方式
- CPU不支持缓存一致性协议时,如奔腾处理器。
线程
是系统分配资源的基本单位。
进程
是调度CPU的基本单位。一个进程中至少包含一个执行线程。每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(保存当前线程的工作变量),堆栈(记录执行记录,其中每一帧保存了一个已经调用但未返回的过程,栈帧的数量与方法的调用次数一致)
线程
线程依赖于操作系统调度CPU。
线程分为两类:
- 用户级线程(User-Level Thread)
- 内核线线程(Kernel-Level Thread)
用户线程:指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应
用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。另外,用户线程是由应用进程利用线程库创建和管理,不依赖于操作系统核心。不需要用户态/核心态切换,速度快。操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。
内核线程 :线程的所有管理操作都是由操作系统内核完成的。内核保存线程的状态和上下
文信息,当一个线程执行了引起阻塞的系统调用时,内核可以调度该进程的其他线程执行。在多处理器系统上,内核可以分派属于同一进程的多个线程在多个处理器上运行,提高进程执行的并行度。由于需要内核完成线程的创建、调度和管理,所以和用户级线程相比这些操作要慢得多,但是仍然比进程的创建和管理操作要快。大多数市场上的操作系统,如Windows,Linux等都支持内核级线程。
Java线程的生命周期
Java内存模型-JMM
Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了
程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
。JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
Java内存模型交互操作:
- lock(锁定):作用于主内存,将一个变量标识为线程独占。
- unlock(解锁):主内存,释放一个处于锁定状态的变量。
- read(读取):主内存,将变量从主内存传输到工作内存。
- load(载入):工作内存,将read读到的值在工作内存中生成一个副本。
- use(使用): 工作内存,将工作内存的变量传递给执行引擎。
- assign(赋值):工作内存,将执行引擎返回的值赋值给工作内存的变量。
- store(存储):工作内存,将工作内中存变量的值传输给主内存。
- write(写入):主内存,将store操作从工作内存中传输的值写入主内存的变量中。