bilibili尚硅谷周阳老师JUC并发编程与源码分析课程笔记第三章——说说Java“锁“事

文章目录

说说Java“锁“事

大厂面试题复盘

并发编程高级面试解析

一、Synchronized 相关问题

  1. Synchronized 用过吗,其原理是什么?
  2. 你刚才提到获取对象的锁,这个"锁"到底是什么? 如何确定对象的锁?
  3. 什么是可重入性,为什么说 Synchronized 是可重入锁?
  4. JVM 对 Java 的原生锁做了哪些优化?
  5. 为什么说 Synchronized 是非公平锁?
  6. 什么是锁消除和锁粗化?
  7. 为什么说 Synchronized 是一个悲观锁? 乐观锁的实现原理又是什么? 什么是 CAS ,它有
  8. 乐观锁一定就是好的吗

二、可重入锁 ReentrantLock 及其它显式锁相关问题

  1. 跟 Synchronized 相比,可重入锁 其实现原理有什么不同?
  2. 那么请谈谈 AQS 框架是怎么回事儿?
  3. 请尽可能详尽地对比下 Synchronized 和 ReentrantLock 的异同
  4. ReentrantLock 是如何实现可重入性的?

三、其它

  1. 你怎么理解 Java 语言多线程的? 怎么处理并发? 线程池有那几个核心参数?
  2. Java 加锁有哪儿种锁? 我先说 synchronized,刚讲到偏向锁,他就不让我讲了,太自信了
  3. 简单说说锁?
  4. hashmap的实现原理? hash冲突怎么解决? 为什么使用红黑树?
  5. spring 里面都使用了哪些设计模式? 循环依赖怎么解决?
  6. 项目中哪个地方用了 CountDownLatch,怎么使用的?

从轻松的乐观锁和悲观锁开讲

悲观锁

认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改

synchronized和Lock的实现类都是悲观锁

适合写操作多的场景,先加锁可以保证写操作时数据正确,显示的锁定之后再操作同步资源

一句话定义:狼性锁

乐观锁

认为自己在使用数据的时候不会有别的线程修改数据或资源,所以不会添加锁

Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据

  • 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
  • 如果这个数据已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如:放弃修改、重试抢锁等等

判断规则

  • 版本号机制Version
  • 最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的

适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升

乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再努力就是

一句话定义:佛系锁

乐观锁一般有两种实现方式:

  • 采用Version版本号机制
  • CAS(Compare-and-Swap,即比较并替换)算法实现

伪代码说明

在这里插入图片描述

通过8种情况演示锁运行案例,看看我们到底锁的是什么

锁相关的8种案例演示code

阿里巴巴Java开发手册

在这里插入图片描述

Code实验
package com.bilibili.juc.lock;

import java.util.concurrent.TimeUnit;

/**
 * 题目:谈谈你对多线程锁的理解,8锁案例说明
 * 口诀:线程 操作 资源类
 * 8锁案例说明:
 * 1. 标准访问ab两个线程,请问先打印邮件还是短信? --------先邮件,后短信  共用一个对象锁
 * 2. sendEmail钟加入暂停3秒钟,请问先打印邮件还是短信?--------先邮件,后短信  共用一个对象锁
 * 3. 添加一个普通的hello方法,请问先打印普通方法还是邮件? --------先hello,再邮件  资源没有争抢,hello方法没有用到对象锁
 * 4. 有两部手机,请问先打印邮件还是短信? --------先短信后邮件  资源没有争抢,不是同一个对象锁
 * 5. 有两个静态同步方法,一部手机, 请问先打印邮件还是短信?--------先邮件后短信  共用一个类锁
 * 6. 有两个静态同步方法,两部手机, 请问先打印邮件还是短信? --------先邮件后短信  共用一个类锁
 * 7. 有一个静态同步方法,一个普通同步方法,一部手机,请问先打印邮件还是短信? --------先短信后邮件  一个类锁一个对象锁
 * 8. 有一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信? ---------先短信后邮件  一个类锁一个对象锁
 */
public class Lock8Demo {
   
   

    public static void main(String[] args) {
   
   
        Phone phone = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> phone.sendEmail(), "a").start();

        // 暂停200毫秒,保证线程先启动
        try {
   
   
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }

        new Thread(() -> /*phone.sendSMS()*/ /*phone.hello()*/ phone2.sendSMS(), "b").start();
    }

}

// 资源类
class Phone {
   
   
    public static synchronized void sendEmail() {
   
   
        try {
   
   
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
        System.out.println("--------sendEmail--------");
    }
    public /*static*/ synchronized void sendSMS() {
   
   
        System.out.println("--------sendSMS--------");
    }
    public void hello() {
   
   
        System.out.println("--------hello--------");
    }
}
案例总结

1-2:

一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法,其它线程都只能等待。换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法。锁的是当前对象this,被锁定后,其它线程都不能进入到当前对象的其它的synchronized方法

3-4:

加个普通方法后发现和同步锁无关

换成两个对象后,不是同一把锁了,情况立即变化

5-6: 都换成静态同步方法后,情况又变化

三种synchronized锁的内容有一些差别:

  1. 对于普通同步方法,锁的是当前实例对象,通常指this,所有的同步方法用的都是同一把锁—>实例对象本身(即Phone phone = new Phone();)
  2. 对于静态同步方法,锁的是当前类的Class对象,即Phone.class唯一的一个模板
  3. 对于同步方法块,锁的是synchronized括号内的对象

7-8

当一个线程试图访问同步代码时,它首先必须得到锁,正常退出或抛出异常时必须释放锁

所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this。也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其它普通同步方法必须等待获取锁的方法释放锁后才能获取锁

所有的静态方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板class。具体实例对象this和唯一模板class,这两把锁是两个不同的对象,所以静态同步方法和普通同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其它的静态同步方法都必须等待该方法释放锁后才能获取锁

解释说明-小总结

在这里插入图片描述

synchronized有三种应用方式

JDK源码(notify方法)说明举例

在这里插入图片描述

8种锁的案例实际体现在3个地方
  • 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁
  • 作用于代码块,对括号里配置的对象加锁
  • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁

从字节码角度分析synchronized实现

javap -c ***.class 文件反编译

javap -c ***.class:对代码进行反编译

假设你需要更多信息:javap -v ***.class 文件反编译,-v代表-verbose:输出附加信息(包括行号、本地变量表、反汇编等详细信息)

synchronized同步代码块
Code
package com.bilibili.juc.lock.sync;

public class LockSyncDemo {
   
   

    Object object = new Object();

    public void m1() {
   
   
        synchronized (object) {
   
   
            System.out.println("--------hello synchronized code block--------");
        }
    }

    public static void main(String[] args) {
   
   

    }

}
反编译

通过javap -c LockSyncDemo.class反编译

在这里插入图片描述

实现使用的是monitorenter和monitorexit指令

一定是一个enter两个exit吗?

一般情况下是一个enter对应两个exit

极端情况:m1方法里面自己添加一个异常试试

package com.bilibili.juc.lock.sync;

public class LockSyncDemo {
   
   

    Object object = new Object();

    public void m1() {
   
   
        synchronized (object) {
   
   
            System.out.println("--------hello synchronized code block--------");
            throw new RuntimeException("--------exp--------");
        }
    }

    public static void main(String[] args) {
   
   

    }

}

反编译结果:一个enter对应一个exit

在这里插入图片描述

synchronized普通同步方法
Code
package com.bilibili.juc.lock.sync;

public class LockSyncDemo2 {
   
   

    public synchronized void m2() {
   
   
        System.out.println("--------hello synchronized m2--------");
    }

    public static void main(String[] args) {
   
   

    }

}
反编译

通过javap -v LockSyncDemo2.class反编译

在这里插入图片描述

小总结

synchronized普通同步方法调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程会将现持有monitor锁,然后再执行该方法,最后在方法完成(无论是否正常结束)时释放monitor

synchronized静态同步方法
Code
package com.bilibili.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值