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();
}
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();
}


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();
  }
}
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()+"启动了。。。");
  });
}

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

  • sleep()

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

    //休眠5秒
    Thread.sleep(5000);
    
  • 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结束
    
  • 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()方法执行完成,或抛出异常或错误,线程进入死亡状态,不会再转入其他状态。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值