监视器锁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