• Java面试题-线程


    1、创建线程的方式及实现

    Java中创建线程有4种方式:

    1. 继承Thread类,重写run()
    2. 实现Runnable接口,重写run()
    3. 实现Callable接口,重写call()
    4. 通过线程池

    1. 继承Thread类

    继承Thread类,重写run()接口。

    /*类继承*/
    class NewThread extends Thread{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"线程开启。。。");
        }
    }
    
    public static void main(String[] args) {
      //类继承调用
      NewThread newThread = new NewThread();
      newThread.start();
    
      //匿名内部类
      new Thread(()->{
        System.out.println(Thread.currentThread().getName()+"线程开启。。。");
      }).start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2. 实现Runnable接口

    实现Runnable接口,重写run()。(Java不可多继承)

    //实现Runnable接口
    class NewRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"线程开启。。。");
        }
    }
    
    
    public static void main(String[] args) {
    	NewRunnable newRunnable = new NewRunnable();
      new Thread(newRunnable).start();
      
      //匿名内部类
      new Thread(new Runnable() {
        @Override
        public void run() {
          System.out.println(Thread.currentThread().getName()+"线程开启。。。");
        }
      }).start();
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    3. 实现Callable接口

    实现Callable接口,重写call()方法,任务交由FutureTask,由线程处理,能够支持返回值

    //这里Callable<T> 决定返回值类型
    class NewCallable implements Callable<String>{
        @Override
        public String call() throws Exception {
            System.out.println(Thread.currentThread().getName()+"线程开启。。。");
            return Thread.currentThread().getName();
        }
    }
    
    public static void main(String[] args) {
      NewCallable newCallable = new NewCallable();
      FutureTask<String> task = new FutureTask<>(newCallable);
      Thread thread = new Thread(task);
      thread.start();
      try {
        System.out.println("获取task返回值:"+task.get());
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    4. 通过线程池

    Executors.newXXXXXPool()其底层都是通过ThreadPoolExecutor进行创建,根据《阿里巴巴Java开发手册》,推荐使用自定义ThreadPoolExecutor能够对线程池更深入的把控。

    更详细关于线程池内容直接看:ThreadPoolExecutor线程池详解与使用

    private static ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(5);
    
    public static void main(String[] args) {
      threadPoolExecutor.execute(()->{
        System.out.println(Thread.currentThread().getName()+"启动了。。。");
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2、sleep()、join()、yield()的区别

    • sleep()

      使当前执行的线程休眠,但不会释放锁。先进入阻塞态,待休眠时间结束后,后转入就绪态等待执行机会。

      //休眠5秒
      Thread.sleep(5000);
      
      • 1
      • 2
    • join()

      非静态方法,让一个线程等待另外一个线程完成才继续执行

      public static void main(String[] args) throws InterruptedException {
        //线程a睡眠2秒
        Thread a = new Thread(() -> {
          System.out.println(Thread.currentThread().getName()+"执行中");
          try {
            Thread.sleep(2000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName()+"阻塞结束");
        },"a");
      	//启动线程
        a.start();
        //main线程阻塞,等待a线程处理完成
        a.join();
        System.out.println(Thread.currentThread().getName()+"结束");
      }
      
      
      //输出结果如下:
      //a执行中
      //a阻塞结束
      //main结束
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
    • yield()

      当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!

    3、说说CountDownLatch原理

    • CountDownLatch应用场景:在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待,直到其他线程操作完成。
    • 基于AQS的共享模式。AQS简介
    • 常用方法:
      • countDown():计数器减1
      • await():直到计数器为0时,线程继续执行,否则是被阻塞状态。
      • await(long timeout, TimeUnit unit):直到计数器为0或超过指定时间内,线程继续执行,否则是被阻塞状态。
    • 实现原理:
      • 内部类Sync继承AbstractQueuedSynchronizer,维护volatile修饰的全局变量state
      • final修饰的Sync类。
      • 创建CountDownLatch()对象时,调用new Sync(count)初始化Sync类,调用AbstractQueuedSynchronizersetState(newState)方法,赋值全局变量state
      • 调用countDown()方法时,调用AbstractQueuedSynchronizersync.releaseShared(1)方法。
          1. tryReleaseShared(releases)通过for循环配合CAS尝试进行-1操作,当执行完成返回nextc == 0;当执行完成结果为0返回true(为0代表计数器结束),否则返回false。
          2. 根据tryReleaseShared(releases)结果,若为true,则进入doReleaseShared(),返回true;否则返回false。
      • 调用await()方法时,调用sync.acquireSharedInterruptibly(1);
        • 如果线程中断,抛出InterruptedException()异常
        • 否则进入tryAcquireShared()方法,尝试获取state值,如果为0则返回1;否则返回-1。
          • 当返回值小于0时,进入doAcquireSharedInterruptibly()方法,阻塞当前线程。

    4、说说CyclicBarrier原理

    CyclicBarrier的应用场景为:实现一组线程相互等待,当所有线程都达到某个屏障点后再进行后续操作。

    似乎和CountDownLatch差不多?

    ​ 这个疑问在我一开始理解他的概念时就冒出来了,后来随着深入学习,发现他与CountDownLatch的使用区别在于:CyclicBarrier可以实现循环拦截

    CyclicBarrier是基于ReentrantLockCondition组合使用。

    CyclicBarrier原理

    ​ 线程调用await(),告诉CyclicBarrier到达屏障,然后线程阻塞;等到所有线程达到屏障count==0,结束阻塞,继续线程后续逻辑。await()核心调用dowait()方法。

    • dowait()方法
      1. ReentrantLock加锁
      2. 如果当前屏障被打破,抛出BrokenBarrierException()异常
      3. 如果当前线程被中断,将当前屏障打破(generation.broken = true;);当前剩余需要拦截到数量置为拦截的总数量(count = parties;);唤醒所有等待的线程;抛出InterruptedException()异常
      4. 当前线程未被中断,内部计数器-1。int index = --count;
      5. 如果内部计数器为0,则说明为最后一个线程到达屏障
        1. 如有指定执行的任务(构造函数中第二参数所传的任务),则执行指定任务。
        2. 唤醒所有线程,重置参数(计数器、栅栏下一代),new generation任务。
        3. 1执行失败,执行:当前屏障打破(generation.broken = true;);当前剩余需要拦截到数量置为拦截的总数量(count = parties;);唤醒所有等待的线程
        4. 返回0。
      6. 不为最后一个线程到达屏障,进入代码循环块
        1. 是否有指定等待时间,如有则继续进入trip.await();否则进入nanos = trip.awaitNanos(nanos);,返回剩余时间
        2. 1抛出异常,则判断屏障是否被打破
          1. 打破:当前屏障打破(generation.broken = true;);当前剩余需要拦截到数量置为拦截的总数量(count = parties;);唤醒所有等待的线程;抛出异常
          2. 未被打破:当前线程挂起Thread.currentThread().interrupt();
        3. 当有线程被唤醒,且屏障被打破,抛出BrokenBarrierException()异常
        4. 通过g != generation判断说明已换代,返回index= --count`
        5. timed && nanos <= 0L说明线程超时,则:当前屏障打破(generation.broken = true;);当前剩余需要拦截到数量置为拦截的总数量(count = parties;);唤醒所有等待的线程;抛出TimeoutException
        6. 关闭1的Reentrant锁

    CyclicBarrier原理

    5、说说Semaphore原理

    Semaphore可以控制同时访问共享资源的线程个数,线程通过 acquire方法获取一个信号量,信号量减一,如果没有就等待;通过release方法释放一个信号量,信号量加一。它通过控制信号量的总数量,以及每个线程所需获取的信号量数量,进而控制多个线程对共享资源访问的并发度,以保证合理的使用共享资源。相比synchronized和独占锁一次只能允许一个线程访问共享资源,功能更加强大

    深入理解Semaphore原理

    6、说说Exchanger原理

    Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

    ​ 因此使用Exchanger的重点是成对的线程使用exchange()方法,当有一对线程达到了同步点,就会进行交换数据。因此该工具类的线程对象是成对的。 Exchanger类提供了两个方法,String exchange(V x):用于交换,启动交换并等待另一个线程调用exchange;String exchange(V x,long timeout,TimeUnit unit):用于交换,启动交换并等待另一个线程调用exchange,并且设置最大等待时间,当等待时间超过timeout便停止等待。

    Exchanger的工作原理及实例

    7、说说CountDownLatch与CyclicBarrier区别

    ​ 这两个类都可以实现一组线程达到某个条件之前进行等待,内部都有计数器,当计数器为0时被阻塞的线程会被唤醒。

    区别:

    • 计数器控制权限:CyclicBarrier计数器由await()方法控制,CountDownLatch计数器由countDown()方法控制。
    • 拦截次数:CyclicBarrier可以实现循环拦截,CountDownLatch则只能拦截一轮。

    8、ThreadLocal原理分析

    ​ ThreadLocal用于线程间的数据隔离,为每一个线程都提供了数据副本,使得不同线程访问的数据不是同一个对象!!!

    ​ 每个Thread对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。

    ThreadLocal原理详解——终于弄明白了ThreadLocal

    ThreadLocal原理详解

    9、讲讲线程池的实现原理

    ​ 预先启动一些线程,线程无限循环从任务队列中获取一个任务进行执行,直到线程池被关闭。如果某个线程因为执行某个任务发生异常而终止,那么重新创建一个新的线程而已,如此反复。

    线程池的实现原理

    10、线程池的几种方式

    ​ 根据《阿里巴巴Java开发手册》,推荐使用第7种方式。

    1、 newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
    2、 newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
    3、 newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
    4、newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
    5、newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
    6、newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
    7、ThreadPoolExecutor():是最原始的线程池创建,上面创建方式都是对ThreadPoolExecutor的封装。

    ThreadPoolExecutor线程池详解与使用

    11、线程的生命周期

    ​ 线程的生命周期分为5个阶段:

    • 新建(New):创建一个线程对象后,该线程就处于新建状态。
    • 就绪(Runnable):调用start()方法后,该线程就处于就绪状态。
    • 运行(Running):获得CPU使用权,执行run()方法,系统分配时间内线程都是运行状态。
    • 阻塞(Blocked):在某些情况下(如获取同步对象,同步对象被其他线程持有),会让出CPU使用权并暂时中止任务的执行。直到情况消除,进入就绪状态,等待CPU分配时间。
    • 死亡(Terminated):线程调用stop()run()方法执行完成,或抛出异常或错误,线程进入死亡状态,不会再转入其他状态。
  • 相关阅读:
    buildadmin+tp8表格操作(2)----表头上方按钮绑定事件处理,实现功能(全选/全不选)
    [附源码]java毕业设计朋辈帮扶系统
    H5前端开发——BOM
    算法训练Day30 回溯算法专题 | LeetCode332. 重新安排行程;51.N皇后(棋盘问题);37.解数独(二维的递归)
    【JQuery】JQuery入门——知识点讲解(三)
    ANR 原理及实践
    BUUCTF easyre 1
    一起学习ML和DL中常用的几种loss函数
    java-php-python-ssm员工培训管理系统计算机毕业设计
    java实现快速排序的方法
  • 原文地址:https://blog.csdn.net/qq_36986510/article/details/125559799