1、面试题复盘
2、乐观锁和悲观锁
2.1、悲观锁
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized
关键字和Lock
的实现类都是悲观锁
适合写操作多的场景,先加锁可以保证写操作时数据正确。显式的锁定之后再操作同步资源
一句话:
狼性锁
2.2、乐观锁
认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。
在Java
中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。
如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等
判断规则
1、版本号机制Version
⒉、最常采用的是CAS
算法,Java
原子类中的递增操作就通过CAS
自旋实现的。
适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再努力就是
一句话: 佛系锁
乐观锁一般有两种实现方式:
1、版本号机制Version
⒉、最常采用的是CAS
算法,Java
原子类中的递增操作就通过CAS
自旋实现的。
2.3、伪代码说明
//============悲观锁的调用方式
public synchronized void m1(){
//加锁后的业务逻辑
}
// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock = new Reentrantlock();
public void m2(){
lock.lock();
try{
//操作同步资源
}finally{
lock.unlock;
}
}
//=============乐观锁的调用方式
//保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
3、synchronized
3.1、8种锁的案例
package com.lock;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* @PACKAGE_NAME: com.lock
* @NAME: Lock8Demo
* @USER: Mrs.Wang
* @DATE: 2022/9/12
* @TIME: 19:29
* @DAY_NAME_SHORT: 周一
* @DAY_NAME_FULL: 星期一
* @PROJECT_NAME: JUC
**/
class Phone{ //资源类
public synchronized void sendEmail(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("------sendEmail");
}
public synchronized void sendSMS(){
System.out.println("------sendSMS");
}
public void hello(){
System.out.println("hello");
}
}
/**
* 8锁案例睡哦吗
* 1、表征访问有ab两个线程,请问先打印邮件还是短信
* 2、sendMail方法中加入暂停3秒钟,请问先打印邮件还是短信
* 3、添加一个普通的hello方法,请问先打印邮件还是hello
* 4、有两部手机,请问先打印邮件还是短信
* 5、有两个静态同步方法,有一部手机,先打印邮件还是手机
* 6、有两个静态同步方法,有两部手机,先打印邮件还是手机
* 7、有一个静态同步方法,有一个普通同步方法,有一部手机,先打印邮件还是手机
* 8、有一个静态同步方法,有一个普通同步方法,有两部手机,先打印邮件还是手机
*
* 1-2
* 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法 (占锁)
* 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法 (等待锁)
*
* 3
* 普通方法没有加锁,访问时不需要占锁,直接访问即可
*
* 4
* 换成两个对象后,不是同一把锁了,情况立刻变化 ,一个对象一把锁
*
* 5-6
* 先邮件,再手机
* 因为用的都是类锁。
* 类锁,锁的是类,Class只有一个
* 三种synchronized锁的内容有一些差别:
* 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁—>实例对象本身
* 对于静态同步方法,锁的是当前类的cLass对象,如Phone.class唯一的一个模板
* 对于同步方法块,锁的是synchronized 括号内的对象
*
* 7-8
* 先是手机,再是邮件
* 因为,静态同步方法占的是类锁,
* 普通同步方法占的是对象锁,
* 是两把锁,不需要等待
*
* 具体实例对象this和唯一模板class,这 两把锁 是两个不同的对象,所以静态同步方法与普通同步方法之向是不会有竞态条件的
*
*
*
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture.runAsync(()->{
phone.sendEmail();
},threadPool);
//保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
CompletableFuture.runAsync(()->{
phone.sendSMS();
},threadPool);
threadPool.shutdown();
}
}
3.2、体现在3个地方
作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
作用于代码块,对括号里配置的对象加锁。
作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
3.3、从字节码角度分析synchronized实现
3.3.1、javap -c ***.class
文件反编译
javap -v ***.class
-v -verbose
输出附加信息(包括行号、本地变量表,反汇编等详细信息)
3.3.2、synchronized 同步代码块
实现使用的是monitorenter和monitorexit指令
public class LockSyncDemo {
Object object = new Object();
public void m1(){
synchronized (object){
System.out.println("----hello");
}
}
public static void main(String[] args) {
}
}
javap -c LockSyncDemo.class
存在两个。一个是正常结束执行,另一个是有异常才执行
一定是一个enter
两个exit
么
一般情况下。
极端
3.3.3、synchronized 普通同步方法
反编译
调用指令将会检查方法的ACC_SYNCHRONIZED
访问标志是否被设置
如果设置了,执行线程会将先持有monitor
锁,然后再执行方法,
最后在方法完成(无论是正常完成还是非正常完成)时释放monitor
3.3.4、synchronized静态同步方法
ACC_STATIC,ACC_SYNCHRONIZED
访问标志区分该方法是否静态同步方法
3.4、反编译synchronized锁的是什么
3.4.1、面试题
为什么任何一个对象都可以成为一把锁
3.4.2、什么是管程monitor
在HostSpot虚拟机中,monitor采用ObjectMonitor实现
上述C++源码解读
ObjectMonitor.java -> ObjectMonitor.cpp ->objectMonitor.hpp
每个对象天生都带着一个对象监视器
objectMonitor.hpp
EntryList
:阻塞队列,被阻塞的线程放入
M o n i t o r 的 本 质 是 依 赖 于 底 层 操 作 系 统 的 M u t e x L o c k 实 现 , \color{red}Monitor的本质是依赖于底层操作系统的Mutex Lock实现, Monitor的本质是依赖于底层操作系统的MutexLock实现,
操 作 系 统 实 现 线 程 之 间 的 切 换 需 要 从 用 户 态 到 内 核 态 的 转 换 , 成 本 非 常 高 。 \color{red}操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。 操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。
4、公平锁和非公平锁
4.1、ReentrantLock卖票
class Ticket {//资源类,模拟3个售票员卖完50张票
private int number = 50;
// ReentrantLock lock = new ReentrantLock(); //没参数默认非公平
ReentrantLock lock = new ReentrantLock(true); //设置为公平
public void sale() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "'\t还剩下∵:" + number);
}
} finally {
lock.unlock();
}
}
}
public class SaleTicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 55; i++) ticket.sale();
}, "a").start();
new Thread(() -> {
for (int i = 0; i < 55; i++) ticket.sale();
}, "b").start();
new Thread(() -> {
for (int i = 0; i < 55; i++) ticket.sale();
}, "c ").start();
}
}
4.2、何为
公平锁 | 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的 Lock lock = new ReentrantLock(true);//true 表示公平锁,先来先得 |
非公平锁 | 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁) Lock lock = new ReentrantLock(false);//false表示非公平锁,后来的也可能先获得锁 Lock lock = new ReentrantLock();//默认非公平锁 |
4.2.3、面试题
为什么会有公平锁/非公平锁的设计?为什么默认非公平
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,
但是从CPU
的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU
的时间片,尽量减少CPU
空闲状态时间。
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时 ,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
此刻再次获取同步状态的概率就变得非常大:
- 因为刚刚释放的线程正在使用
cpu
时间片,其他竞争的线程未必占有时间片
一个线程不用切换,当然没有开销
什么时候使用公平? 什么时候使用非公平
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;
否则那就用公平锁,大家公平使用。
4.3、预埋伏AQS
5、可重入锁(递归锁)
5.1、说明
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会**自动获取锁(**前提,锁对象得是同一个对象),不会因为之前已经
获取过还没释放而阻塞。
如果是1个有synchronized
修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以 Java
中ReentrantLock
和synchronized
都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
5.2、四个字分开解释
可:可以
重:再次
入:进入
锁:同步锁
进入什么:
- 进入同步域(即同步代码块/方法或显式锁锁定的代码)
一句话:
- 一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。
- 自己可以获取自己的内部锁
5.3、可重入锁种类
5.3.1、隐式锁(synchronized关键字使用的锁)默认是可重入锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单的来说就是:
在一个synchronized
修饰的方法或代码块的内部调用本类的其他synchronized
修饰的方法或代码块时,是永远可以得到锁的
同步块
同步方法
5.3.2、Synchronized的重入的实现机理
5.3.3、显示锁(即Lock)也有ReentrantLock这样的可重入锁
6、死锁及排查
6.1、是什么
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系
统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
6.1.1、原因
系统资源不足
进程运行推进的顺序不合适
资源分配不当
6.2、例子
public class DeadLockDemo {
public static void main(String[] args) {
final Object objectA = new Object();
final Object objectB = new Object();
new Thread(()->{
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"\t 自己持有A锁,试图获取B锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"\t 成功获取B锁");
}
}
});
new Thread(()->{
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"\t 自己持有B锁,试图获取A锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"\t 成功获取A锁");
}
}
});
}
}
6.3、如何排查死锁
纯命令
jps -l
类似于 ps ef | grep java
图形化
jvisualvm
7、总结
指针指向monitor
对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor
与之关联,当一个 monitor
被某个线程持
有后,它便处于锁定状态。在Java
虚拟机(HotSpot
)中,monitor
是由ObjectMonitor
实现的,其主要数据结构如下(位于HotSpot
虚拟机
源码ObjectMonitor.hpp
文件,C++
实现的)