JavaJUC面试

124 阅读17分钟

JavaJUC - 学习笔记

基础


并发 VS 并行

  • 并发:多个线程抢夺同一资源
  • 并行:多个线程同时执行各自的任务

进程 VS 线程 VS 管程

  • 进程:在系统中运行的一个应用程序就是一个进程,每一个进程都有自己的内存空间和系统资源
  • 线程:在一个进程内会有一个或多个线程
  • 管程:

线程的生命周期和6种状态

  • NEW: 初始状态,线程被创建出来但没有被调用 start() 。
  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
  • BLOCKED :阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。

  • 线程创建之后它将处于 NEW(新建)  状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行)  状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行)  状态。
  • 当线程执行 wait()方法之后,线程进入 WAITING(等待)  状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。
  • TIMED_WAITING(超时等待)  状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。
  • 当线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞)  状态。
  • 线程在执行完了 run()方法之后将会进入到 TERMINATED(终止)  状态。

wait/sleep 的区别

共同点 :两者都可以暂停线程的执行。

区别 :

  • sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
  • wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。为什么这样设计呢?

可重入锁

可重入锁又叫递归锁,给这个资源上过这个一个锁之后,还可以再次上锁,需要注意的是,上几次锁就需要释放几次 可重入性:就是一个线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次。(简单来说:A线程在某上下文中或得了某锁,当A线程想要在次获取该锁时,不会应为锁已经被自己占用,而需要先等到锁的释放)假使A线程即获得了锁,又在等待锁的释放,就会造成死锁。

注意:synchronizedreentrantlock 都是可重入锁

synchronized关键字

synchronized锁的范围、是否为同一把锁 - 码云案例

synchronized 关键字

如果业务中涉及到资源共享的问题的话加上synchronized锁,会从无锁状态变成偏向锁, 偏向锁:被同一个代码块访问这个资源,当你出现竞争的时候,这个偏向锁会有一个升级的过程 这个升级就是轻量级锁(CAS)(自旋锁)一个线程在这里用资源,另外一个线程循环等待,循环等待的时候也会限号CPU,循环到一定次数的时候(默认15次)超过后,会升级为重量级锁,并且这个锁只能升不能降

公平锁与非公平锁

  • 公平锁:按照顺序拿
  • 非公平锁:会有插队的情况

悲观锁与乐观锁

  • 悲观锁:效率相对来说较低,不支持并发操作
  • 乐观锁:支持并发操作(版本号操作)

读锁(共享锁)与 写锁(独占锁、排他锁)

一个资源可以被多个读的线程访问或者一个写的线程访问,但是

读读:可以多个线程共享 读写:只能存在一个线程 写写:只能存在一个线程

注意:在读操作进行时,不允许进行写操作。但是在写操作进行时,可以进行读操作

如何创建线程池?

为什么要用线程池?

池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

进阶


创建线程的四种方式

  • 继承Thread类
new Thread(){
    //重写run方法,设置线程任务
    @Override
    public void run() {
        // 需要执行的方法
    }
}.start();
  • 实现Runnable接口
new Thread(() -> {
    // 需要执行的方法
}).start();
  • 使用Callable和Future创建线程
FutureTask<Integer> futureTask = new FutureTask<>(()->{
    System.out.println(Thread.currentThread().getName()+" come in callable");
    return 1024;
});
new Thread(futureTask,"mary").start();
// 获取返回结果,get()方法会抛出异常:
// CancellationException - 如果计算被取消
// InterruptedException - 如果当前线程在等待时中断
// ExecutionException - 如果计算抛出异常
System.out.println(futureTask.get());
  • ThreadPoolExecutor线程池

Runnable和Callable的区别

  • 执行方法:Runnable内部调用的是run()方法,Callable为call()方法
  • 返回值:Runnable没有返回值,Callable有返回值
  • 抛出异常:Runnable不会抛异常,Callable会抛出异常

Future接口

一个异步任务接口,可以为主线程开一个分支任务 Future是Java5新加的一个接口,它提供了一种异步并行计算的功能。如果主线程需要执行一个很耗时的计算任务,就可以通过Future把任务放到异步线程中执行。主线程继续处理其他任务或先行结束,子啊通过Future计算结果。

乐观锁与悲观锁

  • 悲观锁:顾名思义,认为在使用数据的时候一定会有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会不会被其他线程修改

synchronizedlock 锁都属于悲观锁,为了保证资源只有一个线程可以访问

  • 乐观锁:认为在使用数据的时候不会有别的线程修改数据或资源,所以不会加锁。

一般常用Version版本号机制 和 CAS算法,

线程的中断机制

如何停止中断运行中的线程

  • 通过一个volatile变量实现
  • 通过AtomicBoolean原子布尔类型实现
  • 通过

当前线程中的中断标志位为true,是不是线程就立即停止

当对一个线程,调用 interrupt() 时,

  • ① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。

  • ② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。

interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。

image.png

LockSupport

image.png

JMM

JMM有哪些特征or它的三大特征是什么

JMM(Java内存模型)的三大特征:

  • 可见性
  • 原子性
  • 有序性

你知道什么是Java内存模型JMM么

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。

原则:JMM的关键技术点都是围绕多线程的原子性、可见性、和有序性展开的

原子性:一个原子事务要么完整执行,要么不执行

volatile

volatile修饰的变量有两大特点:

  • 可见性: 可见性主要体现在操作一个volatile变量时的写和读操作
    • 当写一个volatile变量时,JMM会把该线程对应本地内存中的变量值(副本)立即刷新回主内存中
    • 当读一个vilatile变量时,JMM会把该线程对应本地内存中的变量值(副本)设置为无效,重新回到主内存中读取最新的共享变量
  • 有序性
    • 排序要求:禁止指令重排

指令重排:是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序 不存在数据依赖关系,可以进行重排序

为什么volatile可以保证可见性和有序性:

内存屏障Memory技术 内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性

volatile使用场景:

  • 对于读多写少的场景: 为了保证原子性没必要读操作和写操作都加锁,只需要配合volatile关键字并且在写操作上加上 synchronized 关键字可以减少同步的开销

happens-before先行发生原则你有了解过么

CAS - compare and swap(乐观锁)

它是一条CPU并发原语,它的功能是 判断内存某个位置的值 是否为预期值,如果是则更改为新值,这个过程是原子的。CAS的出现主要是为了解决多线程并发情况下,数据的不一致问题

CAS底层的核心是UnSafe类,配合volatile关键字保证数据的可见性

谈谈你对UnSafe类的理解

Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,基于Unsafe类可以直接操作特定内存的数据

原子操作类 - java.util.concurrent.atomic CAS的具体实现

  • 基本类型原子类: AtomicBoolean AtomicInteger AtomicLong
  • 数组类型原子类: AtomicIntegerArray AtomicLongArray AtomicReferenceArray
  • 引用类型原子类: AtomicReference AtomicStampedReference AtomicMarkableReference

AtomicReference 自定义引用类型原子类,通过泛型的方式传入一个对象。 AtomicStampedReference 戳标记版本号引用类型原子类,由于版本号可以记录修改次数,可以解决修改多次的问题。 AtomicMarkableReference 带有标记位的引用类型原子类,将状态戳简化为 true|false,表示是否修改过

  • 对象的属性修改原子类: AtomicIntegerFieldUpdater AtomicLongFieldUpdater AtomicReferenceFieldUpdater
  • DoubleAccumulator DoubleAdder LongAccumulator LongAdder

《阿里巴巴Java开发手册》规定 如果是JDK8,推荐使用LongAdder对象,比AatomicLong性能更好(减少乐观锁的重试次数)

为什么AtomicInteger不加synchronized能实现原子性?答案:

  • 1.Unsafe是CAS的核心类,由于java方法无法直接访问底层系统,需要 通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法。注意,Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
  • 2.变量valueOffset表示该变量值在内存中偏移地址,因为Unsafe就是根据内存偏移地址获取数据的
  • 3.变量value用volatile修饰,保证了多线程之间的可见性

CAS缺点:

  • 1、底层使用了do() while() 可能会造成循环时间长,cpu开销较大
  • 2、导致ABA问题,所谓ABA就是线程A先从主内存读取到一个值为1,这个时候由于线程切换,线程A被挂起。这是线程B也读取到了数据1,并且把1改成了2,之后因为某种原因又改回成了1。当线程A再次执行时发现和预期值一致,然后又进行更改。虽然最终结果是一致的,但是A并不知道这个数据被修改过。

AtomicStampedReference的戳标记版本号解决ABA问题,就是版本号比较,类似于MySQL表中的版本号字段

ThreadLocal

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自已的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务D)与线程关联起来

1.1、ThreadLocal中ThreadLocalMap的数据结构和关系?

ThreadLocalMapThreadLocal的静态内部类,ThreadLocal实际上是在维护一个ThreadLocalMap,而ThreadLocalMap里面使用的是HahMap中的Entry对象来存储数据,并且把key包装成一个弱引用

当我们在给ThreadLocal赋值时,实际上是把ThreadLocalMap中的key设置为当前线程,再将value赋值;

1.2、ThreadLocal的key是弱引用,这是为什么?

当我们在New一个ThreadLoacal对象时,默认使用的是强引用,假如当我们的方法执行结束后,ThreadLocal对象就没有了,但是此时线程中的Entry中的Key还指向这个对象,若这个Key引用是强引用,就会导致key对应的value对象不能被gc回收,在使用线程池的情况下,因为线程会被复用,对象又无法被回收,会造成内存泄漏;

若这个key引用是弱引用,当gc执行时,会将弱引用对象回收置为null,会大概率减少内存泄漏问题;

当然这也引发了后续的问题...

1.2.1、JVM强引用、软引用、弱引用、虚引用

image.png

  • 强引用: 当内存不足时,JVM开始垃圾回收,对于强引用对象,就算出现了OOM也不会对该对象进行回收

  • 软引用: 可以豁免一些垃圾回收;当系统内存充足时,他 不会 被回收; 当系统内存不充足时 会 被回收

  • 弱引用: 比软引用的生存期更短;对于弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存

  • 虚引用:

1.3、ThreadLocall内存泄露问题你知道吗?

内存泄漏:不再使用的对象或者变量占用的内存不能被回收,就是内存泄漏

1.4、ThreadLocal中最后为什么要加remove方法?

Java对象内存布局和对象头

image.png

一个对象有哪些构成?(对象在堆内存中的存储布局or存储结构)

  • 对象头
    • 对象标记MarkWord
    • 类元信息(又叫类型指针)ClassPointer
  • 实例数据 instance Data
  • 对齐填充(padding) :保证8个字节的倍数

8字节的倍数方便JVM寻址,计算机底层的寻址一般都设计为8的倍数,具体原理见计组

对象头有多大

在64位系统中MarkWord占了8个字节(64bit-位),指针类型占了8个字节,一共是16个字节

  • hash值
  • GC分代年龄
  • 锁状态
  • ...

8bit(位) = 1字节 1024字节 = 1kb 1024kb = 1兆 1024兆 = 1G

Synchronized

image.png

关于锁《阿里巴巴Java开发手册》中强制规定:高并发时,同步调用应该去考量锁的的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁;

谈谈你对Synchronized得理解

一提到synchronized关键字就会想到重量级锁,实际上在Java5之前(也就是synchronized没有优化之前)确实是重量级锁,不管是单个线程还是多个线程来抢夺资源,都会频繁的出现从 用户态内核态的切换 ,这是很消耗系统资源的; 但是后来发现在实际场景中,大多数情况下都是由同一个线程拥有这个资源,很少发生竞争;这个时候(Java6之后)就引入了 偏向锁轻量级锁

Synchronized的锁升级过程

注意:在多线程情况下加了synchronized关键字,如果出现多个线程抢夺资源,在Java5之前(没有优化Synchronized之前)会频繁出现从 用户态内核态的切换 ,这是很消耗系统资源的

synchronized锁升级涉及到的几种锁:

  • 偏向锁: 如果加了 Synchronized ,却只有一个线程争抢锁资源的时候.将线程拥有者标识为当前线程.

    • 线程在第一次拥有锁的时候,会在被锁对象的对象头中记录下偏向线程的ID,之后每次出现竞争时,会检查锁的偏向线程ID当前线程ID 是否一致
    • 如果一致: 那么直接使用当前线程,就不需要每次进行用户态内核态的切换,那么偏向锁几乎没有额外的开销,会大大提高系统性能
    • 如果不一致: 表示发生了竞争,这个时候会用CAS来替换MarkWord中的线程ID为新线程ID
    • 竞争成功 表示之前的 线程不存在了,MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
    • 竞争失败 这个时候可能需要升级成轻量级锁,才能保证线程间公平竞争锁;
  • 轻量级锁(自旋锁): 一个或多个线程通过CAS去争抢锁,如果抢不到则一直自旋.

  • 重量级锁: 多个线程争抢锁,向内核申请锁资源,将未争抢成功的锁放到队列中直接阻塞.

为什么要有锁的升级过程? 在最开始的时候,其实就是无锁直接到重量级锁,但是重量级锁需要向内核申请额外的锁资源,这就涉及到用户态和内核态的转换,比较浪费资源,而且大多数情况下,其实还是一个线程去争抢锁,完全不需要重量级锁.

synchronized:实现原理,monitor对象什么时候生成的?

知道monitor的monitorenter和monitorexit这两个是怎么保证同步的吗,或者说,这两个操作计算机底层是如何执行的

synchronized得优化过程

偏向锁和轻量级锁有什么区别

AQS AbstractQueuedSynchronized

抽象的队列同步器,每一个请求线程都会被封装成一个Node节点,在队列中排队