死锁 可重入锁
一个线程,连续针对一把锁,加锁两次,就可能造成死锁
形如上述代码,一个线程针对一把锁,连续枷锁两次,第一次加锁,能够加锁成功
第二次加锁,就会加锁失败(锁已经被占用)
就会在第二次加锁这里阻塞等待,等到第一把锁被解锁,第二次加锁才能成功!
第一把锁解锁,则要求执行完synchronized
代码块,也就是要求第二把锁能加锁成功!
针对上述情况,不会产生死锁的话,这样的锁就叫做“可重入锁”
针对上述情况,会产生死锁,这个锁叫做“不可重入锁”
synchronized
这个锁就是可重入的!
可重入锁底层实现,只需让让锁里面记录好是哪个线程持有的这把锁就可以了!
t
线程尝试针对this
来加锁~~this
这个锁里面就记录了是t
线程持有了它
第二次进行加锁的时候,锁发现还是t
线程,就直接通过了,不会阻塞等待!
那怎么知道执行到加锁代码,要不要真正的解锁吗?
引入一个计数器!!
每次加锁,计数器++
每次解锁,计数器–;
如果计数器为0,此时的加锁操作才真加锁
同样,计数器为0,此时的解锁操作才真解锁
💞可重入锁的实现要点:
- 让锁里持有线程对象,记录是谁加了锁
- 维护一个计数器,用来衡量什么时候是真加锁,啥时候是真解锁,啥时候是直接放行
如果程序抛出异常,没人catch
,就脱离了之前的代码块,脱离一层代码块,计数器就-1,脱离多层代码块,减到0了也就解锁了~
加锁代码中出现异常是不会死锁的~无论如何解锁代码都能执行到的!
这也是把synchronized
设计成以一个关键字的原因~
结合代码块来做解锁操作,无论是正常执行完代码块,还是异常执行完代码块,都会触发解锁操作!
内存可见性问题
💦内存可见性问题:在编译器优化的背景下,一个线程把内存给改了,另一个线程并不能及时的感知到!
例如:
- 一个线程负责读数据(LOAD),一个线程负责修改数据,假设,读操作非常频繁
当频繁进行多次读内存(LOAD)和进行比较(CMP)时,程序就不再每次循环都重新读取内存,而是只读一次,后续的读内存则简化成直接读寄存器!
但这样是有问题的!
编译器对于多线程代码的判定并没有那么准,当其他线程把当前内存中的数据进行改动的话,编译器会出现误判操作~
比如我把上面的代码运行:
❗线程没有结束!
💌解决方案:避免让编译器做出"不读内存"的优化! 优化的目的是为了算的快,但前提是算得准!!! |
解决内存可见性问题的方案
volatile
“可变的” : 可以用这个关键字来修饰一个变量,此时被修饰的变量,编译器就不会做出"不读内存"的优化
但是并不保证原子性~
针对一个线程读,一个线程修改,这个场景,使用volatile
是合适的~
但是根据两个线程同时修改,volatile
是起不到作用的!
volatile
禁止了编译器优化,避免了直接读取CPU寄存器中(工作内存)缓存的数据,而是每次都重新读内存(主内存)!
站在JVM的角度来看待volatile
:
正常程序执行的过程中,会把主内存的数据,先加载到工作内存中,再进行计算处理
编译器优化可能会导致不是每次都真的读取主内存,而是直接去工作内存中的缓存数据(就可能导致内存可见性)
volatile
起到的效果,就是保证每次读取内存都是从主内存重新读取
此处工作内存(cpu寄存器)并不是真的内存!!主内存才是真的内存!
❓为什么说:"工作内存“和硬件无关?
CPU内部的结构是不一样的~
有的CPU只带寄存器
有的CPU除了寄存器,还带缓存
有的CPU缓存还有多级缓存(L1,L2,L3)
所以不同的CPU方式不一样,此时使用"工作内存"就指上面的全部(寄存器+缓存)!
JVM通过了一个更高层次的抽象来简化了实际的硬件数据交互过程,并且更加的宏观,不管是什么样的CPU都可以叫做"工作内存”!!