java多线程面试题

919

1.什么是线程,什么是进程,它们之前的区别和联系

①.线程是CPU独立运行和独立调度的基本单位;

②.进程是操作系统资源分配的基本单位,是执行着的应用程序。

③.两者的联系:

  • 进程和线程都是操作系统所运行的程序运行的基本单元。
  • 进程是线程的容器,真正完成代码执行的是线程,而进程则作为线程的执行环境。

④.两者的区别:

  • 进程具有独立的空间地址,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
  • 线程只是一个进程的不同执行路径,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉会影响这个进程。
  • 进程是执行着的应用程序,而线程是进程中执行的代码片段。一个程序至少包含一个进程,一个进程至少包含一个线程,一个进程中的多个线程共享当前进程所拥有的资源。

2.线程和进程各自有什么区别和优劣

  • 进程是资源分配的最小单位,线程是程序执行的最小单位。
  • 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作开销非常大。
  • 线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据。
  • 进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
  • 多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

3.java中创建线程有几种不同的方式

  • 继承Thread类
public class MyThead extends Thread {

    @Override
    public void run() {
        System.out.println("支线程" + Thread.currentThread().getName());
    }
}

//客户端创建线程
public static void main(String[] args) {
        MyThead thead1 = new MyThead();
        MyThead thead2 = new MyThead();
        thead1.start();
        thead2.start();
        System.out.println("主线程"+Thread.currentThread().getName());
}

通过继承的方式实现,实现简单,但应用场景偏少,java不支持多继承。
  • 实现Runnable
public class MyThead1 implements Runnable {

    @Override
    public void run() {
        System.out.println("支线程" + Thread.currentThread().getName());
    }
}

//客户端创建线程
public static void main(String[] args) {
        MyThead1 thead1 = new MyThead1();
        MyThead1 thead2 = new MyThead1();
        thead1.run();
        thead2.run();
        System.out.println("主线程"+Thread.currentThread().getName());
}

最常用的方式,实现简单,基本任何场景都能使用。
  • 通过Callable和Future创建线程
public class MyThead2 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("支线程" + Thread.currentThread().getName());
        return 0;
    }
}

//客户端创建线程
public static void main(String[] args) {
        MyThead2 thead1 = new MyThead2();
        MyThead2 thead2 = new MyThead2();
        FutureTask<Integer> result1 = new FutureTask<>(thead1);
        new Thread(result1).start();
        try {
            System.out.println("支线程结果" + result1.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        FutureTask<Integer> result2 = new FutureTask<>(thead2);
        new Thread(result2).start();
        try {
            System.out.println("支线程结果" + result2.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("主线程" + Thread.currentThread().getName());

}

特殊场景使用,如果当创建的线程需要又返回值时,可以使用,但是如果使用FutureTask获取返回值会导致主线程阻塞。

4.线程的几种状态

  • 新建状态(new):新创建了一个线程对象。
  • 就绪状态(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权。
  • 运行状态(running):就绪状态(runnable)的线程获得了cpu 时间片(timeslice ),执行程序代码。
  • 阻塞状态(block):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。
    阻塞的情况分三种:
    1.等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
    2.同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
    3.其他阻塞: 运行(running)的线程执行Thread.sleep(long ms)或t.join ()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪(runnable)状态。
  • 死亡状态(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

5.什么是死锁,如何避免死锁

  • 死锁:两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是两个进程都陷入了无限的等待中。
  • 多线程产生死锁的四个必要条件: 1.互斥条件: 一个资源每次只能被一个进程使用。
    2.保持和请求条件: 一个进程因请求资源而阻塞时,对已获得资源保持不放。
    3.不可剥夺条件: 进程已获得资源,在未使用完成前,不能被剥夺。
    4.循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系。
  • 避免死锁:破坏任何一个条件即可

6.Thread 类中的start() 和 run() 方法有什么区别

  • 通过调用线程类的start()方法来启动一个线程,使线程处于就绪状态,即可以被JVM来调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成实际的业务逻辑,当run()方法结束后,此线程就会终止。
  • 如果直接调用线程类的run()方法,会被当作一个普通的函数调用,程序中仍然只有主线程这一个线程。即start()方法能够异步的调用run()方法,但是直接调用run()方法却是同步的,无法达到多线程的目的。
  • 因此,只用通过调用线程类的start()方法才能达到多线程的目的。

7.Java中如何停止一个线程

JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法,但是由于潜在的死锁威胁。 因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。 当run()或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。
先说两个概念:锁池和等待池
1.锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
2.等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中。
然后再来说notify和notifyAll的区别
1.如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
2.当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只有一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
3.优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

8.什么是线程池

  • 线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

9.常见的线程池和使用场景

  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
    //创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
    public static void singleTheadPoolTest() {
        //默认参数corePoolSize为1;maximumPoolSize为1;keepAliveTime为0L;unit为:TimeUnit.MILLISECONDS;workQueue为:new LinkedBlockingQueue<Runnable>() 无界阻塞队列
        ExecutorService pool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int ii = i;
            pool.execute(() -> out.println(Thread.currentThread().getName() + "=>" + ii));
        }
    }

//运行结果
线程名称:pool-1-thread-1,执行0      
线程名称:pool-1-thread-1,执行1       
线程名称:pool-1-thread-1,执行2      
线程名称:pool-1-thread-1,执行3     
线程名称:pool-1-thread-1,执行4      
线程名称:pool-1-thread-1,执行5    
线程名称:pool-1-thread-1,执行6    
线程名称:pool-1-thread-1,执行7    
线程名称:pool-1-thread-1,执行8   
线程名称:pool-1-thread-1,执行9
  • newFixedThreadPool 一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。
//创建可容纳固定数量线程的池子,每个线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界阻塞队列)
public static void fixTheadPoolTest() {
		//接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;keepAliveTime为0L(不限时);unit为:TimeUnit.MILLISECONDS;WorkQueue为:new LinkedBlockingQueue<Runnable>() 无界阻塞队列
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int ii = i;
            fixedThreadPool.execute(() -> {
                out.println("线程名称:" + Thread.currentThread().getName() + ",执行" + ii);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
 
//运行结果
线程名称:pool-1-thread-3,执行2
线程名称:pool-1-thread-1,执行0
线程名称:pool-1-thread-2,执行3
线程名称:pool-1-thread-3,执行4
线程名称:pool-1-thread-1,执行5
线程名称:pool-1-thread-2,执行6
线程名称:pool-1-thread-3,执行7
线程名称:pool-1-thread-1,执行8
线程名称:pool-1-thread-3,执行9
  • newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
//当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。
public static void cacheThreadPool() {
		//默认参数:corePoolSize为0;maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为60L;unit为TimeUnit.SECONDS;workQueue为SynchronousQueue(同步队列)
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 1; i <= 10; i++) {
            final int ii = i;
            try {
                Thread.sleep(ii * 1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            cachedThreadPool.execute(()->out.println("线程名称:" + Thread.currentThread().getName() + ",执行" + ii));
        }

    }
    
//运行结果
线程名称:pool-1-thread-1,执行1
线程名称:pool-1-thread-1,执行2
线程名称:pool-1-thread-1,执行3
线程名称:pool-1-thread-1,执行4
线程名称:pool-1-thread-1,执行5
线程名称:pool-1-thread-1,执行6
线程名称:pool-1-thread-1,执行7
线程名称:pool-1-thread-1,执行8
线程名称:pool-1-thread-1,执行9
线程名称:pool-1-thread-1,执行10
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
//创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构
public static void sceduleThreadPool() {
        //corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为0;unit为:TimeUnit.NANOSECONDS;workQueue为:new DelayedWorkQueue() 一个按超时时间升序排序的队列
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        Runnable r1 = () -> out.println("线程名称:" + Thread.currentThread().getName() + ",执行:3秒后执行");
        scheduledThreadPool.schedule(r1, 3, TimeUnit.SECONDS);
        Runnable r2 = () -> out.println("线程名称:" + Thread.currentThread().getName() + ",执行:延迟2秒后每3秒执行一次");
        scheduledThreadPool.scheduleAtFixedRate(r2, 2, 3, TimeUnit.SECONDS);
        Runnable r3 = () -> out.println("线程名称:" + Thread.currentThread().getName() + ",执行:普通任务");
        for (int i = 0; i < 5; i++) {
            scheduledThreadPool.execute(r3);
        }
    }


//运行结果
线程名称:pool-1-thread-1,执行:普通任务
线程名称:pool-1-thread-5,执行:普通任务
线程名称:pool-1-thread-4,执行:普通任务
线程名称:pool-1-thread-3,执行:普通任务
线程名称:pool-1-thread-2,执行:普通任务
线程名称:pool-1-thread-1,执行:延迟2秒后每3秒执行一次
线程名称:pool-1-thread-5,执行:3秒后执行
线程名称:pool-1-thread-4,执行:延迟2秒后每3秒执行一次
线程名称:pool-1-thread-4,执行:延迟2秒后每3秒执行一次
线程名称:pool-1-thread-4,执行:延迟2秒后每3秒执行一次
线程名称:pool-1-thread-4,执行:延迟2秒后每3秒执行一次 

10.线程池中的几种重要的参数

  • corePoolSize 线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收
  • maximumPoolSize 线程池中可以容纳的最大线程的数量
  • keepAliveTime 线程池中除了核心线程之外的其他线程的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间。
  • util 计算这个时间的一个单位。
  • workQueue 等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。
  • threadFactory 创建线程的线程工厂。
  • handler 一种拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。

11.线程池的等待队列

  • ArrayBlockingQueue 一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
  • SynchronousQueue 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
  • PriorityBlockingQueue 一个具有优先级的无限阻塞队列。

12.线程池的拒绝策略

当请求任务不断的过来,而系统此时又处理不过来的时候,我们需要采取的策略是拒绝服务。在ThreadPoolExecutor中已经包含四种处理策略。

  • AbortPolicy策略 该策略会直接抛出异常,阻止系统正常工作。
  • CallerRunsPolicy 策略 只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
  • DiscardOleddestPolicy策略 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
  • DiscardPolicy策略 该策略默默的丢弃无法处理的任务,不予任何处理。
    除了JDK默认提供的四种拒绝策略,我们可以根据自己的业务需求去自定义拒绝策略,自定义的方式很简单,直接实现RejectedExecutionHandler接口即可。

13.线程池如何关闭,初始化线程池时线程数的选择

  • 关闭线程池可以调用shutdownNow和shutdown两个方法来实现 shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
    shutdown:当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。
  • 线程数的选择 如果任务是IO密集型,一般线程数需要设置2倍CPU数以上,以此来尽量利用CPU资源。
    如果任务是CPU密集型,一般线程数量只需要设置CPU数加1即可,更多的线程数也只能增加上下文切换,不能增加CPU利用率。
    上述只是一个基本思想,如果真的需要精确的控制,还是需要上线以后观察线程池中线程数量跟队列的情况来定。