java并发编程
2021-01-12 14:29:02 0 举报
AI智能生成
java并发编程
作者其他创作
大纲/内容
线程基础
线程的实现方式只有一种就是构造一个Thread类,最终调用run(),
run里面是要执行的任务。右边本质是一样的
run里面是要执行的任务。右边本质是一样的
继承Thread
thread,start()启动线程,调用被重写的run方法
*实现Runnable(Callable一样)
重写run,并将实现类传给Thread类的对象
CPU高速缓存
带有高速缓存的CPU执行计算的流程
1.程序以及数据被加载到主内存
2.指令和数据被加载到CPU的高速缓存
3.CPU执行指令,把结果写到高速缓存
4.高速缓存中的数据写回主内存
单核cpu多线程非原子操作也会有线程安全问题,在循环次数小的时候看不出问题(执行在一个时间片,循环的耗时处理没有经历过线程切换)
线程切换,线程 栈空间各自有变量的缓存
线程切换,线程 栈空间各自有变量的缓存
多级缓存结构
多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。
这里就引出了一个一致性的协议MESI
这里就引出了一个一致性的协议MESI
MESI协议缓存状态
M 修改 (Modified)
该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。
E 独享、互斥 (Exclusive)
该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。
S 共享 (Shared)
该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。
I 无效 (Invalid)
该Cache line无效。
MESI多核运行
双核读取
1.CPU A发出了一条指令,从主内存中读取x
2.CPU A从主内存通过bus读取到 cache a中并将该cache line 设置为E状态。
3.CPU B发出了一条指令,从主内存中读取x。
4.CPU B试图从主内存中读取x时,CPU A检测到了地址冲突。这时CPU A对相关数据做出响应。
此时x 存储于cache a和cache b中,x在chche a和cache b中都被设置为S状态(共享)。
此时x 存储于cache a和cache b中,x在chche a和cache b中都被设置为S状态(共享)。
修改数据
1.CPU A 计算完成后发指令需要修改x.
2.CPU A 将x设置为M状态(修改)并通知缓存了x的CPU B,
CPU B将本地cache b中的x设置为I状态(无效)
CPU B将本地cache b中的x设置为I状态(无效)
3.CPU A 对x进行赋值。
同步数据
1.CPU B 发出了要读取x的指令
2.CPU B 通知CPU A,CPU A将修改后的数据同步到主内存时cache a 修改为E(独享)
3.CPU A同步CPU B的x,将cache a和同步后cache b中的x设置为S状态(共享)。
问题:缓存的一致性消息传递是要时间的,这就使其切换时会产生延迟。当一个缓存被切换状态时其他缓存收到消息完成各自的切换并且发出回应消息这么一长串的时间中CPU都会等待所有缓存响应完成。可能出现的阻塞都会导致各种各样的性能问题和稳定性问题。等待确认的过程会阻塞处理器,这会降低处理器的性能。因为这个等待远远比一个指令的执行时间长的多。
CPU切换状态阻塞解决
存储缓存(Store Bufferes)
处理器把它想要写入到主存的值写到缓存Store Bufferes,然后继续去处理其他事情。
当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。
当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。
风险
就是处理器会尝试从存储缓存(Store buffer)中读取值,但它还没有进行提交。
这个的解决方案称为Store Forwarding,它使得加载的时候,如果存储缓存中存在,则进行返回。
这个的解决方案称为Store Forwarding,它使得加载的时候,如果存储缓存中存在,则进行返回。
保存什么时候会完成,这个并没有任何保证
CPU A保存着finished在E(独享)状态,而value并没有保存在它的缓存中。(例如,Invalid)。
在这种情况下,value会比finished更迟地抛弃存储缓存。完全有可能CPU B读取finished的值为true,而value的值不等于10。
在这种情况下,value会比finished更迟地抛弃存储缓存。完全有可能CPU B读取finished的值为true,而value的值不等于10。
失效队列
执行失效也不是一个简单的操作,它需要处理器去处理。另外,存储缓存(Store Buffers)并不是无穷大的,
所以处理器有时需要等待失效确认的返回。这两个操作都会使得性能大幅降低。为了应付这种情况,引入了失效队列
所以处理器有时需要等待失效确认的返回。这两个操作都会使得性能大幅降低。为了应付这种情况,引入了失效队列
1.对于所有的收到的Invalidate请求,Invalidate Acknowlege消息必须立刻发送
2.Invalidate并不真正执行,而是被放在一个特殊的队列中,在方便的时候才会去执行。
3.处理器不会发送任何消息给所处理的缓存条目,直到它处理Invalidate
内存屏障
程序自身控制是否优化,内存屏障
写屏障
Store Memory Barrier 告诉处理器在执行这之后的指令之前,应用所有已经在存储缓存(store buffer)中的保存的指令
读屏障
告诉处理器在执行任何的加载前,先应用所有已经在失效队列中的失效操作的指令。
volidate
JVM编译后,生成的汇编指令中,对于volatile标记的变量,会添加一个 Lock前缀
可见性
Lock前缀指令会引起处理器缓存回写到内存
处理器将数据回写到内存会导致其他处理器的缓存无效(包括其他线程工作内存)
禁止指令重排序
是阻止屏障两边的指令重排序
指令重排序
指令重排序概念
编译器优化的重排序
编译器在不改变单线程程序语义的前提下,重新安排语句的执行顺序
指令级并行的重排序
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,
处理器可以改变语句对应机器指令的执行顺序。
处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
重排序影响例子
实例化一个对象步骤
1.分配内存空间
2.初始化对象
3.将内存空间的地址赋值给对应的引用
由于操作系统可以对指令进行重排序,可能执行的顺序
1.分配内存空间。
2.将内存空间的地址赋值给对应的引用。
3.初始化对象
多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果
使用hsdis观察synchronized和volatile
synchronized
底层 lock cmpxchg
本质是CAS改标记成功即上锁成功
volatile
字节码级 ACC_VOLATILE
JVM级内存屏障
屏障两边指令不能重排序,保证有序
as if serial
happends-before
对一个volatile变量的写操作happen-before对此变量的任意操作。
hotspot实现
lock addl 0 往寄存器加0
是否是单核cpu
单核,单个处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果
AtomicLong原子操作类单核也是线程安全的
i++依然在单核多线程下不安全,线程堆栈变量缓存
非单核加锁
LOCK多处理器中执行对共享内存的独占访问,将修改的内存刷新到主存,并使其他cpu缓存失效
即内存可见性
即使用MESI缓存一致性协议
有序的指令无法越过LOCK这个标志的屏障,内存屏障
禁止重排序
两个特性
线程可见性
禁止指令重排
hotspot反汇编器
java语言 解释执行
即时编译器 直接ASM运行编译成汇编码,热点代码会直接执行汇编码,不用再解释执行
如何正确的停止线程
1.正确停止线程的方法: thread.interrupt(),通知线程中断;
2.线程内逻辑需配合响应中断
1)正常执行循环中使用 Thread.currentThread().isInterrupted()判断中断标识;
2)若含有sleep()等Waiting操作,会唤醒线程,抛出interruptedException,抛出后中断标识会重置
对于中断异常,要么正确处理,重新设置中断标识;要么在方法上声明抛出异常以便调用方处理
对于中断异常,要么正确处理,重新设置中断标识;要么在方法上声明抛出异常以便调用方处理
3.为什么用 volatile 标记位的停止方法是错误的
例如 生产-消费模式,含有阻塞put操作时,volatile 标记变量改变也无法唤醒阻塞中的生产者线程
4.stop()、suspend() 和 resume()是已过期方法
有很大安全风险,它们强行停止线程,有可能造成线程持有的锁或资源没有释放。
线程的6中运行状态
wait/notify/notifyAll 方法的使用注意事项
为什么 wait 方法必须在 synchronized 保护的同步代码中使用?
如果不写在同步代码中
1.首先,消费者线程调用 take 方法并判断 buffer.isEmpty 方法是否返回 true,若为 true 代表buffer是空的,则线程希望进入等待,但是在线程调用 wait 方法之前,就被调度器暂停了,所以此时还没来得及执行 wait 方法。
2.此时生产者开始运行,执行了整个 give 方法,它往 buffer 中添加了数据,并执行了 notify 方法,但 notify 并没有任何效果,因为消费者线程的 wait 方法没来得及执行,所以没有线程在等待被唤醒。
3.此时,刚才被调度器暂停的消费者线程回来继续执行 wait 方法并进入了等待。
为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?
1.因为 Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。
2.因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。
wait/notify 和 sleep 方法的异同?
相同
1.它们都可以让线程阻塞。
2.它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常
不同
1.wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
2.在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
3.sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
4.wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。
多级缓存架构
一个cpu中的多核处理器共享L3,每个核有自己的L1,L2
按块读取加载到缓存中,即缓存行,大多数64个字节
缓存行越大,局部性空间效率越高,但读取时间慢
缓存行越小,局部性空间效率越低,但读取时间快
缓存行失效例子,缓存行伪共享
当两个线程访问的数据在同一个缓存行,并有修改操作 且volatile或锁->内存可见
A线程修改会导致B线程的缓存行失效,B线程需要重新从主存中加载数据到缓存中
执行效率变低
demo,对对象T的属性volatile long x修改,前后补齐7个long,
保证对象T的x一定自己独占缓存行,不会有缓存行共享导致修改通知其他cpu的缓存行失效
保证对象T的x一定自己独占缓存行,不会有缓存行共享导致修改通知其他cpu的缓存行失效
disruptor 单机最快的消息队列就是这样实现的
RingBuffer,环形双向链表
1.7 当前访问位置的指针为 long类型,补齐缓存行
1.8 @sun.misc.Contended注解,必须打开-XX:-RestrictContended 被修饰的变量不能和其他数据放在同一行缓存行
合并写Write Combining
cpu的寄存器往外写的buffer
只有4个字节
写满4个字节刷新一次内存
happends-before
1. 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
2. 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
3. volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作。
4. happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happe-before C操作。
5. 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
6. 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
7. 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
8. 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。
线程池
并发容器
原子类
future
java内存模型
死锁
AQS
线程安全
锁
synchronized
CAS 比较并且交换 unsafe底层
无锁或者自旋锁
ABA问题,其他线程有多次修改,最后比较还是A同样修改成功
增加版本号,每次修改版本号+1
native compareAndSwapInt()
hotspot源码 jvm实现
cmpxchg指令 非原子
is_MP是否是多核处理器
多核cpu需要加lock指令lock cmpxchg,内存总线加锁,对该内存的操作不允许被其他cpu打断
单cpu不用加lock,不会被其他cpu打断访问内存,内存不会被修改
锁定某个对象
修改对象的markword的锁标记
锁的四种状态
无锁
偏向锁
将当前线程的指针 放入到对象的markword,代表获取到锁了,对应线程即可以执行自己的任务
为什么有偏向锁?
多数synchronized方法,很多情况下只有一个线程在运行
例如StringBuffer和Vector的sync方法
偏向锁的效率一定会比自旋锁效率高么?
明确知道有多个线程竞争,会导致偏向锁升级自旋锁,升级时需要偏向锁撤销,即修改对象头锁标记
默认打开,jvm启动会有很多锁操作,明确知道有锁竞争,直接使用自旋锁
所以需要延迟开启偏向锁,延迟4s,jvm启动4s之后对象初始偏向锁标记
所以需要延迟开启偏向锁,延迟4s,jvm启动4s之后对象初始偏向锁标记
轻量级锁(自旋锁)
当有其他线程获取锁发现markword有线程指针时,轻度竞争,CAS,写入自己线程栈中Lock Record的指针
不需要经过操作系统内核
升级条件
只要有其他线程同时来获取锁就会从偏向锁升级成轻量级锁
自旋,消耗cpu计算资源
重量级锁
重度竞争升级,需要申请操作系统资源,加LOCK前缀
升级条件
子主题
升级条件
1.6之前 轻量级锁有线程自旋超过10次,太多线程自旋cpu消耗过大,升级为重量级锁;1.6之后,jvm自适应自旋,自动升级重量级锁
有超过cpu核数二分之一的线程在竞争轻量级锁,升级为重量级锁
队列等待,等待中的线程不消耗cpu资源
锁升级过程
jdk1.6之前都是需要切换到内核态-重量级锁,1.6之后优化有了锁升级过程,在某些情况在用户态就可以解决锁的问题
偏向锁和轻量级锁(自旋锁)在用户空间完成的,重量级锁需要经过内核调用
字节码实现
monitorEnter
monitorExit
正常退出
异常finally 退出
对应到汇编码依然是lock cmpxchg
本质是CAS改标记成功即上锁成功
阻塞队列
threadLocal
线程协作
CAS原理
final和不变性
0 条评论
下一页