2.锁类型

1、面试题复盘

image-20220912191036198

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

存在两个。一个是正常结束执行,另一个是有异常才执行

image-20220912205806523


一定是一个enter两个exit

一般情况下。

极端

image-20220912210202903

3.3.3、synchronized 普通同步方法

反编译

image-20220912210415004

调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置

如果设置了,执行线程会将先持有monitor锁,然后再执行方法,

最后在方法完成(无论是正常完成还是非正常完成)时释放monitor

3.3.4、synchronized静态同步方法

ACC_STATIC,ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法

3.4、反编译synchronized锁的是什么

3.4.1、面试题

为什么任何一个对象都可以成为一把锁

3.4.2、什么是管程monitor

image-20220912211314588

image-20220912211326246


在HostSpot虚拟机中,monitor采用ObjectMonitor实现

上述C++源码解读

ObjectMonitor.java -> ObjectMonitor.cpp ->objectMonitor.hpp

每个对象天生都带着一个对象监视器

objectMonitor.hpp

image-20220912213011109

image-20220912213056055

EntryList:阻塞队列,被阻塞的线程放入

M o n i t o r 的 本 质 是 依 赖 于 底 层 操 作 系 统 的 M u t e x L o c k 实 现 , \color{red}Monitor的本质是依赖于底层操作系统的Mutex Lock实现, MonitorMutexLock

操 作 系 统 实 现 线 程 之 间 的 切 换 需 要 从 用 户 态 到 内 核 态 的 转 换 , 成 本 非 常 高 。 \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

image-20220912222803916

image-20220912222829941

5、可重入锁(递归锁)

5.1、说明

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会**自动获取锁(**前提,锁对象得是同一个对象),不会因为之前已经

获取过还没释放而阻塞。

如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。

所以 JavaReentrantLocksynchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

5.2、四个字分开解释

可:可以

重:再次

入:进入

锁:同步锁

进入什么:

  • 进入同步域(即同步代码块/方法或显式锁锁定的代码)

一句话:

  • 一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。
  • 自己可以获取自己的内部锁

5.3、可重入锁种类

5.3.1、隐式锁(synchronized关键字使用的锁)默认是可重入锁

指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。

简单的来说就是:

在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的


同步块

同步方法

5.3.2、Synchronized的重入的实现机理

image-20220913195618348


image-20220912213011109

image-20220912213056055

5.3.3、显示锁(即Lock)也有ReentrantLock这样的可重入锁

6、死锁及排查

6.1、是什么

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系

统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

image-20220913202226743

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

image-20220913202943290

image-20220913203007130

图形化

jvisualvm

7、总结

指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个 monitor被某个线程持

有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机

源码ObjectMonitor.hpp文件,C++实现的)

image-20220913204048983

image-20220913204333414

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值