20240113面试练习题4

1. 线程中 start() 方法和 run() 方法有什么区别?

调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run();直接调用 run() 方法,无法达到启动多线程的目的,相当于主线程线性执行 Thread 对象的 run() 方法。
一个线程对线程的 start() 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常;run() 方法没有限制。


2. 线程是如何通讯的?它的通讯方法有哪些?(说出你知道的所有通讯方法)

等待和通知机制:使用 Object 类的 wait() 和 notify() 方法来实现线程之间的通讯。当一个线程需要等待另一个线程执行完某个操作时,它可以调用 wait() 方法使自己进入等待状态,同时释放占有的锁,等待其他线程调用 notify() 或 notifyAll() 方法来唤醒它。被唤醒的线程会重新尝试获取锁并继续执行。
信号量机制:使用 Java 中的 Semaphore 类来实现线程之间的同步和互斥。Semaphore 是一个计数器,用来控制同时访问某个资源的线程数。当某个线程需要访问共享资源时,它必须先从 Semaphore 中获取一个许可证,如果已经没有许可证可用,线程就会被阻塞,直到其他线程释放了许可证。
栅栏机制:使用 Java 中的 CyclicBarrier 类来实现多个线程之间的同步,它允许多个线程在指定的屏障处等待,并在所有线程都达到屏障时继续执行。
锁机制:使用 Java 中的 Lock 接口和 Condition 接口来实现线程之间的同步和互斥。Lock 是一种更高级的互斥机制,它允许多个条件变量(Condition)并支持在同一个锁上等待和唤醒。


3. 说一下线程的生命周期?

理论层面的状态:

新建
刚new出来的线程对象
就绪
线程执行了start()方法后
执行
拥有CPU的执行权
阻塞
线程会处于阻塞状态
死亡
run方法执行完

代码层面的状态:
NEW 至今尚未启动的线程处于这种状态。
RUNNABLE 正在 Java 虚拟机中执行的线程处于这种状态。
BLOCKED 受阻塞并等待某个监视器锁的线程处于这种状态。
WAITING 无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
TIMED_WAITING 等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
TERMINATED 已退出的线程处于这种状态。
在这里插入图片描述


4. 如何停止线程?

1、线程正常退出,也就是当 run() 方法完成后线程中止。
2、使用 stop() 方法强行终止线程,但是不推荐使用这个方法,该方法已被弃用。
3、使用 interrupt() 方法中断线程,在当前线程中打一个停止的标记,并不是真的停止线程。

1、在 run() 方法执行完毕后,该线程就终止了。但是在某些特殊的情况下,run() 方法会被一直执行;如:
在服务端程序中可能会使用 while(true) { … }
这样的循环结构来不断的接收来自客户端的请求。此时就可以用修改标志位的方式来结束 run() 方法。

public class ThreadTest extends Thread {
	    public volatile boolean exit = false; 
	    public void run() { 
	        while (!exit); 
	    } 
	public static void main(String[] args) throws InterruptedException {
		ThreadTest thread = new ThreadTest(); 
        thread.start(); 
        Thread.sleep(5000); // 主线程延迟5秒 
        thread.exit = true;  // 终止线程thread 
        thread.join(); 
        System.out.println("线程退出!"); 
	}
}

2、调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。

例如,存在一个对象 u 持有 ID 和 NAME 两个字段,假如写入线程在写对象的过程中,只完成了对 ID 的赋值,但没来得及为 NAME 赋值,就被 stop() 导致锁被释放,那么当读取线程得到锁之后再去读取对象 u 的 ID 和 Name 时,就会出现数据不一致的问题。

public class ThreadTest{

	 static class Thread1 extends Thread {
	        @Override
	        public void run() {
	            for (int i = 0; i < 500000; i++) {
	                System.out.println("打印的数字"+i);
	            }
	        }
	 }
	public static void main(String[] args) throws InterruptedException {
		Thread1 thread1 = new Thread1();
		thread1.start();
        //保证子线程进入运行状态,避免还没运行就被终止
        Thread.sleep(100);
        //暴力停止子线程
        thread1.stop();
	}
}

3、使用 interrupt() 中断线程
调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。这一点很重要,如果中断后,线程立即无条件退出,那么我们又会遇到 stop() 方法的老问题。如果希望线程 t 在中断后停止,就必须先判断是否被中断,并为它增加相应的中断处理代码。

Thread thread = new Thread(() -> {
    System.out.println("thread start");
    for (int i=0;i<3000;i++){
        if(Thread.currentThread().isInterrupted()){
            System.out.println("thread isInterrupted");
            break;
        }else {
            System.out.println(i);
        }
    }
    System.out.println("thread end");
});
thread.start();

5. wait() 方法和 sleep() 方法有什么区别?

1、所属不同:
a. sleep定义在Thread类,静态方法
b. wait定义在 Object类中,非静态方法

2、唤醒条件不同
a. sleep: 休眠时间到
b. wait: 在其他线程中,在同一个锁对象上,调用了notify或notifyAll方法

3、使用条件不同:
a. sleep 没有任何前提条件
b. wait(), 必须当前线程,持有锁对象,锁对象上调用wait()

4、休眠时,对锁对象的持有,不同:(最最核心的区别)
a. 线程因为sleep方法而处于阻塞状态的时候,在阻塞的时候不会放弃对锁的持有
b. 但是wait()方法,会在阻塞的时候,放弃锁对象持有


6. 线程池相比于线程有什么优点?

降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

提高响应速度:当任务到达时,可以不需要等待线程创建就能立即执行。

提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,监控和调优。


7. 说下线程池创建参数都有哪些?它们都有哪些含义?

线程池的构造函数有7个参数,分别是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。

corePoolSize 线程池核心线程大小
线程池中会维护一个最小的线程数量,即使这些线程处于空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。任务提交到线程池后,首先会检查当前线程数是否达到了corePoolSize,如果没有达到的话,则会创建一个新线程来处理这个任务。

maximumPoolSize 线程池最大线程数量
当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列中。如果队列也已满,则会去创建一个新线程来出来这个处理。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。

keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定

unit 空闲线程存活时间单位
keepAliveTime的计量单位

workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。

③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等

handler 拒绝策略

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
①CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列


8. 线程工厂有什么用?不设置线程工厂会怎样?

在JDK的源码使用工厂模式,ThreadFactory就是其中一种。

在我们一般的使用中,创建一个线程,通常有两种方式:
继承Thread类,覆盖run方法,实现我们需要的业务
继承Runnable接口,实现run方法,实现我们需要的业务,并且调用new Thread(Runnable)方法,将其包装为一个线程执行

设想这样一种场景,我们需要一个线程池,并且对于线程池中的线程对象,赋予统一的线程优先级、统一的名称、甚至进行统一的业务处理或和业务方面的初始化工作,这时工厂方法就是最好用的方法了

不设置线程工厂会怎样?
我们在项目开发的过程中,如果有很多地方使用多线程,却没有给线程命名,这样当出现问题的时候就比较不容容易排查


9. 线程的优先级有什么用?如何设置线程池的优先级?

Java程序中的多个线程是并发执行的,某个线程若想被执行必须要得到CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制称为线程的调度。

在计算机中,线程调度有两种模型,分别是分时调度模型和抢占式调度模型。分时调度模型,是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片。抢占式调度模型,是指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU使用权。Java虚拟机默认采用抢占式调度模型,通常情况下程序员不需要去关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度。

在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。通过调用线程对象的setPriority()方法,可以设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。线程的优先级用1~10的整数来表示,数字越大优先级越高。

注意:
从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行


10. 说一下线程池的执行流程?

1、提交任务后会首先进行当前工作线程数与核心线程数的比较,如果当前工作线程数小于核心线程数,则直接调用 addWorker() 方法创建一个核心线程去执行任务;

2、如果工作线程数大于核心线程数,即线程池核心线程数已满,则新任务会被添加到阻塞队列中等待执行,当然,添加队列之前也会进行队列是否为空的判断;

3、如果线程池里面存活的线程数已经等于核心线程数了,且阻塞队列已经满了,再会去判断当前线程数是否已经达到最大线程数 maximumPoolSize,如果没有达到,则会调用 addWorker() 方法创建一个非核心线程去执行任务;

4、如果当前线程的数量已经达到了最大线程数时,当有新的任务提交过来时,会执行拒绝策略

public void execute(Runnable command) {
    	// 如果当前任务为空,抛出空指针异常
        if (command == null)
            throw new NullPointerException();
    	// 获取当前线程池的状态+线程个数变量的组合值
        int c = ctl.get();
    	// 如果当前线程数小于核心线程池大小,那么就创建线程并执行当前任务
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
    	// 如果线程池处于RUNNING状态,把任务添加到阻塞队列
        if (isRunning(c) && workQueue.offer(command)) {
            // 二次检查
            int recheck = ctl.get();
            // 如果当前线程池状态不是RUNNING则从队列中删除任务,并且执行线程池的拒绝策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
            // 如果当前线程池为空,则创建一个线程
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
    	// 如果队列满了,则新增线程,新增线程失败则执行线程池的拒绝策略
        else if (!addWorker(command, false))
            reject(command);
    }
  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值