八股文---JUC(1)

1.线程和进程的区别?

进程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。

当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。

线程

一个进程之内可以分为一到多个线程。

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行

Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。在 windows 中进程是不活动的,只是作为线程的容器

区别

  • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
  • 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)

AI回答

线程与进程的区别可以从多个维度进行分析,主要包括资源管理、执行单位、独立性、资源消耗、通信机制及对系统的影响等方面。以下是对这些区别的详细总结:

  1. 资源管理
    • 进程:拥有独立的内存空间,包括堆、栈和全局变量,资源隔离性强。
    • 线程:共享进程的内存空间,资源消耗较低。
  1. 执行单位
    • 进程:执行应用程序,可以包含多个线程。
    • 线程:在进程内执行任务,一个进程可有多个线程。
  1. 独立性
    • 进程:独立运行,崩溃不会影响其他进程。
    • 线程:共享进程资源,崩溃可能导致整个进程终止。
  1. 资源消耗
    • 进程:创建和切换开销大,资源消耗多。
    • 线程:创建和切换开销小,资源消耗少。
  1. 通信机制
    • 进程:使用管道、共享内存、消息队列等复杂方式。
    • 线程:使用锁、信号量等同步工具,通信简便。
  1. 调度与管理
    • 进程:由操作系统调度,管理独立。
    • 线程:由操作系统调度,但在应用层面需管理协调。
  1. 适用场景
    • 进程:适合处理独立任务,如运行多个程序。
    • 线程:适合在同一任务中执行多个子任务,提升效率。
      总结来说,进程是较大的独立资源单位,适合独立任务;而线程是轻量级的执行单位,适合在同一进程中处理多任务,提升效率。理解这些区别有助于在编程和系统设计中合理选择使用进程和线程,优化程序性能。

⭐️2.并行与并发的区别

并行

并行(parallel)是同一时间动手做(doing)多件事情的能力

并发

并发(concurrent)是同一时间应对(dealing with)多件事情的能力

回答

并行(Parallelism)和并发(Concurrency)的核心区别在于任务执行的方式:

  • 并行是物理上的同时执行,依赖多核/多线程硬件(如同时处理两个任务的CPU核心)。
  • 并发是逻辑上的交替执行,通过任务切换模拟“同时”(如单核CPU分时处理多个任务)。

通俗比喻:

并行是多个收银台同时结账(真正的同时工作)。

并发是一个收银台交替处理多个顾客的扫码和支付(快速切换,看似同时)。

⭐️⭐️3.线程创建的方式

1.继承Thread类


class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running");
    }
}
// 启动线程
new MyThread().start();

  • 优点:简单直接,适合快速实现。
  • 缺点:Java单继承限制,无法继承其他类。

2.实现runnable

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable running");
    }
}
// 启动线程  
new Thread(new MyRunnable()).start();
  • 优点:解耦任务与线程,可复用类,支持Lambda简化(new Thread(() -> {...}))。
  • 缺点:无返回值,需通过共享变量或回调获取结果

3.实现Callable接口

class MyCallable implements Callable<String> {
    @Override
    public String call() {
        return "Task result";
    }
}
// 启动线程  
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());  
new Thread(futureTask).start();
String result = futureTask.get(); // 阻塞获取结果
  • 优点:支持返回值,可捕获异常,适合需要结果的异步任务。
  • 缺点:代码稍复杂,需处理Future的阻塞问题。

4.线程池创建线程

ExecutorService executor = Executors.newFixedThreadPool(4);  
executor.submit(() -> {
    System.out.println("Task from thread pool");
});  
executor.shutdown();

优点:资源复用(避免频繁创建/销毁线程)、支持任务队列、可管控线程数量。

缺点:需合理配置参数(如核心线程数、拒绝策略)

回答

Java中线程创建主要有四种方式:继承Thread类、实现Runnable或Callable接口,以及使用线程池。

  1. 继承Thread类简单但耦合性高;
  2. Runnable解耦任务逻辑,支持Lambda;
  3. Callable支持返回值和异常处理;
  4. 线程池是生产环境首选,通过复用线程提升性能并避免资源耗尽。

实际开发中,推荐使用Runnable+线程池,兼顾灵活性和资源管理。

.

4.runnable 和 callable 有什么区别

1.Runable接口run方法没有返回值

2.Callable接口call方法有返回值,是个泛型,和Future,FutureTask配合可以异步获取执行的结果

3.Callable接口的call方法可以抛出异常,而Runable接口的run方法只能内部处理

回答

Runnable和Callable都是定义任务的接口,但Callable支持返回值和抛出异常通常与Future结合实现异步结果获取

如果是无需返回值的任务(如日志记录),用Runnable更简单;

若需要异步计算并获取结果(如调用外部API),必须用Callable。

实际开发中,推荐优先使用Callable+线程池,通过Future管理任务生命周期,避免资源泄漏。

5.线程的 run()和 start()有什么区别?

run方法就是普通方法可以随便调用

start方法是用来启动线程只能调用一次

回答

start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。

run(): 封装了要被线程执行的代码,可以被调用多次

⭐⭐6.线程包括哪些状态,状态之间是如何变化

线程状态

关键状态转换

1.NEW-->RUNABLE

Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // NEW
thread.start();
System.out.println(thread.getState()); // RUNNABLE

2.RUNABLE-->BLOCKED

synchronized (lock) {
    // 线程A持有锁时,线程B尝试进入同步块 → BLOCKED
}

3.RUNABLE-->WAITING

synchronized (lock) {
    lock.wait(); // 释放锁并进入WAITING状态
}

4.RUNABLE-->TIMED_WAITING

Thread.sleep(1000); // 进入TIMED_WAITING, 1秒后恢复

5.WAITING/TIME_WAITING-->RUNABLE

synchronized (lock) {
    lock.notify(); // 唤醒等待的线程
}

6.RUNABLE-->TERMINATED

thread.start();
thread.join(); // 等待线程结束
System.out.println(thread.getState()); // TERMINATED

回答

在Java中,线程共有6种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING和TERMINATED。

  • NEW是线程刚创建未启动的状态;
  • RUNNABLE表示线程正在运行或就绪等待CPU时间片;
  • BLOCKED是线程因竞争锁而阻塞;
  • WAITING和TIMED_WAITING是线程主动等待或被挂起,区别在于后者有超时机制;
  • TERMINATED是线程执行结束后的终止状态。

状态转换的关键触发点包括:

  • start()使线程从NEW进入RUNNABLE;
  • 竞争锁失败进入BLOCKED,锁释放后回到RUNNABLE;
  • 调用wait()或join()进入WAITING,需notify()唤醒;
  • sleep()或带超时的wait()进入TIMED_WAITING,超时后自动恢复;
  • 线程执行完毕或异常终止进入TERMINATE

7.sleep 和 wait的区别是什么?

回答

共同点

  • wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

不同点

  • 方法归属不同
    • sleep(long) 是 Thread 的静态方法
    • 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
  • 醒来时机不同
    • 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
    • wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
    • 它们都可以被打断唤醒
  • 锁特性不同(重点)
    • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
    • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
    • 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)

8.如何停止一个正在运行的线程
 

1. 使用标志位(推荐)

通过共享的volatile变量控制线程退出,确保线程能安全完成收尾工作。

class MyThread extends Thread {
    private volatile boolean stopped = false; // 必须用volatile保证可见性

    @Override
    public void run() {
        while (!stopped) {
            // 执行任务
            System.out.println("Running...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 恢复中断状态
                break;
            }
        }
        System.out.println("Thread stopped safely");
    }

    public void stopThread() {
        stopped = true;
        this.interrupt(); // 双保险:唤醒可能处于sleep/wait的线程
    }
}

// 使用示例
MyThread thread = new MyThread();
thread.start();
Thread.sleep(3000);
thread.stopThread();

优点:安全可控,允许线程清理资源。

适用场景:循环执行的线程任务


2. 调用interrupt()中断线程

通过中断信号协作,线程需检查中断状态或处理

Thread thread = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        try {
            System.out.println("Working...");
            Thread.sleep(1000); // 阻塞方法会响应中断
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted");
            Thread.currentThread().interrupt(); // 重新设置中断标志
            break;
        }
    }
});
thread.start();
Thread.sleep(3000);
thread.interrupt(); // 发送中断信号

关键点:

阻塞方法(如sleep(), wait(), join())会立即抛出InterruptedException。

非阻塞代码需手动检查isInterrupted()。

适用场景:需要快速响应中断的任务。


3. 通过线程池关闭(ExecutorService)

使用线程池管理时,调用shutdown()或shutdownNow()

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("Running...");
    }
});
Thread.sleep(3000);
executor.shutdownNow(); // 发送中断信号并停止所有线程

区别:

shutdown():等待已提交任务完成。

shutdownNow():尝试中断所有运行中的线程。

适用场景:使用线程池的并发任务。


4. 使用Future取消任务(带返回值任务)

通过Future.cancel(true)中断正在执行的Callable任务

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("Calculating...");
    }
    return 42;
});
Thread.sleep(1000);
future.cancel(true); // true表示允许中断线程

为什么不推荐用Thread.stop()?

❌ 立即释放所有锁,可能导致对象状态不一致。

❌ 不执行finally块,容易引发资源泄漏。

❌ 已被官方标记为@Deprecated。

避免使用以下已废弃方法:

Thread.stop():暴力终止,可能导致状态不一致。

Thread.suspend()/resume():易导致死锁。

⭐⭐9.synchronized关键字的底层原理

1. 基本概念

synchronized 是 Java 中实现线程同步的核心机制,通过内置锁(监视器锁,Monitor Lock)确保多线程环境下对共享资源的互斥访问。其底层实现涉及 对象头、锁状态升级、JVM 指令 等关键机制。

2. 对象头与 Monitor

对象头结构:

Java 对象在内存中分为三部分:

  • 对象头(Header)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)。

对象头中的锁信息:存储锁标志位(Lock Flag)、偏向线程ID、轻量级锁指针、重量级锁指针等。

锁标志位(Mark Word):标识当前对象的锁状态(无锁、偏向锁、轻量级锁、重量级锁)。

Monitor 机制:

每个 Java 对象都与一个 Monitor 关联。

线程执行 synchronized 代码块时,需先通过 monitorenter 指令获取对象的 Monitor。

执行完毕时,通过 monitorexit 指令释放 Monitor。

Monitor内部具体的存储结构:

  • Owner:存储当前获取锁的线程的,只能有一个线程可以获取
  • EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
  • WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程

具体的流程:

  • 代码进入synchorized代码块,先让lock(对象锁)关联的monitor,然后判断Owner是否有线程持有
  • 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功
  • 如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)
  • 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待

3.回答

synchronized 底层使用的JVM级别中的Monitor 来决定当前线程是否获得了锁,如果某一个线程获得了锁,在没有释放锁之前,其他线程是不能或得到锁的。synchronized 属于悲观锁。synchronized 因为需要依赖于JVM级别的Monitor ,相对性能也比较低。

monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因
monitor内部维护了三个变量

  • WaitSet:保存处于Waiting状态的线程
  • EntryList:保存处于Blocked状态的线程
  • Owner:持有锁的线程

只有一个线程获取到的标志就是在monitor中设置成功了Owner,一个monitor中只能有一个Owner
在上锁的过程中,如果有其他线程也来抢锁,则进入EntryList 进行阻塞,当获得锁的线程执行完了,释放了锁,就会唤醒EntryList 中等待的线程竞争锁,竞争的时候是非公平的

10.syncronized锁升级的过程讲一下

无锁:这是没有开启偏向锁的时候的状态,在JDK1.6之后偏向锁的默认开启的,但是有一个偏向延迟,需要在JVM启动之后的多少秒之后才能开启,这个可以通过JVM参数进行设置,同时是否开启偏向锁也可以通过JVM参数设置。

偏向锁:这个是在偏向锁开启之后的锁的状态,如果还没有一个线程拿到这个锁的话,这个状态叫做匿名偏向,当一个线程拿到偏向锁的时候,下次想要竞争锁只需要拿线程ID跟MarkWord当中存储的线程ID进行比较,如果线程ID相同则直接获取锁(相当于锁偏向于这个线程),不需要进行CAS操作和将线程挂起的操作。

轻量级锁:在这个状态下线程主要是通过CAS操作实现的。将对象的MarkWord存储到线程的虚拟机栈上,然后通过CAS将对象的MarkWord的内容设置为指向Displaced Mark Word的指针,如果设置成功则获取锁。在线程出临界区的时候,也需要使用CAS,如果使用CAS替换成功则同步成功,如果失败表示有其他线程在获取锁,那么就需要在释放锁之后将被挂起的线程唤醒。

重量级锁:当有两个以上的线程获取锁的时候轻量级锁就会升级为重量级锁,因为CAS如果没有成功的话始终都在自旋,进行while循环操作,这是非常消耗CPU的,但是在升级为重量级锁之后,线程会被操作系统调度然后挂起,这可以节约CPU资源。

线程A进入 synchronized 开始抢锁,JVM 会判断当前是否是偏向锁的状态,如果是就会根据 Mark Word 中存储的线程 ID 来判断,当前线程A是否就是持有偏向锁的线程。如果是,则忽略 check,线程A直接执行临界区内的代码。 但如果 Mark Word 里的线程不是线程 A,就会通过自旋尝试获取锁,如果获取到了,就将 Mark Word 中的线程 ID 改为自己的;如果竞争失败,就会立马撤销偏向锁,膨胀为轻量级锁。 后续的竞争线程都会通过自旋来尝试获取锁,如果自旋成功那么锁的状态仍然是轻量级锁。然而如果竞争失败,锁会膨胀为重量级锁,后续等待的竞争的线程都会被阻塞。

11.CAS 你知道吗?

CAS全称:Compare And Swap(比较在交换),体现的一种乐观锁的思想,在没有锁的情况保证了线程操作共享数据的原子性。

一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值