多线程学习Day03

Montior

Java对象头

Integer 8+4字节   而int 4字节

普通对象

数组对象

Mark Word 结构

64 位虚拟机 Mark Word

Monitor

可翻译成监视器或者管程

每个java对象都可以关联一个Monitor对象,如果使用synchroinzed给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针

这是Monitor的结构

这里相当于这个锁的拥有者时Thread-2,Thread3,4,5在阻塞中等待锁,执行完同步代码的内容之后,唤醒EntryList中的线程来竞争锁,竞争是非公平的

synchronizedh的原理

....描述不出来,来个小故事把

故事角色
老王 - JVM
小南 - 线程
小女 - 线程
房间 - 对象
房间门上 - 防盗锁 - Monitor
房间门上 - 小南书包 - 轻量级锁
房间门上 - 刻上小南大名 - 偏向锁
批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向
小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。
但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。
后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。
于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字。
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包

轻量级锁

使用场景:一个对象虽然有多线程访问,但是访问的时间是错开的(也就是说没有竞争),可用轻量级锁来优化,它对使用者是透明的,还用synchronized

public class Test11 {
    static final Object obj=new Object();
    static int counter=0;

    public static void main(String[] args) {

    }
    public static void method1(){
        synchronized (obj){
            //同步块A
            method2();
        }
    }
    public static void method2(){
        synchronized (obj){
            //同步块B
        }
    }
}

创建锁记录对象,对每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

                    

让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录,00表示的是轻量级锁

                 

如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁

               

如果cas失败,有两种情况

1.如果其他线程已经持有了该Object的轻量级锁,表明有竞争,进入锁膨胀过程

2.如果自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数

           

解锁时,如果由null的锁记录,表示有重入,重置锁记录,重入计数减一

            

如果锁记录不为null,这时使用cas将Mark Word的值恢复给对象头,如果成功就成功了,失败就说明现在已经时重量级锁了,进入重量级锁的解锁过程,按照Monitor地址找到Monitor对象,设置owner为null,唤醒EntryList中BLOCKED线程。

锁膨胀

尝试在加轻量级锁的过程中,CAS操作无法成功,可能时其他线程为此对象加上了轻量级锁,这时需要进行锁膨胀,变成重量级锁

失败了开始申请Monitor锁,线程1被阻塞了

自旋优化

重量级锁竞争时,可用自旋来进行优化(多核CPU才有意义),如果自旋成功就可以避免阻塞了,省去了上下文切换的开销。java6之后自旋锁是自适应的,比如对象刚自旋成功过一次,之后就会多自旋几次,java7之后不能控制自旋是否开启

偏向锁

轻量级锁在没有竞争时,每次重入仍需要CAS操作

偏向锁(对应的也是01)只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID时自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

 static final Object obj = new Object();
    public static void m1() {
        synchronized( obj ) {
            // 同步块 A
            m2();
        }
    }
    public static void m2() {
        synchronized( obj ) {
            // 同步块 B
            m3();
        }
    }
    public static void m3() {
        synchronized( obj ) {

            // 同步块 C
        }
    }

          

偏向状态

一个对象创建时: 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 - XX:BiasedLockingStartupDelay=0 来禁用延迟 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值。码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁。

偏向锁撤销

撤销 - 调用对象 hashCode 调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被 撤销 轻量级锁会在锁记录中记录 hashCode 重量级锁会在 Monitor 中记录 hashCode 在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking

撤销 - 其它线程使用对象 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

撤销 - 调用 wait/notify,这个机制只有重量级锁有

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象 的 Thread ID

当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至 加锁线程

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象 都会变为不可偏向的,新建的对象也是不可偏向的

锁消除

在Java中,锁消除是一种JVM优化技术,其目的是去掉不必要的同步操作,从而提升程序的性能。锁消除是基于这样一个事实:如果确定一段代码中的同步操作是不可能被多个线程同时访问的,那么这个同步操作就是多余的,可以安全地移除。

当JVM通过逃逸分析确认了某个锁对象不会被其他线程访问时,它就可以安全地消除这个锁,无需实际执行同步操作。这种情况通常出现在:

  • 同步块仅被单个线程访问:如果在一个同步块中,涉及的数据结构(如某个局部对象)不会被其他线程所访问,那么这个同步块实际上是线程安全的,JVM可以消除这个同步块的锁。

  • 锁定对象没有共享:例如,一个对象在方法中被创建,并且仅在该方法的同步块中使用,之后就不会被引用,这样的对象是不会被其他线程访问的。

 wait notify(偏向锁终于结束了)

       

Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态

BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片

BLOCKED 线程会在 Owner 线程释放锁时唤醒

WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争

API介绍

obj.wait() 让进入 object 监视器的线程到 waitSet 等待
obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法

@Slf4j(topic = "c.Test11")
public class Test11 {
    final static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        }).start();
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        }).start();
// 主线程两秒后执行
        sleep(2000);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notify(); // 唤醒obj上一个线程
 //obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }

}
正确使用姿势

sleep和wait的区别

1) sleep 是 Thread 方法,而 wait 是 Object 的方法

2) sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用

3) sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁

4)它们状态 TIMED_WAITING(有时限的等待)

@Slf4j(topic = "c.Test12")
public class Test12 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    /*try {
                        //等待时间其他线程一直阻塞
                        sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }*/
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }
        sleep(1000);
        new Thread(() -> {
            synchronized (room){
                hasCigarette = true;
                log.debug("烟到了噢!");
                room.notify();
            }

        }, "送烟的").start();
    }
}

结果

16:41:57 [小南] c.Test12 - 有烟没?[false]
16:41:57 [小南] c.Test12 - 没烟,先歇会!
16:41:57 [其它人] c.Test12 - 可以开始干活了
16:41:57 [其它人] c.Test12 - 可以开始干活了
16:41:57 [其它人] c.Test12 - 可以开始干活了
16:41:57 [其它人] c.Test12 - 可以开始干活了
16:41:57 [其它人] c.Test12 - 可以开始干活了
16:41:58 [送烟的] c.Test12 - 烟到了噢!
16:41:58 [小南] c.Test12 - 有烟没?[true]
16:41:58 [小南] c.Test12 - 可以开始干活了

解决了其它干活的线程阻塞的问题
但如果有其它线程也在等待条件,像下面这样,只用notify就可能会有问题了,notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线
程,称之为【虚假唤醒】解决方法,改为 notifyAll

@Slf4j(topic = "c.Test13")
public class Test13 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    //虚假唤醒
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {

                    log.debug("没干成活...");
                }
            }
        }, "小南").start();
        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();
        sleep(1000);
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                room.notifyAll();
            }
        }, "送外卖的").start();
    }
}
16:48:12 [小南] c.Test13 - 有烟没?[false]
16:48:12 [小南] c.Test13 - 没烟,先歇会!
16:48:12 [小女] c.Test13 - 外卖送到没?[false]
16:48:12 [小女] c.Test13 - 没外卖,先歇会!
16:48:13 [送外卖的] c.Test13 - 外卖到了噢!
16:48:13 [小南] c.Test13 - 有烟没?[false]
16:48:13 [小南] c.Test13 - 没干成活...
16:48:13 [小女] c.Test13 - 外卖送到没?[true]
16:48:13 [小女] c.Test13 - 可以开始干活了

解决小南的问题,把if (!hasCigarette)换成while

这里

synchronized (lock){
        while(条件不成立){
            lock.wait;
        }
        //干活
    }
    //另一个线程
    synchronized (lock){
        lock.notifyAll();
    }

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值