监视器锁monitor lock
- synchronized用法
- 1.修饰普通方法
- 2.修饰一个代码块
- 3.修饰一个静态方法
- synchronized的特性
- 1.互斥
- 2.刷新内存:和volatile类似
- 3.不可重入/可重入
- synchronized的锁优化机制
- 1.锁膨胀/锁升级
- 2.锁粗化
- 3.锁消除
- 死锁的其他场景
- 标准库中线程安全/不安全类
- JJM(Java memory Model)Java内存模型
- 让线程执行按照固定顺序wait.notify
synchronized用法
使用synchronized 的时候,本质上是在针对某个“对象”进行加锁
synchronized的本质操作,是修改了对象中的”对象头”里面的一个标记
释义为同步,同步在多线程中指的是互斥.
1.修饰普通方法
锁的对象是 this,在this的对象头设置标志位
当两个线程同时尝试对同一个对象加锁的时候,才有竞争如果是两个线程在针对两个不同对象加锁,就没有竞争
2.修饰一个代码块
2.修饰一个代码块
需要显式指定针对哪个对象加锁.(Java中的任意对象都可以作为锁对象)
这种随手拿个对象都能作为所对象的用法,这是Java中非常有特色的设定.(别的语言都不是这么搞.正常的语言都是有专门的锁对象)
3.修饰一个静态方法
相当于针对当前类的类对象加锁.
Counter.class(反射)
上图两个方法等价
synchronized的特性
1.互斥
2.刷新内存:和volatile类似
3.不可重入/可重入
同一个线程对同一个锁,连续加锁两次,
出现死锁,就是不可重入
没有不会出现死锁,就是可重入
外层锁:进入方法,加锁成功
里层锁:进入代码块,因为两次加锁都是同一对象,得等外层锁解锁后,才能再次加锁,
故:陷入了死锁
而synchronized是可重入锁,故上述代码片段不会导致死锁,
可重入锁内部,会记录当前的锁被哪个线程占用的同时也会记录一个”加锁次数”线程a针对锁第一次加锁的时候,显然能够加锁成功,锁内部就记录了当前的占用着是a,同时加锁次数为1.后续再a对锁进行加锁,此时就不是真加锁,而是单纯的把计数给自增,加锁次数变为2,后续再解锁的时候,先把计数进行-1 .当锁的计数减到0的时候,就真的解锁
synchronized的锁优化机制
1.锁膨胀/锁升级
无锁->偏向锁->自旋锁->重量级锁
偏向锁:首个线程加锁,就会先进入偏向锁状态.偏向锁,不是真的加锁,只是做了个标记,带来的好处就是后续如果没人竞争的时候,就避免了加锁解锁的开销
自旋锁:又有其他线程加锁产生竞争了,进入轻量级锁状态
重量级锁:如果竞争进一步加剧,就会进入重量级锁状态.
这里的偏向锁,和懒汉模式也有点像,只是在必要的时候,才进行操作如果不必要,则能省就省
2.锁粗化
加锁代码涉及到的范围.
加锁代码的范围越大,认为锁的粒度越粗范围越小,则认为粒度越细
编译器就会有一个优化,就会自动判定说,如果某个地方的代码锁的粒度太细了就会进行粗化,如果两次加锁之间的间隔较大(中间隔的代码多),一般不会进行这种优化.如果加锁之间间隔比较小(中间隔的代码少),就很可能触发这个优化.
3.锁消除
有些代码,明明不用加锁,结果你给加上锁了,编译器就会发现这个加锁好像没啥必要,就直接把锁给去掉了.
死锁的其他场景
1.一个线程,一把锁
2.两个线程,两把锁
3.N个线程,M把锁
死锁的四个必要条件
1.互斥使用~ -个锁被一个线程占用了之后,其他线程占用不了(锁的本质,保证原子性)
2.不可抢占-一个锁被一 个线程占用了之后,其他的线程不能把这个锁给抢走. (挖墙脚是不行的)
3.请求和保持当-一个线程占据了多把锁之后,除非显式的释放锁,否则这些锁始终都是被该线程持有的~
4.环路等待等待关系,成环了~~ A等B,B等C,C又等A
要避免环路等待
标准库中线程安全/不安全类
安全
不安全
JJM(Java memory Model)Java内存模型
cpu->工作内存(work memory)
雷村->主内存(main memory)
让线程执行按照固定顺序wait.notify
wait内部会做三件事
1.先释放锁
⒉.等待其他线程的通知.
3.收到通知之后,重新获取锁,并继续往下执行
因此要想使用wait / notify,就得搭配 synchronized
public class Demo18 { private static Object locker = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { // 进行 wait synchronized (locker) { System.out.println("wait 之前"); try { locker.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("wait 之后"); } }); t1.start(); Thread.sleep(3000); Thread t2 = new Thread(() -> { // 进行 notify synchronized (locker) { System.out.println("notify 之前"); locker.notify(); System.out.println("notify 之后"); } }); t2.start(); }}
wait notify.都是针对同一个对象来操作的.
例如现在有一个对象O~~
有10个线程,都调用了o.wait. 此时10个线程都是阻塞状态.
如果调用了o.notify,就会把10个其中的一个给唤醒.(唤醒哪个?不确定)
针对notifyAll,就会把所有的10个线程都给唤醒
wait唤醒之后,会重新尝试获取到锁~~(这个过程就会发生竞争)
相对来说,更常用的还是notify