Java 八股文:JUC 篇

5 - JUC 篇

5.1 线程基础

5.1.1 进程、线程与协程?
  1. 概念
    • 进程:程序执行的独立实例,拥有自己的内存空间。有多个状态,如运行、等待、就绪和终止。
    • 线程:进程中的一个实体,是CPU调度的最小单位。同一进程的线程共享进程资源,如内存。
    • 协程:轻量级线程,用于非阻塞的异步编程。可以在等待操作时挂起,节省资源,提高效率。
  2. 区别
    • 进程是正在运行程序的 实例,进程中包含了线程,每个线程执行不同的 任务
    • 不同的进程使用 不同 的内存空间,在当前进程下的所有线程可以 共享 内存空间。
    • 线程比进程更轻量,线程上下文切换成本一般上要比进程上下文切换低。
    • 协程比线程更轻量,适合处理大量并发的异步任务。
5.1.2 并行与并发的区别?

并发:

  • 并发是指多个任务在同一个时间段内进行,它们可能在不同的处理器上,也可能在同一个处理器上快速切换。
  • 在多线程环境中,即使只有一个CPU核心,线程也可以通过 时间片轮转 实现并发执行。

并行:

  • 并行是指多个任务同时进行,且每个任务都在不同的处理器上执行。
  • 并行需要多个处理器或核心,这样多个任务可以真正地 同时进行,而不是通过时间片轮转。

区别:

  • 并发更侧重于任务的重叠执行,而并行侧重于任务的真正同时执行。
  • 并发可能涉及I/O操作和CPU计算的交替,而并行则通常涉及多个计算任务的并发处理。
5.1.3 线程创建的方式有哪些?
  1. 直接使用 Thread 类

    Thread t = new Thread(() -> { log.debug("running"); }, "t1");
    
  2. 使用 Runnable 接口配合 Thread

    Runnable r = () -> { log.debug("running"); };
    Thread t = new Thread(r, "t2");
    
  3. 使用 Callable 接口配合 Thread

    FutureTask<String> ft = new FutureTask<>(() -> {
        log.debug("running");
        return "Hello from Callable";
    });
    Thread t = new Thread(ft, "t3");
    
  4. 使用 ThreadPoolExecutor 线程池

    ExecutorService e = Executors.newFixedThreadPool(4);
    e.submit(() -> { log.debug("running"); });
    
5.1.4 Runnable 和 Callable 有什么区别?
  • Runnable 接口 run 方法没有返回值。且异常只能在内部消化,不允许抛出异常。
  • Callable 接口 call 方法有返回值,需要 FutureTask 获取结果。且允许抛出异常。
5.1.5 线程的 run() 和 start() 有什么区别?
  • start():只能被调用一次,用来启动线程。
  • run():可以被调用多次,用来执行封装的代码。
5.1.6 线程的生命周期 / 六种状态

线程的六种状态包括新建、可运行、运行、阻塞、等待、计时等待和终止状态。

  1. 线程从 新建 状态开始,通过调用 start() 方法进入 可运行 状态,等待操作系统分配时间片。
  2. 获得时间片后,线程进入 运行 状态执行任务。
  3. 在运行过程中,线程可能因为等待锁、调用 wait() 方法或 sleep() 方法而进入 阻塞等待计时等待 状态。
  4. 当线程完成任务或因异常结束时,它将进入 终止 状态。
5.1.7 wait 和 sleep 方法的不同?
  • wait 的调用必须获取 对象锁,而 sleep 则不用。

  • wait 是 Object 的成员方法,执行后 会释放 对象锁(其他人可以用)。

  • sleep 是 Thread 的静态方法,执行后 不会释放 对象锁(其他人也用不了)。

5.1.8 新建三个线程,如何保证它们按顺序执行?

通过在创建线程的顺序中使用 join() 方法,可以确保线程按照创建的顺序执行。

Thread t1 = new Thread(Runnable1);
Thread t2 = new Thread(Runnable2);
Thread t3 = new Thread(Runnable3);

t1.start();       t1.join();      // 等待t1执行完成
t2.start();       t2.join();      // 等待t2执行完成
t3.start();
5.1.9 notify() 和 notifyAll() 有什么区别?
  • notifyAll:唤醒所有 wait 的线程
  • notify:随机唤醒一个 wait 线程
5.1.10 如何停止一个正在运行的线程?
  1. 使用 interrupt()线程的中断状态是一个标志,可以通过 Thread.interrupt() 设置,Thread.isInterrupted() 检查。
  2. 使用标志变量:设置一个共享的标志变量,线程定期检查这个变量,如果变量被设置,则退出运行。
  3. 使用 Future.cancel()如果线程是 ExecutorService 管理的,可以使用 Future.cancel() 方法来取消任务。

注意: 直接调用 stop() 方法来停止线程是不安全的,因为它可能导致资源泄露和不一致的状态。

5.2 线程安全

5.2.1 Synchronized 原理及锁升级?

1. Synchronized 底层原理

  1. Synchronized 对象锁采用 互斥 的方式,让同一时刻至多只有一个线程能持有对象锁。
  2. 它的底层由 monitor 实现,monitor 是 JVM 级别的对象( C++实现),内部有三个属性:
    • owner 关联的是当前获得锁的线程
    • entrylist 关联的是处于阻塞状态的线程队列
    • waitset 关联的是处于等待状态的线程队列

2. 锁升级

  1. 重量级锁:当多个线程竞争激烈时,synchronized 会退化为重量级锁。

    重量级锁通过操作系统的互斥锁来实现,涉及到用户态和内核态的切换,以及进程上下文切换,会带来较高的性能开销。

  2. 轻量级锁:在竞争不激烈的情况下,synchronized 会使用轻量级锁。

    轻量级锁主要通过CAS操作来尝试获取锁,这是一种无锁的非阻塞算法,它避免了较高的开销,因此在低竞争环境下性能较好。

  3. 偏向锁:当一个线程多次获取同一个锁时,JVM会将这个锁偏向这个线程,但在多线程竞争时会升级为轻量级锁或重量级锁。

    即在该线程再次获取锁时,会检查对象头的mark word中的线程ID,如果一致,则无需进行CAS操作。

5.2.2 谈一谈 JVM(Java Virtual Machine)?
  • JVM 是一个抽象计算机,它提供了一个运行时环境,允许 Java 字节码在任何平台上运行。
  • JVM 包括类加载器、运行时数据区和执行引擎。
  • 类加载器负责加载类文件,运行时数据区包括堆、栈、方法区等,执行引擎则负责执行字节码。
5.2.3 谈一谈 CAS(Compare-And-Swap)?
  • CAS 基于乐观锁的思想,是一种无锁的非阻塞算法,用于实现多线程下的原子操作。
  • 它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。
  • 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。
5.2.4 谈一谈 AQS(AbstractQueuedSynchronizer)?
  • AQS 是一个用于构建锁和其他同步器的框架,它提供了一套可扩展的原子操作。
  • AQS 使用一个整数状态来表示同步状态,通过内置的FIFO队列来管理线程。
  • AQS 提供了 acquirereleasetryAcquiretryRelease 等方法,允许自定义同步器实现自己的同步逻辑。
5.2.5 ReentrantLock 的实现原理?
  1. ReentrantLock 是一个可重入的互斥锁,它基于 AQS 实现。
  2. 它支持公平锁和非公平锁两种模式,公平锁会按照线程等待的顺序获取锁,非公平锁则可能造成饥饿。
  3. ReentrantLock 提供了 lock()unlock()tryLock() 等方法,允许更灵活的锁定操作。
5.2.6 Synchronized 和 Lock 的区别?
  1. 语法层面
  • Synchronized 是 关键字,源码在 jvm 中,用 c++ 语言实现。自动释放锁。
  • Lock 是 接口,源码由 jdk 提供,用 java 语言实现。手动释放锁。
  1. 功能层面
  • Lock 提供了 Synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量。
  • Lock 有适合不同场景的实现,如 ReentrantLockReentrantReadWriteLock(读写锁)。
  1. 性能层面
  • 在没有竞争时,Synchronized 的性能一般,但轻量级锁、偏向锁也不赖。
  • 在竞争激烈时,Lock 的性能更好。
5.2.7 死锁的必要条件?
  • 线程是程序执行的最小单位,锁用于同步线程间的资源访问。一个线程需要同时获取多把锁,就容易发生死锁。

  • 死锁的四个必要条件:互斥、占有和等待、不可剥夺、环路等待。

  • 如何避免:避免循环等待,确保资源有序分配等。

5.2.8 如何进行死锁诊断?
  1. jps:jdk 自带工具,检查进程的状态信息。
  2. jstack:jdk 自带工具,检查线程的堆栈信息,查看日志。
  3. jconsoleVisualVM:可视化工具,也可以检查死锁问题。
5.2.9 请谈谈你对 volatile 的理解?
  1. 保证线程间的可见性。用 volatile 修饰共享变量,能够防止编译器的优化,让一个线程对共享变量的修改对另一个线程可见。
  2. 禁止进行指令重排序。用 volatile 修饰共享变量,会加入不同的屏障,阻止其他读写操作越过屏障。
5.2.10 并发问题的根本原因?
  1. 并发问题的根本原因
    • 没有保证原子性:一个线程在 CPU 中操作不可暂停,也不可中断。解决:synchronizedlock
    • 没有保证可见性:让一个线程对共享变量的修改对另一个线程可见。解决:volatilesynchronizedlock
    • 没有保证有序性:因为处理器为了提高运行效率,对代码进行了优化而无序。解决:volatile
  2. volatile 的理解
    • 保证线程间的可见性。volatile 修饰共享变量能防止编译器的优化,让一个线程对共享变量的修改对另一个线程可见。
    • 禁止进行指令重排序。volatile 修饰共享变量会加入不同的屏障,阻止其他读写操作越过屏障。
5.2.11 Synchronized 用于静态方法与普通方法的区别?

1. 锁的对象

  • 对于普通方法,锁的对象是调用该方法的实例对象(即 this)。
  • 对于静态方法,锁的对象是当前类的 Class 对象。

2. 影响范围

  • 对于普通方法,锁的范围是实例对象级别的,即每个实例对象有自己的锁。
  • 对于静态方法,锁的范围是类级别的,即所有该类的实例对象共享同一个锁。

3. 性能影响

  • 静态方法的锁通常比普通方法的锁开销更小,因为它锁定的是类而不是实例对象。
5.2.12 乐观锁和悲观锁的区别?
  1. 乐观锁:

    • 乐观锁假设冲突很少发生,通常通过检测在操作过程中数据是否被其他线程修改来实现。

    • 它适用于 读多写少 的场景,常见的实现方式是使用版本号或时间戳。

  2. 悲观锁:

    • 悲观锁假设冲突经常发生,通常在操作数据时直接加锁。

    • 它适用于 写操作频繁 的场景,可以防止数据不一致,但可能导致线程阻塞和性能下降。

5.3 线程池

5.3.1 线程池创建及执行流程?
  1. ThreadPoolExecutor 创建线程池的核心参数:

    • corePoolSize:核心线程数目

    • maximumPoolSize:最大线程数目 = 核心线程数目 + 非核心线程数目

    • keepAliveTime:生存时间,生存时间内没有新任务,此线程资源会释放

    • unit:生存时间单位,如秒、毫秒等

    • workQueue:阻塞队列,当没有空闲核心线程时,新来任务会加入到此队列排队

    • threadFactory:线程工厂,可以定制线程对象的创建,例如设置线程名字

    • handler:拒绝策略,当所有线程都在忙,阻塞队列也放满时,会触发拒绝策略

  2. 线程池的执行原理:

5.3.2 线程池的 BlockingQueue?如何体现阻塞?
  • 有界队列是指队列有固定的大小,当队列满了之后,新的任务将不能被提交,除非队列中的任务被消费。

  • 无界队列则没有固定的大小限制,理论上可以无限增长,直到内存耗尽。

LinkedBlockingQueue ArrayBlockingQueue
默认无界,支持有界 强制有界
底层是链表 底层是数组
是懒惰的,创建节点的时候添加数据 提前初始化 Node 数组
入队会创建新 Node Node 需要提前创建好
两把锁(头尾) 一把锁
  1. 线程提交任务到队列时:如果已满,put()offer()方法会阻塞当前线程,直到队列中有空间可用。
  2. 线程从队列获取任务时:如果为空,take()poll()方法会阻塞当前线程,直到队列中有任务可用。
5.3.3 如何确定核心线程数?
  • 并发较高、任务执行时间短:

    • CPU 核数 + 1,减少线程上下文的切换。
  • 并发不高、任务执行时间长:

    • IO 密集型的任务:CPU核数 * 2 + 1
    • 计算密集型任务:CPU核数 + 1
5.3.4 线程池的种类有哪些?
  1. newFixedThreadPool:**定长 **的线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  2. newSingleThreadExecutor:**单线程 **化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按 FIFO 顺序执行。
  3. newCachedThreadPool可缓存 的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  4. newScheduledThreadPool:可执行 延迟任务 的线程池,支持定时及周期性任务执行。
5.3.5 为什么不建议用 Executors 创建线程池?

线程池不建议使用 Executors 创建,而是通过 ThreadPoolExecutor,可以规避资源耗尽的风险。分析源码:

  1. FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAXVALUE,可能堆积 大量请求,从而导致 OOM。
  2. CachedThreadPool:允许的创建线程数量为 Integer.MAXVALUE,可能创建 大量线程,从而导致 OOM。
5.3.6 线程池的拒绝策略有哪些?

线程池的拒绝策略就是当线程池忙不过来,任务太多,队列都满了,它怎么处理新来的任务。Java里有几个招儿:

  1. AbortPolicy:这个策略就是直接说“不”,如果新任务来了,它就直接抛出一个异常,告诉你“不行,我忙不过来了”。

  2. CallerRunsPolicy:这个策略比较温和,它会告诉提交任务的线程说“你等等,我先帮你跑这个任务”。

    但是,如果线程池已经关了,它就不管了。这个策略可能会让程序变慢,但是如果真的需要每个任务被处理,可以用这个。

  3. DiscardPolicy:这个策略就是“无视”,新任务来了,它看都不看,直接扔掉,不处理。

  4. DiscardOldestPolicy:这个策略有点“喜新厌旧”,它会看队列里哪个任务最老,然后把老任务扔掉,给新任务腾地方。

5.3.7 线程池的作用?
  1. 资源优化:线程池通过重用已经创建的线程来执行新的任务,避免了频繁创建和销毁线程的开销,从而优化了系统资源的使用。
  2. 提高性能:线程池减少线程创建和销毁的开销,因为创建和销毁是耗时的过程,线程池可以提高程序的响应速度和吞吐量。
  3. 控制并发:线程池限制程序中同时运行的线程数量,避免线程过多而系统过载和资源竞争,从而提高系统稳定性和响应能力。
  4. 提高线程管理的简便性:线程池提供了统一的线程管理机制,使得线程的创建、调度、执行和销毁更加方便和高效。
  5. 异常和任务管理:线程池集中处理线程执行中出现的异常,并且对任务进行调度和优先级管理,使得任务执行更加有序和可控。

5.4 使用场景

5.4.1 线程池使用场景?
  1. 资源管理: 线程池可以限制并发线程的数量,有效管理资源。
  2. 提高响应速度: 线程池可以减少线程创建和销毁的开销,快速响应任务请求。
  3. 提高线程的可管理性: 线程池提供了线程的统一管理,包括线程的创建、销毁和监控。
  4. 控制并发级别: 在需要控制并发任务数量的场景下,线程池可以限制同时运行的线程数量。
5.4.2 如何控制某个方法允许并发访问线程的数量?

在多线程中提供了一个工具类 Semaphore,信号量。在并发的情况下,可以控制方法的访问量:

  1. 创建 Semaphore 对象,可以给一个容量。
  2. acquire() 可以请求一个信号量,此时信号量个数 - 1。
  3. release() 可以释放一个信号量,此时信号量个数 + 1。
5.4.3 谈谈 ThreadLocal?
  1. ThreadLocal 实现了资源对象的线程隔离,让每个线程各用各的,避免争用 引发线程安全问题。
    • 就像每个线程都把物品放在私人储物柜里,别的线程看不到也拿不到,就不会因为大家都想用同一样东西而打架。
  2. ThreadLocal 实现了线程内的 资源共享。每个线程内有一个 ThreadLocalMap 类型的成员变量:
    • 调用 set 方法,以 ThreadLocal 作为 key,以资源对象作为 value 存入。
    • 调用 get 方法,以 ThreadLocal 作为 key,以资源对象作为 value 取出。
  3. 内存泄漏 问题:key 是弱引用,会被 GC 释放内存。而 value 是强引用,不会释放。建议主动调用 remove 释放。
    • 就像如果有人吃完了饭,但是忘记把自己的餐具放回原位,这些餐具就会一直占用空间,这就是所谓的“内存泄漏”。
    • ThreadLocal 里,如果你不再需要某个线程的资源,但是没有手动清理,那么这些资源就会一直占用内存。
    • 因为 key(储物柜号码)是弱引用,可能会被垃圾回收器清理掉,但是 value(物品)是强引用,不会被自动清理。
5.4.4 Java 中有哪些类型的锁?
  • 锁的应用场景

    • 对象锁(Synchronized)适用于保护单个对象实例的访问。

    • 类锁(Synchronized)适用于控制对类级别资源的访问,如静态变量。

    • 重入锁(ReentrantLock)适用于需要复杂同步控制的场景。

    • 读写锁(ReentrantReadWriteLock)适用于读操作频繁且写操作较少的场景。

    • 自旋锁(Spin Lock)适用于锁持有时间短且竞争激烈度低的场景。

  • 锁的性能比较

    • Synchronized经过 JVM 优化,性能较高,但可能导致线程阻塞。

    • ReentrantLock提供更多功能,但性能略低于Synchronized

    • ReentrantReadWriteLock适用于读多写少,提高读操作并发性。

    • SpinLock适用于锁保护时间短,避免上下文切换开销,但可能导致CPU消耗。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容