线程并发笔记(一)
三个概念
1、可见性;
2、有序性;
3、原子性;
一、可见性
并发问题都是程序在不合适的时间读取了不该读取的数据,所以想要透彻弄明白并发实质还是需要看计算机的数据如何存储。
计算的存储大体分为四个地方:硬盘(我们数据持久化之类的都是说的在这里)、内存、高速缓存、存储器。离cpu 越近的,cpu 读取速度越高,按照离cpu 的距离排序:寄存器、高速缓存、内存、硬盘。
这里我们主要讲一下高速缓存,缓存其实就是内存的部分拷贝,主要集成在cpu芯片上的, 也是分了等级:三级缓存、二级缓存、一级缓存,按照等级,越低的缓存离cpu越近,存储的数据也就越小,一级缓存的数据大小为 64字节。
L1:CPU一级缓存;
L2:CPU二级缓存;
L3:CPU三级缓存;
其中 L3 是计算 CPU共享的,L1、L2是CPU中 核 独享的;
现在我们如果运行一个程序,那么数据是如何流转的?
首先是硬盘将数据通过总线等传输给内存,然后内存将数据传输到高速缓存,高速缓存在将数据给到寄存器,cpu直接读取寄存器数据。由此看出,cpu 读取数据都是从最近的开始读取,所以我们每次运行程序的时候都是第一次比较慢一些,之后的运行速度会比较快一点。这是因为,第一次cpu 读取时发现数据不在,然后一层一层的往下找,然后将数据在一层一层的写到 内存、高速缓存上。
单核来说 引入高速缓存没问题,但是多核的缓存是不是会有一致性的问题呢?
现在我们如果是多核的cpu 然后是每个cpu 运行数据的一部分,按照形式上都是内存到 缓存然后到寄存器的,但是缓存这里是 L2\L1 是单核独享的,如何保证两个核心读取一份数据能够保持一致?这就引出伪共享
线程1、2公共使用同一个CacheLine
x、y在同一个CacheLine
x、y都是volatile
如果线程1不断修改x,线程2不断修改y,那么修改的时候线程1就要不断通知线程2更新x、线程2就要不断通知线程1更新y
这里其实是缓存一致性协议,根据cpu 的厂商不通他们用的协议不一样,比如因特尔的叫做 MESI 协议等
这样的不断通知不断重新读取很浪费性能
这就叫伪共享
注意这是在计算机硬件级别,并不是程序控制的
所以在java程序中很多为了避免这种性能的浪费,采取了以空间换时间的方法-> 将主要的数据单独放到一个缓存行中。比如多线程框架中的 RingBuffer 这个类
他们使用7个long (其实继承的父类也有7个long)类型的数据占啦56个字节,也就是可以保证无论如何每个数据单独在一个缓存行中,这样就免来回通知的问题啦。
|long | long | long | long | long | long | long | data | long | long | long | long | long | long | long |
这也就是我们多线程并发说的可见性
二、有序性
第二个问题就是我们程序写的逻辑都是从上到下按照我们写的逻辑执行么?
不一定,
这里说一下在执行程序时为了提高性能,提高并行度,编译器和处理器常常会对指令做重排序
为了性能,单线程并不是保证严格的数据一致性,它保证的是数据的最终一致性
比如 int a=1;int b=1; 可能执行的顺序就是 int b=1;int a=1;
但是指令重排 会遵守数据的依赖性,也就是 如果 int a=1;int b=1;int c =a+b;
虽然 a\b 的赋值顺序不一定,但是 肯定在c 赋值之前都已经完成啦。
我们可以用DCL 这个案例来说明一下:
懒汉的单例模式
package Singleton;
public class LazySingleton {
//懒汉式单例模式
//比较懒,在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢
private static LazySingleton intance = null;//静态私用成员,没有初始化
private LazySingleton()
{
//私有构造函数
}
public static LazySingleton getInstance()//如果这里加锁,每次拿到这个对象都要同步,这显然不合理,锁一般是锁最小的粒度。
{
if(intance == null)
{
synchronized(LazySingleton.class){
if(intance==null){ //这里在判断一下,是因为第一次可能同时好几个线程进来,然后导致排队取锁,然后重复的new ,这显然还是不行,所以要判断一下null
intance = new LazySingleton();
}
}
}
return intance;
}
}
至此,感觉是不是这个懒汉的单例模式就ok了?加上锁,也不怕多线程的并发问题啦?其实,还是有问题的,我们简单的将对象的创建过程抽象描述一下:
- 申请一块内存
- 执行对象init 方法构造对象,也就是内存填值
- 将引用指针给到变量
这里我们可以看到 步骤二步骤三 不存在依赖关系
如果线程1 正常拿到锁去创建 对象;线程2 在 (完全又可能发生 :线程1 创建对象,但是还没完全创建--步骤二和步骤三 发生指令重排)时判断 对象不为空,直接返回,此时返回的就是一个不完全的对象(object)。
所以我们还需要加 volatile 关键字来禁止指令重排.
这就是多线程并发的有序性。
注:笔记是观看马士兵老师课程与参看其他博客文章写的总结笔记。