多线程编程
1.同步: 线程间协作
2.互斥:独占锁
3.分工:大任务的拆解
并发相关的技术
1.java线程模型(JMM)
并发特性:原子性,有序性,可见性
2.线程
java中的线程是内核级别的线程,new Thread()也是一个普通的类,只是调用了 native方法,JVM不具备调度CPU的权限, javaThread—>osThread—>pthead_create,java中new Thread只是建立了一个映射关系,绑定了jvm的osThrea,JVM再去调用操作系统创建一个线程,所以线程的创建是有开销的,所以使用线程池,达到线程复用
3.锁机制 同步器(目的就是让多个线程序列化的访问临界资源)
内置锁:synchronized
juc: 独占锁,共享锁,读写锁 AQS(抽象队列同步器)
4.工具类&并发容器
5.并行
并发的风险
性能问题:多线程情况下性能的消耗在于线程的切换,线程1保存当前的操作,线程2也有可能去加载上一次保存的数据
活跃性问题(饥饿,死锁,活锁)
线程安全
并发和并行的区别
并发是一种程序的逻辑结构的设计模式,同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,涉及到时间片的切换
并行是在一个时刻有多条指令再处理器上同时执行,计算器必须要是有多核才能发生并行的情况,每个核上同时有进程再跑
JMM线程模型
JMM模型是java内存模型 主要是针对于线程
JMM主要是围绕原子性,有序性,可见性,其中使用volatile关键字解决了有序性和可见性底层原理是通过locak addL指令的特性,还有cup缓存一致性(MESI) 和内存屏障来解决的,原子性是通过cas(比较交换) 解决的
volatile关键字主要做了两件事
保证了可见性(伪共享),使用Lock指令加了一个主存的总线锁,使用 CPU缓存一致性协议MESI (M:修改 ,E:独占,S:共享,I无效:)
线程中有本地内存(L1,L2,L3三级缓存) 数据首先再主内存中 ,线程使用的时候会通过3—2-1的顺序加载到内存中,最后在寄存器中处理,当A线程加载到数据的时候,数据的状态为E,这时候B线程也加载了变量,变量的状态改为S(这里是一种嗅探机制),线程二再寄存器中对变量进行了赋值或者改变原来的值,会先刷会线程的本地内存,这时候B线程中变量的状态为M,通过嗅探机制会将A线程中的变量的状改为I,B线程会将修改后的数据优先刷回主存中,这个是A线程会通过主存去获取最新的数据到本地内存中,这样保证了伪共享
在MESI的基础上做了优化,出现了MOESI,主要就是优化点在于数状态修改为M(修改状态)的时候,别的线程标记为I(无效),这个时候线程会通过广播的方式吧修改后的值直接复制到别的线程的缓存中,避免了刷回主存的性能开销。
怎么避免伪共享,可以使用缓存行填充活着java8中的@Contended注解来避免
缓存行填充原始数据:
{
long X;
// TODO 填充
long Y
}
一个long类型为8字节(8byte)可以在X后面再添加7个long类型,这样一个X就会吧一个缓存行填满,迫使Y去开辟新的缓存行,空间换时间的体现
禁止重排序通过C++底层再变量前后添加内存屏障来保证的
什么是重排序,重排序是怎么发生的,如何禁止,禁止的原理是什么
重排序是指再执行汇编指令的时候发生的重新排序的行为,例如一个对象的创建 A a = new A(); ,这其中有三个步骤 第一步先分配一片内存空间,第二部初始化对象,第三部将分配好的内存地址执行初始胡后的对象,这个步骤在单线成的时候及时发生了重排序也不会印象最后的结果,但是在多线程中,执行流程经过重排序后顺序为132,当第一个线程执行到13后,将要执行2的时候,这时候另一个线程通过 a != null 的方式拿到了A的单例对象的,实际上当前的A还没有被初始号,这个时候就会发生问题
通过在变量前用volatile修饰,可以禁止重排序
被volatile修饰的变量操作的时候回添加一个内存屏障
一共有四种内存屏障
volatile x ;
StoreStore // 内存屏障
x = 1; // 写操作
StoreLoad // 内存屏障
b = x // 读操作 要去读取x的值
LoadLoad // 内存屏障
LoadStore // 内存屏障
还可以手动的去添加一个内存屏障使用Unsafe类中的storeFence()去手动添加一个内存屏障,内存屏障底层也有使用locak addL 指令实现的
CAS
cas 比较交换,是一只无锁的算法保证了原子性,
atomicInteger类就是通过cas保证的原子性,使用volatile保证可见性和禁止重排序
cas中原理是有有一个当前值,还有偏移量,还有当前值的内存地址,在修改的时候会先通过内存地址从主内存中获取到当前值的数据,接下来吧内存中的数据值与当前值进行比较,相等的活就进行偏移,不相等的话就进行自旋操作,只到主内存中的数据与当前值相等,结果返回true 外层通过!true的方式跳出循环
cas的缺点是只能保证一个变量的原子操作,同时长时间的自旋会给cup带来压力最后还有ABA的问题
什么是ABA问题
例子:两个线程A,B 还有一个变量X初始值为1
A线程中要对X进行+1的操作,这时候B线程也拿到了变量X,B线程中显示对X变量进行了+10,又对X变量进行了-10,再把X写回主内存,这个时候线程A中通过内存地址从主内存中获取到了X的最新的值和当前值进行比较,比较结果为相等,进行+1的操作,但是实际上当前变量X已经被B线程操作过了,这就好比是某人挪用了公款,再被发现前把钱补回来了,没有本人发现,但是现在的钱已经不是原来的钱了,挪用公款的人可能对钱进行了操作,生出了更多的钱,这就是ABA问题
LongAdder优化了ABA的问题,实际上就是添加了版本号(时间戳+值)