面试之JUC和JVM,通俗易懂。

Java架构师社区

共 12980字,需浏览 26分钟

 ·

2021-08-07 03:03

关注我们,设为星标,每天7:30不见不散,架构路上与您共享 

回复"架构师"获取资源

垃圾回收机制算法

  • 复制算法:用于年轻代

  • 标记清除算法:用于老年代

  • 标记整理算法:用户老年代

  • 分代收集算法:年轻代特点是区域相对老年代较小,对像存活率低,使用复制算法,老年代的特   点是区域较大,对像存活率高。标记清除和标记整理混合使用。

个对象怎么判断是垃圾被回收

  • 引用计数法:是通过判断对象的引用数量来决定对象是否可以被回收,很难处理循     环引用,相互引用的两个对象则无法释放。

  • 可达性分析:这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作 为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。                                  

JVM 架构,回收机制

方法区:存储已被虚拟机加载的类元数据信息(元空间)

堆:存放对象实例,几乎所有的对象实例都在这里分配内存

虚拟机栈:虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会  同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、   方法出口等信息

程序计数器:当前线程所执行的字节码的行号指示器

本地方法栈:本地方法栈则是为虚拟机使用到的Native方法服务。

volatile 关键字

Volatile是Java虚拟机提供的轻量级的同步机制(三大特性)

  • 保证可见性(及时通知):变量被volatile修饰之后,当该变量被修改之后使用到该变量的地方都会被感知到

  • 不保证原子性:原子性是指不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要具体完成,要么同时成功,要么同时失败。

  • 禁止指令重排:计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排。但是处理器在进行重排时候,必须考虑到指令之间的数据依赖性。

说一下synchronized和lock的区别

1)synchronized属于JVM层面,属于java的关键字

monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象 只能在同步块或者方法中才能调用 wait/ notify等方法)

Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁

2)使用方法:

synchronized:不需要用户去手动释放锁,当synchronized代码执行后,系统会自动让线程释放对锁的占用

ReentrantLock:则需要用户去手动释放锁,若没有主动释放锁,就有可能出现死锁的现象,需要lock() 和 unlock() 配置try catch语句来完成

3)等待是否中断

synchronized:不可中断,除非抛出异常或者正常运行完成

ReentrantLock:可中断,可以设置超时方法

设置超时方法,trylock(long timeout, TimeUnit unit)

lockInterrupible() 放代码块中,调用interrupt() 方法可以中断

4)加锁是否公平

synchronized:非公平锁

ReentrantLock:默认非公平锁,构造函数可以传递boolean值,true为公平锁,false为非公平锁

5)锁绑定多个条件Condition

synchronized:没有,要么随机,要么全部唤醒

ReentrantLock:用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized那样,要么随机,要么全部唤醒

悲观锁和乐观锁

乐观锁:顾名思义,就是十分乐观,它总是认为不会出现问题,无论干什么都不去 上锁,如果出现了问题,再次更新值测试,这里使用了version字段。

也就是每次更新的时候同时维护一个version字段。

可以使用CAS(compare and swap)实现,CAS是由CPU硬件实现,所以执行相当快.CAS 有三个操作参数:内存地址,期望值,要修改的新值,当期望值和内存当中的值进 行比较不相等的时候,表示内存中的值已经被别线程改动过,这时候失败返回,当 相等的时候,将内存中的值改为新的值,并返回成功。 

悲观锁:顾名思义,就是十分悲观,它总是认为什么时候都会出现问题,无论什么 操作都会上锁,再次操作,synchronized就是一个典型的悲观锁

线程池的创建

Executors工具类和ThreadPoolExecutor方式创建推荐使用ThreadPoolExecutor方式创建,其中有七个参数

  • corePoolSize:线程池中的常驻核心线程数

  • maximumPoolSize:线程池中能够容纳同时 执行的最大线程数,此值必须大于等于1

  • keepAliveTime:多余的空闲线程的存活时间 当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到 只剩下corePoolSize个线程为止

  • unit:keepAliveTime的单位

  • workQueue:任务队列,被提交但尚未被执行的任务

  • threadFactory:表示生成线程池中工作线程的线程工厂, 用于创建线程,一般默认的即可

  • handler:拒绝策略,表示当队列满了,并且工作线程大于 等于线程池的最大线程数(maximumPoolSize)时,如何来拒绝 请求执行的runnable的策略

cpu占用满了如何排查

  • 先用top命令,找到cpu占用最高的进程 PID 

  • 再用ps -mp pid -o THREAD,tid,time   

  • 查询进程中,那个线程的cpu占用   率高记住TID

  • jstack 29099 >> xxx.log   打印出该进程下线程日志

  • sz xxx.log 将日志文件下载到本地

写一个死锁

/**
 * 资源类
 */
class Lock implements Runnable {
    private String lockA;
    private String lockB;
    public Lock(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }
    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "用了" + lockA + "想获取" + lockB);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "用了" + lockB + "想获取" + lockA);
            }
        }
    }
}
public class DeadLockDemo01 {
    /**
     * 线程 操作 资源类
     */
    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";

        new Thread(new Lock(lockA, lockB), "T1").start();
        new Thread(new Lock(lockB, lockA), "T2").start();
    }
}

线程数是怎么设置的

使用ThreadPoolExecutor 第一个参数就是核心线程数

说一下创建线程的方式,以及线程的状态吧

传统的是继承thread类和实现runnable接口

java5以后又有实现callable接口和java的线程池获得

线程状态

①创建状态

②就绪状态

③运行状态

④阻塞状态

⑤死亡状态

线程池的运行原理

  • 在创建了线程池后,线程池中的线程数为零。

  • 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:

  • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

  • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

  • 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

  • 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

  • 当一个线程完成任务时,它会从队列中取下一个任务来执行。

  • 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:

  • 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。

  • 所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

介绍一下jvm

  • 方法区:存储已被虚拟机加载的类元数据信息(元空间)

  • 堆:存放对象实例,几乎所有的对象实例都在这里分配内存

  • 虚拟机栈:虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会 同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法 出口等信息

  • 程序计数器:当前线程所执行的字节码的行号指示器

  • 本地方法栈:本地方法栈则是为虚拟机使用到的Native方法服务。

写一个线程安全的单例模式()

// 懒汉式单例模式会有线程安全问题

public void SingleTonDemo{
// 私有化构造器

private SingleTonDemo(){};

 

// 私有属性

private static Volatile SingleTonDemo std = null;  

 

// 提供公有方法

public SingleTonDemo getStd(){
If(std == null){
Synchronized(SingleTonDemo.class){
If(std == null){
Std = new SingleTonDeno();

}

}

}

return std;

}

}

谈谈你对juc的理解

  1. 是Java5.0提供的一个java.util.concurrent包,在包中提供了一些并发编程中很常用的工具类。

  2. 创建线程有四种方式:

继承Thread类

实现Runnable接口

实现Callable接口 + FutureTask类

线程池

allable接口与runnable接口的区别?

相同点:

都是接口,都可以编写多线程程序,都采用Thread.start()启动线程

不同点:

具体方法不同:一个是run,一个是call

Runnable没有返回值;Callable可以返回执行结果,是个泛型

Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。

synchronized和lock锁的区别

Synchronized是java的一个关键字

Lock 属于api层面

Synchronized可以自动解锁

Lock需要手动解锁

Synchronized中途不可以中断

Lock可以中断

Synchrozied属于公平锁

Lock既可以是公平锁又可以是非公平锁根据传入的参数

唤醒机制:synchronized只能全部唤醒

      Lock可以根据条件唤醒不同的线程

谈谈你对volatile的理解

Volatile是一个轻量级的同步机制,它有三个属性:

  1. 保证可见性

  2. 不保证原子性

  3. 禁止指令重排

说说类加载器类型和JVM内存结构

类加载器:启动类加载器(Bootstrap)C++、扩展类加载器(Extension)Java、应用程序类加载器(AppClassLoader)Java、用户自定义加载器 Java.lang.ClassLoader的子类,用户可以定制类的加载方式

  1. 双亲委派模型。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自 己去尝试加载这个类, 而是把请求委托给父加载器去完成,依次向上。

  • 栈:虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息

  • 堆:存放对象实例,几乎所有的对象实例都在这里分配内存

  • 方法区:存储已被虚拟机加载的类元数据信息(元空间)

  • 程序计数器:当前线程所执行的字节码的行号指示器

  • 本地方法栈:本地方法栈则是为虚拟机使用到的Native方法服务。

  • 执行引擎

  • 本地方法接口:执行本地方法库

thread.sleep()和wait()区别?还问了wait()锁的问题?

  1. 所属类不同:sleep属于Thread类,wait属于Object类

  2. 是否释放锁:sleep不释放线程所拥有的监视器资源,而wait会把监视器资源释放

  3. 用法不同:wait()方法通常用于线程间的交互和通信,sleep通常用于暂停执行

  4. 用途不同:wait()方法被调用后,如果没有设置等待时间,线程不会自动苏醒,需要别的线程调用共享变量的notify()或notifyAll()方法。sleep()方法在等待时间到了之后,会自动苏醒

sleep()睡眠时,保持对象锁,仍然占有该锁;其他线程无法访问
而wait()睡眠时,释放对象锁。其他线程可以访问

valitile关键字

Java多线程中的轻量的同步机制有三个属性

  • 保证可见性

  • 不保证原子性

  • 禁止指令重排

JMM是Java内存模型,也就是Java Memory Model,简称JMM,本身是一种抽象的概念,实际上并不存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式

JMM关于同步的规定:

线程解锁前,必须把共享变量的值刷新回主内存

线程解锁前,必须读取主内存的最新值,到自己的工作内存

加锁和解锁是同一把锁

线程的创建中,runnable和callable区别?

相同点:都是接口,都可以编写多线程程序,都采用Thread.start()启动线程

不同点:

  • 具体方法不同:一个是run,一个是call

  • Runnable没有返回值;Callable可以返回执行结果,是个泛型

  • Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往    上继续抛

  • 它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。

分布式锁如何实现?

分布式锁的三种实现方式:mysql redis(性能最高) zk(安全性最高)

特征:

1.独占排他

2.可重入性(可选)

3.防止死锁的发生

4.防误删

5.自动续期

6.原子性:加锁 解锁

7.redlock算法

实现:

setnx实现独占排他

防止死锁发生 过期时间 set key value ex 3000 nx

lua脚本 或者 set key value ex 3000 nx  保证原子性

为了做到防误删 给锁添加过期时间

监控子线程做到自动续期

总结:

加锁:set key value ex 3000 nx 实现独占排他 设置过期时间,防止死锁发生,并保证原子性

解锁:lua脚本解锁,防止误删并保证原子性

监控子线程做到自动续期

重试:获取锁失败的线程,重试

可重入锁:使用了lua脚本,hash数据结构,每重入一次value+1,每释放一次value-1,直到value=0时,删除锁

redisson分布锁框架:

ReentrantLock可重入锁

FireLock公平锁

RedLock红锁:大部分实例获取到锁

ReadWriteLock读写锁

CountDownLatch闭锁

Semaphore分布式信号量

怎么做到自动续期:看门狗子线程

怎么防止集群情况下锁失效:RedLock

 

AOP封装缓存和分布式锁:

注解 + 环绕通知

IOC原理:反转控制 大工厂 + 配置文件 + 反射

4种初始化方式:无参构造器 静态工厂 实例化工程  factoryBean

DI依赖注入:依赖于IOC

   2种方式:setter注入 构造方法

AOP原理:动态代理(JDK代理 CGLIB代理)

@Aspect @Before @Around

切入点表达式:execution(* com.atguigu.gmall.pms.service.*.*(..)) annotation(注解的全路径)

JoinPoint

joinPoint.getArgs()

joinPoint.getTarget().getClass()

(MethodSignature)joinPoint.getSignature

线程池创建方式,和前两个参数

线程池创建的两种方式:Executors工具类和ThreadPoolExecutor类

  • 核心线程数

  • 存活时间

  • 时间单位

  • 最大线程数

  • 阻塞队列

  • 线程工厂

  • 拒绝策略

 mq怎么保证消息不丢失

  • 提供者消息确认 

  • 消费者消息确认(手动确认和自动确认,springAMQP有三种:NONE AUTO MANUAL)

  • 消息持久化(交换机、队列、消息)

线程工具类介绍一下

Executors工具类线程池创建的方法有:固定数的,单一的,可变的。

线程池不允许使用Executors去创建,而是通过ThreadToolExecutors的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors返回的线程池对象弊端如下:

  • FixedThreadPool和SingleThreadPool:

  • 运行的请求队列长度为:Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM

  • CacheThreadPool和ScheduledThreadPool

  • 运行的请求队列长度为:Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OO

线程的状态

创建 就绪 运行 阻塞 死亡

死锁是怎么发生的,怎么解决

死锁的四个必要条件

  • 互斥

解决方法:把互斥的共享资源封装成可同时访问

  • 占有且等待

解决方法:进程请求资源时,要求它不占有任何其它资源,也就是它必须一次性申请到所有的资源,这种方式会导致资源效率低。

  • 非抢占式

解决方法:如果进程不能立即分配资源,要求它不占有任何其他资源,也就是只能够同时获得所有需要资源时,才执行分配操作

  • 循环等待

解决方法:对资源进行排序,要求进程按顺序请求资源。

如何保证线程的同步?

使用synchronized ReentrandLock保证线程同步

Java锁的基本状态

无锁 偏向锁 轻量级锁 重量级锁

一个对象加锁的状态?

锁是存在哪里的呢?

锁存在Java的对象头中的Mark Work。Mark Work默认不仅存放着锁标志位,还存放对象hashCode等信息。运行时,会根据锁的状态,修改Mark Work的存储内容。如果对象是数组类型,则虚拟机用3个字宽存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。

字宽(Word): 内存大小的单位概念, 对于 32 位处理器 1 Word = 4 Bytes, 64 位处理器 1 Word = 8 Bytes

每一个 Java 对象都至少占用 2 个字宽的内存(数组类型占用3个字宽)。

第一个字宽也被称为对象头Mark Word。对象头包含了多种不同的信息, 其中就包含对象锁相关的信息。

第二个字宽是指向定义该对象类信息(class metadata)的指针

四种状态

锁有四种状态:无锁状态、偏向锁、轻量级锁、重量级锁

随着锁的竞争,锁的状态会从偏向锁到轻量级锁,再到重量级锁。而且锁的状态只有升级,没有降级。也就是只有偏向锁->轻量级锁->重量级锁,没有重量级锁->轻量级锁->偏向锁。

锁状态的改变是根据竞争激烈程度进行的,在几乎无竞争的条件下,会使用偏向锁,在轻度竞争的条件下,会由偏向锁升级为轻量级锁, 在重度竞争的情况下,会升级到重量级锁。

你为什么说synchronized是重量级锁?

操作系统维护锁的状态使当前线程挂起外,只要是synchronized,一有竞争也会引起阻塞,阻塞和唤醒操作又涉及到了上下文操作,大量消耗CPU,降低性能。

重量级锁是需要依靠操作系统来实现互斥锁的,这导致大量上下文切换,消耗大量CPU,影响性能。

公平锁和非公平锁的区别?

  • 公平锁

是指多个线程按照申请锁的顺序来获取锁,类似于排队买饭,先来后到,先来先服务,就是公平的,也就是队列

  • 非公平锁

是指多个线程获取锁的顺序,并不是按照申请锁的顺序,有可能申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的线程(也就是某个线程一直得不到锁)

  • 如何创建

并发包中ReentrantLock的创建可以指定析构函数的boolean类型来得到公平锁或者非公平锁,默认是非公平锁

/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(true);

  • 两者区别

公平锁:就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列中的第一个,就占用锁,否者就会加入到等待队列中,以后安装FIFO的规则从队列中取到自己

非公平锁:非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。题外话

Java ReenttrantLock通过构造函数指定该锁是否公平,默认是非公平锁,因为非公平锁的优点在于吞吐量比公平锁大,对于synchronized而言,也是一种非公平锁

JUC下的工具类有哪些

CountDownLatch

概念

让一些线程阻塞直到另一些线程完成一系列操作才被唤醒

CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程就会被阻塞。其它线程调用CountDown方法会将计数器减1(调用CountDown方法的线程不会被阻塞),当计数器的值变成零时,因调用await方法被阻塞的线程会被唤醒,继续执行

概念

和CountDownLatch相反,需要集齐七颗龙珠,召唤神龙。也就是做加法,开始是0,加到某个值的时候就执行

CyclicBarrier的字面意思就是可循环(cyclic)使用的屏障(Barrier)。它要求做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await方法

概念

信号量主要用于两个目的

一个是用于共享资源的互斥使用

另一个用于并发线程数的控制

git的合并冲突

冲突是指当你在提交或者更新代码时被合并的文件与当前文件不一致

常见冲突的生产场景如下

更新代码

提交代码

多个分支代码合并到一个分支时

多个分支向同一个远端分支推送代码时

git的合并中产生冲突的具体情况:
<1>两个开发者(分支中)修改了同一个文件(不管什么地方)
<2>两个开发者(分支中)修改了同一个文件的名称
  注意:两个分支中分别修改了不同文件中的部分,不会产生冲突,可以直接将两部分合并。

  总结:上面各种情况的本质都是,当前文件与合并文件不一致,因此不论哪种情况其解决冲突的方法是一样的。

最后:代码冲突之后,和为贵!!!

分布式锁用的是什么?

MySQL redis zk三种方式

线程中sleep()、 wait()、 join()、 yield()的区别

  • sleep()方法

sleep()方法是让当前运行这一句的代码休眠指定的一段时间,在休眠时间里,线程不会获取CPU的执行权,如果当前线程持有了对象锁,是不会释放对象锁的,过了休眠时间线程自动转为可运行状态。

  • wait()方法

wait()方法是让当前线程等待一段时间,这段时间里,线程将一直处于阻塞状态,直到被notify()或者notifyAll()方法唤醒,如果线程持有对象锁,会释放对象锁,wait()和notify()方法都是object对象的方法,而不是线程独有的方法,另外,wait()和notify()方法运行时必须持有锁(即代码要是同步的),否则会报错,wait()会释放锁也是因为notify()要得到锁,但是notify()方法并不会释放锁,所以一般把notify()放在代码最后。

  • yield()方法

yield()方法是线程完成自己的任务时,自己回到可运行状态,参与争夺CPU执行权,且线程不会释放对象锁。

  • join()方法

在一个线程A中运行了线程B的join()方法,则线程A必须等到线程B执行完后才能开始执行,可以用于保证线程的执行先后顺序。

  • interrupt()

interrupt()方法是为线程设立一个中断标志,相当于一个通知,但是线程是否中断,是有线程自己决定的,也就是说线程调用interrpt()方法不代表着线程一定会中断,如果线程中运行了sleep()方法,并抛出InterruptedException,那么当前线程的中断状态会被重置。

  • isInterrupted()

isInterrupted()方法是判断调用该方法的线程是否处于中断状态,但是不会重置线程状态。


文章来源:https://blog.csdn.net/Ding9610/article/details/108432903




到此文章就结束了。如果今天的文章对你在进阶架构师的路上有新的启发和进步,欢迎转发给更多人。欢迎加入架构师社区技术交流群,众多大咖带你进阶架构师,在后台回复“加群”即可入群。







这些年小编给你分享过的干货

1.第七期打卡送书5本(5月1日-6月1日)

2.ERP系统,自带进销存+财务+生产功能,拿来即用

3.带工作流的SpringBoot后台管理项目快速开发解决方案
4.最好的OA系统,拿来即用,非常方便

5.SpringBoot+Vue完整的外卖系统,手机端和后台管理,附源码!

转发在看就是最大的支持❤️

浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报