1.Java多线程详解(含常见面试题)

该博客围绕Java线程展开,介绍了线程与进程的概念及区别,阐述了创建线程的方式、线程状态、常用方法等基础知识。还讲解了线程同步、死锁及避免方法,对比了synchronized与Lock等锁机制。此外,详细介绍了线程池的参数、创建方式、执行流程和拒绝策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 什么是线程?


​ 说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

​ 而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位

​ 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的的单位。

​ 注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错局。


2. 线程和进程的区别?


​ 一个程序下至少有一个进程,一个进程下至少有一个线程,一个进程下也可以有多个线程来增加程序的执行速度。

单线程:只有主线程一条执行路径

多线程:多条执行路径,主线程和子线程并行交替执行


3. 守护(daemon)线程


​ 守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。

​ 线程分为用户线程和守护线程

​ 虚拟机必须确保用户线程执行完毕

​ 虚拟机不用等待守护线程执行完毕

​ 如:后台记录操作日志,监控内存,垃圾回收等待…


4. 创建线程有哪几种方式?


创建线程有三种方式:

继承 Thread类:

  1. 自定义线程类继承Thread类
  2. 写run()方法,编写线程执行体
  3. 创建线程对象,调用start()方法启动线程

注:不建议使用,避免OOP单继承局限性

实现 Runnable 接口:

  1. 实现runnable接口,
  2. 重写run方法,
  3. 创建runnable接口实现类,调用start方法
    new Thread (testThread).start();

注:推荐使用,避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

实现 Callable 接口:

  1. 实现Callable接口,需要返回值类型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
  5. 提交执行:Future result1 = ser.submit(t1);
  6. 获取结果: boolean r1 = result1.get()
  7. 关闭服务:ser.shutdownNow();

Runnable 和 Callable的区别:

1.runnable 没有返回值,callable 可以拿到有返回值;

2.runnable 需要重写run方法,callable 需要重写call方法;

3.callable 需要抛出异常;

4.callable 可以看作是 runnable 的补充。


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


  1. start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
  2. start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
  3. run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用 start()方法而不是run()方法。

6. 线程的状态?


在这里插入图片描述

创建状态:Thread t = new Thread()线程对象一旦创建就进入到了新生状态

就绪状态:当调用start()方法,线程立即进入就绪状态,但不意味着立即调度执行

运行状态:进入运行状态,线程才真正执行线程体的代码块

阻塞状态:当调用sleep,wait或同步锁定时,线程进入阻塞状态,就是代码不往下执行,阻塞事件解除后,重新进入就绪状态,等待cpu调度执行。

死亡状态:线程中断或者结束,一旦进入死亡状态,就不能再次启动

Thread.State

线程状态。线程可以处于以下状态之一:

NEW:尚未启动的线程处于此状态。
RUNMABLE:在Java虚拟机中执行的线程处于此状态。
BLOCKED:被阻塞等待监视器锁定的线程处于此状态。
WAITING:正在等待另一个线程执行特定动作的线程处于此状态。
TIHED_MAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
TERNHINATED:已遗出的线程处于此状态。

7. 线程休眠(sleep)


  1. sleep(时间)指定当前线程阻塞的毫秒数;
  2. sleep存在异常InterruptedException;
  3. sleep时间达到后线程进入就绪状态;
  4. sleep可以模拟网络延时,倒计时等;
  5. 每一个对象都有一个锁,sleep不会释放锁。

8. 线程礼让(yield)


礼让线程,让当前正在执行的线程暂停,但不阻塞

将线程从运行状态转为就绪状态

让cpu重新调度,礼让不一定成功!看CPU心情


9. Java Object wait() 方法


  • Object wait() 方法让当前线程进入等待状态。直到其他线程调用此对象的 notify() 方法notifyAll() 方法
  • 当前线程必须是此对象的监视器所有者,否则还是会发生 IllegalMonitorStateException 异常。
  • 如果当前线程在等待之前或在等待时被任何线程中断,则会抛出 InterruptedException 异常。

10. yield() 与 wait()的比较


​ wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而yield()的作用是让步,它也会让当前线程离开“运行状态”。

它们的区别是:

​ (1) wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而不yield()是让线程由“运行状态”进入到“就绪状态”。

​ (2) wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁。


11. sleep() 和 wait() 有什么区别?


  • 类的不同:sleep() 来自 Thread,wait() 来自 Object。

  • 释放锁:sleep() 不释放锁;wait() 释放锁。

  • 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。


12. Join


​ Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞

​ 可以想象成插队,强制执行


13. 线程优先级


​ Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。

线程的优先级用数字表示,范围从1~10.

  • Thread.MIN_PRIORITY = 1;
  • Thread.MAX_PRIORITY = 10;
  • Thread.NORM_PRIORITY = 5;

使用以下方式改变或获取优先级

getPriority() . setPriority(int xxx)

优先级低只是意味着获得调度的概率低.并不是优先级低就不会被调用了.这都是看CPU的调度.

优先级的设定建议在start()调度前


14. 并行和并发有什么区别?


并行:多个处理器或多核处理器同时处理多个任务。

并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。

并发 = 两个队列和一台咖啡机。

并行 = 两个队列和两台咖啡机。


15. 线程同步


多个线程操作同一个资源

​ 现实生活中,我们会遇到”同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,每个人都想吃饭,最天然的解决办法就是,排队,一个个来。

​ 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用

​ 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized , 当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题︰
​1. 一个线程持有锁会导致其他所有需要此锁的线程挂起
2. 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
3. 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。


16. 同步方法


​ 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法∶synchronized方法和synchronized 块。

synchronized 关键字声明的方法同一时间只能被一个线程访问。

同步方法:public synchronized void method(int args)

​ synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个
synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

缺陷:若将一个大的方法申明为synchronized将会影响效率


17. 同步块


​ 同步块:synchronized (Obj ){ }

Obj 称之为同步监视器

​ Obj可以是任何对象,但是推荐使用共享资源作为同步监视器同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class [反射中讲解]

​ 同步监视器的执行过程

  1. 第一个线程访问,锁定同步监视器,执行其中代码。
  2. 第二个线程访问,发现同步监视器被锁定,无法访问。
  3. 第一个线程访问完毕,解锁同步监视器。
  4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。

18. 同步方法和同步块,哪个是更好的选择?


​ 同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行开需要等符狄得这个对象上的锁。

​ 同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。

请知道一条原则:同步的范围越小越好。


19. 死锁


​ 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有”两个以上对象的锁”时,就可能会发生“死锁“的问题。

​ 当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。


20. 死锁避免方法


产生死锁的四个必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用。

  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

​ 上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生

怎么防止死锁

  1. 避免一个线程同时获得多个锁
  2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  3. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
  4. 按顺序获取锁,当多个线程需要获取多个锁时,为了避免死锁,我们可以约定一个获取锁的顺序,而且所有线程都按照这个顺序来获取锁。这样,即使出现了争抢资源的情况,由于按照相同的顺序获取锁,就不会发生循环等待的情况,从而避免了死锁的发生。
  5. 使用并发工具类,在Java中,有很多并发工具类可以帮助我们更方便地处理多线程编程中的问题,避免死锁的发生。

21. Lock(锁)


  • ​ 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充
  • ​ java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • ​ **ReentrantLock **类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
class A{
    private final ReentrantLock lock = new ReenTrantLock();
    public void m(){
        lock.lock();
        try{
        	//保证线程安全的代码;
        }
        finally{
        	lock.unlock();
        		//如果同步代码异常,要将unlock()写入finally语句块
        }
    }
}

22. synchronized 与 Lock 的对比


  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁,如果忘记了释放锁会发生死锁) ,而synchronized是隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

优先使用顺序:

Lock >同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)


23. synchronized 和 ReentrantLock 区别是什么?


synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。

主要区别如下:

​ ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;

​ ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;

​ ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。

​ volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。


24. notify()和 notifyAll()有什么区别?


​ notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。


25. 为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?


​ 因为Java所有类的都继承了Object,Java想让任何对象都可以作为锁,并且wait(),notify()等方法用于等待对象的锁或者唤醒线程,在Java的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。

​ 有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。


26. 线程池


​ 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

​ 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

好处:

​ 提高响应速度(减少了创建新线程的时间)

​ 降低资源消耗(重复利用线程池中线程,不需要每次都创建)

​ 便于线程管理 (线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用)


27. 线程池参数


​ ThreadPoolExecutor(线程池)其实也是JAVA的一个类,我们一般通过Executors工厂类的方法,通过传入不同的参数,就可以构造出适用于不同应用场景下的ThreadPoolExecutor(线程池)

构造参数参数介绍:
corePoolsize:核心线程数量

maximumPoolsize:最大线程数量

keepAliveTime:线程保持时间,N个时间单位

unit: 时间单位(比如秒,分)

workQueue:阻塞队列

threadFactory:线程工厂

handler:线程池拒绝策略


28. 什么是Executors?


Executors框架实现的就是线程池的功能。

Executors工厂类中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool、newSingleThreadExecutor等方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池


29. 在Java中Executor和Executors的区别?


  • Executors工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
  • Executor接口对象能执行我们的线程任务。
  • ExecutorService接口继承了Executor接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
  • 使用ThreadPoolExecutor可以创建自定义线程池。

30. 线程池中submit()和execute()方法有什么区别?


相同点:

相同点就是都可以开启线程执行池中的任务。

不同点:

接收参数:execute()只能执行Runnable类型的任务。submit()可以执行Runnable和Callable类型的任务。

返回值:submit()方法可以返回持有计算结果的Future对象,而execute()没有。

异常处理: submit0方便Exception处理。


31. 创建线程池的七种方式


  1. FixedThreadPool

    创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。

public static void fixedThreadPool() {
    // 创建 2 个数据级的线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(2);

    // 创建任务
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
        }
    };

    // 线程池执行任务(一次添加 4 个任务)
    // 执行任务的方法有两种:submit 和 execute
    threadPool.submit(runnable);  // 执行方式 1:submit
    threadPool.execute(runnable); // 执行方式 2:execute
    threadPool.execute(runnable);
    threadPool.execute(runnable);
}
  1. CachedThreadPool

    创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。

public static void cachedThreadPool() {
    // 创建线程池
    ExecutorService threadPool = Executors.newCachedThreadPool();
    // 执行任务
    for (int i = 0; i < 10; i++) {
        threadPool.execute(() -> {
            System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        });
    }
}
  1. SingleThreadExecutor

    创建单个线程数的线程池,它可以保证先进先出的执行顺序。

public static void singleThreadExecutor() {
    // 创建线程池
    ExecutorService threadPool = Executors.newSingleThreadExecutor();
    // 执行任务
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + ":任务被执行");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        });
    }
}
  1. ScheduledThreadPool

    创建一个可以执行延迟任务的线程池。

public static void scheduledThreadPool() {
    // 创建线程池
    ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
    // 添加定时执行任务(1s 后执行)
    System.out.println("添加任务,时间:" + new Date());
    threadPool.schedule(() -> {
        System.out.println("任务被执行,时间:" + new Date());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
    }, 1, TimeUnit.SECONDS);
}
  1. SingleThreadScheduledExecutor

    创建一个单线程的可以执行延迟任务的线程池。

public static void SingleThreadScheduledExecutor() {
    // 创建线程池
    ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
    // 添加定时执行任务(2s 后执行)
    System.out.println("添加任务,时间:" + new Date());
    threadPool.schedule(() -> {
        System.out.println("任务被执行,时间:" + new Date());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
    }, 2, TimeUnit.SECONDS);
}
  1. newWorkStealingPool

    创建一个抢占式执行的线程池(任务执行顺序不确定),注意此方法只有在 JDK 1.8+ 版本中才能使用。

public static void workStealingPool() {
    // 创建线程池
    ExecutorService threadPool = Executors.newWorkStealingPool();
    // 执行任务
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
        });
    }
    // 确保任务执行完成
    while (!threadPool.isTerminated()) {
    }
}
  1. ThreadPoolExecutor

最原始的创建线程池的方式,它包含了 7 个参数可供设置。

public static void myThreadPoolExecutor() {
    // 创建线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
    // 执行任务
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

32. 线程池的执行流程


ThreadPoolExecutor 关键节点的执行流程如下:

  • 当线程数小于核心线程数时,创建线程。
  • 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
  • 当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,创建线程;若线程数等于最大线程数,抛出异常,拒绝任务。

33. 线程池拒绝策略(handler)


​ 当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略:

  • AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。

​ 这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。

  • CallerRunsPolicy:由调用线程处理该任务。

​ 此拒绝策略由调用线程(提交任务的线程)直接执行被丢弃的任务的。

  • DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。

​ 使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。例如,某些视频网站统计视频的播放量就是采用的这种拒绝策略。

  • DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

​ 此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值