线程相关面试题

线程状态

线程可以划分为三种状态:运行状态(表示正在运行);可运行状态,也是就绪状态(表示cpu资源足够即可立即执行,与其他线程竞争cpu资源);阻塞状态(没有获取到锁,即没有cpu资源;或者没有被唤醒);

阻塞队列,就绪队列,cpu同一时间只能运行一个线程

 

线程5个阶段:创建,就绪,运行,阻塞,终止

start方法并不是立即执行线程代码,而是使线程进入可运行状态,具体什么时候运行是有CPU决定。

多线程代码执行顺序是不确定的,每次执行结果都随机

start()方法不能重复调用,若重复调用,会出现IllegalThreadStateException

继承Thread或实现Runnable接口会使类具有多线程特征,所有多线程的代码都在run()方法里面

Thread类也是实现了Runnable接口

不管以何种方式来实现多线程,最终还是通过Thread类的API来控制线程

Runnable接口比Thread类优势:

避免单继承限制;代码可以被多个线程共享,代码和数据独立;可以放入线程池中进行调用;适合多个相同的程序代码去处理同一资源。(比如微信公众号生成图片,多个地方会使用到)

在java中,每次程序运行至少启动两个线程,一个是main线程,一个是垃圾回收线程。

线程状态转化

 

1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
 

线程调度

1、线程拥有优先级(1-10):优先级高的线程会获得较多的机会运行,可以通过setPriority来进行设置,默认取值为5。优先级有继承关系,如果A线程中创建了B线程,那么B将和A具有相同的优先级。优先级在不同的操作系统中不好映射,建议使用默认常量作为优先级,这样保证同样的优先级采用了同样的调度方式。

2、线程睡眠:通过调用sleep使线程进入阻塞状态,可设定睡眠时间,当睡眠结束后,进入就绪状态

3、线程等待:wait()方法,导致当前线程等待,直到其他线程调用此对象的notify()或notifyAll()进行唤醒操作

4、线程让步:yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程

5、线程加入;join()方法。在当前线程中调用另一个线程的join()方法,则当前线程进入阻塞状态,直到另一个线程运行结束,当前线程再由阻塞转为就绪状态

6、线程唤醒:notify(),唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择其中一个线程。选择是任意性的

 

什么情况下使用join()?

如果子线程需要进行大量的耗时运算,主线程将于子线程之前结束。如果主线程需要用到子线程的结果,这个时候就需要join

wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。 
   如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。 
 

interrupt();并不是中断线程,只是向线程发送中断信号,改变线程状态为中断(不能中断运行中的线程,只是改变状态)

 

操作系统调度基本单位:进程【系统为进程分配资源,不对线程分配资源】
操作系统调度最小单位:线程
cpu调度基本单位:线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

wait、sleep、yield区别

都是用来暂停线程方法的,其中sleep、yield是定义在Thread类中,而wait 方法是定义在Object类中

wait是用于线程间通信,sleep是用于短时间暂停当前线程。当线程调用wait方法时,会释放它所持有的对象的资源和锁,而sleep不会释放(即还持有锁)。

wait()应该在同步代码块中调用

yield方法,使当前线程让出cpu占用权,不可能使较低较低优先权的线程获得cpu占有权,而sleep可以

yield仅仅释放线程所占有的cpu资源,进入执行队列,并不是等待状态,具体谁能获取到cpu资源完全取决于调度器。让当前线程进入可运行状态,以允许具有相同优先级的其他线程获得运行机会。有可能进入可执行状态后又马上被执行。

yield方法会临时暂停当前正在执行的线程,来让有同样优先级的正在等待的线程有机会执行。如果没有正在等待的线程,或者所有正在等待的线程的优先级都比较低,那么该线程会继续运行

Thread.sleep()是一个静态方法,作用在当前线程上;wait()是一个实例方法,并且只能在其他线程调用本实例的notify()方法时被唤醒。使用sleep()暂停的线程会在被唤醒之后立即进入就绪状态;使用wait()方法时,被暂停的线程唤醒后,会先获取锁(阻塞状态),然后再进入就绪状态。

Thread.yield(),作用在当前线程

线程安全

多个线程同时运行一段代码,要保证和单线程运行的结果一致,就是线程安全的。

public class Test {
  public static void main(String[] args) {
   MyRunnable r = new MyRunnable();
   Thread t1 = new Thread(r);
   Thread t2 = new Thread(r);
   Thread t3 = new Thread(r);
   t1.start();
   t2.start();
   t3.start(); 
  }
}
class MyRunnable implements Runnable{
  private int sum = 100;
  public void run(){
   while(true){
    if(sum>0){
     try {
      Thread.sleep(10);
     catch (InterruptedException e) {
      e.printStackTrace();
     }
     System.out.println(sum);
     sum--;
    }
   }
  }
}

如上代码,多个线程执行同一个任务,操作同一属性sum,对其进行自减并打印。运行结果出现了很多重复,且有-1,与预期结果不一致。为什么呢?主要是因为三个线程在执行过程中不断抢夺cpu执行权,当某一个线程通过Thread.sleep()进入睡眠状态时,cpu的执行权交由了两外两个线程,三个线程都运行到这里,面临的是输出sum,并执行sum--

当我们使用多线程访问同一资源的时候,且多个线程对资源有写的操作,就容易出现线程安全问题。

所以java提供了同步机制去解决

同步代码块

 synchronized(同步锁){ 
	//需要同步操作的代码
}

无论是否失去cpu的执行权,其他线程只能处于等待状态。

public class Test {
  public static void main(String[] args) {
   MyRunnable r = new MyRunnable();
   Thread t1 = new Thread(r);
   Thread t2 = new Thread(r);
   Thread t3 = new Thread(r);
   t1.start();
   t2.start();
  t3.start();
  }
}
class MyRunnable implements Runnable{
  private int sum = 100;
  Object obj = new Object();
  public void run(){
   while(true){
    synchronized(obj){
     if (sum > 0) {
      try {
       Thread.sleep(10);
      } catch (InterruptedException e) {
       e.printStackTrace();
      }
      System.out.println(sum);
      sum--;
     }
    }
   }
  }
}
public class Test {
 public static void main(String[] args) {
   MyRunnable r = new MyRunnable();
   Thread t1 = new Thread(r);
   Thread t2 = new Thread(r);
   Thread t3 = new Thread(r);
   t1.start();
   t2.start();
   t3.start();
 }
}
class MyRunnable implements Runnable {
  private int sum = 100;
  Object obj = new Object();
  public void run() {
   while (true) {
    show();
   }
  }
 public synchronized void show() {
  if (sum > 0) {
   try {
    Thread.sleep(10);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
   System.out.println(sum);
   sum--;
  }
 }
}

Lock锁

public class Test {
 public static void main(String[] args) {
   MyRunnable r = new MyRunnable();
   Thread t1 = new Thread(r);
   Thread t2 = new Thread(r);
   Thread t3 = new Thread(r);
   t1.start();
   t2.start();
   t3.start();
 }
}
class MyRunnable implements Runnable {
  private int sum = 100;
  Lock l = new ReentrantLock();
  public void run() {
   while (true) {
    l.lock();
    if (sum > 0) {
     try {
      Thread.sleep(10);
      System.out.println(sum);
      sum--;
     } catch (InterruptedException e) {
      e.printStackTrace();
     } finally {
      l.unlock();
    }
   }
  }
 }
}

计时等待状态

sleep使当前线程进行休眠

锁阻塞状态

比如多个线程执行前需获取锁,在获取锁的等待过程就属于锁阻塞状态

无限等待状态

调用wait()方法会进入无限等待状态,直到调用notify()方法,释放锁对象

等待唤醒机制

多线程之间的协作机制。比如A线程在触发条件后,调用wait()方法进入等待状态;其他线程执行完指定代码后在调用notify()方法将其唤醒;如果有多个线程在进行等待时,如需要,可以调用notifyAll()来唤醒所有等待的线程。

  • wait():线程不再参与调度,不去竞争锁,进入无限等待,直到被唤醒,进入调度队列
  • notify():把一个等待中的线程释放出来
  • notify():释放所有等待的线程

注意:

wait和notify都属于Object方法,所以锁对象可以是任意对象;必需在通过代码块中使用;必需用对象来调用方法,且是同一个对象;

 

线程类的一些常用方法: 

  sleep(): 强迫一个线程睡眠N毫秒。 
  isAlive(): 判断一个线程是否存活。 
  join(): 等待线程终止。 
  activeCount(): 程序中活跃的线程数。 
  enumerate(): 枚举程序中的线程。 
    currentThread(): 得到当前线程。 
  isDaemon(): 一个线程是否为守护线程。 
  setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) 
  setName(): 为线程设置一个名称。 
  wait(): 强迫一个线程等待。 
  notify(): 通知一个线程继续运行。 
  setPriority(): 设置一个线程的优先级。
 

Synchronized关键字的作用域

修饰方法(实例对象),修饰静态方法(类对象),方法中的某块(当前对象)

被Synchronized修饰的方法是不能被继承的

 

线程同步与异步

线程同步:多个线程同时访问同一资源,为保证线程安全,进行加锁处理。只有获取到锁的线程才能进行操作(使用同一个锁),例如同步代码块,必需要等到执行结束(获取到结果),才能进行下一步操作

单线程是不需要同步的

线程异步:调用者在没有得到结果前,就可以继续执行后续的操作。调用完成后,一般通过状态、通知或回调来通知调用者。调用的返回并不受调用者控制。

 

线程数据传递

1、声明变量,通过构造方法传递

2、set方法传递

3、通过回调函数

互斥锁,读写锁,乐观锁,悲观锁,公平锁,非公平锁,自旋锁

什么时候需要锁呢?

共享实例变量,共享连接资源

Lock

lock()获取锁,拿不到将一直阻塞,无视interrupt()方法,unlock()释放锁,tryLock()尝试获取锁,如果取得返回true,tryLock(time,单位)单位时间内获取锁

newCondition()条件状态

lockInterruptibly()获取锁,会响应interrupt

ReentrantLock可重入锁

构造函数,默认非公平锁。内部有一个Sync变量,其所有方法基本都是调用Sync方法,比如lock方法,调用的是sync.lock()

    public ReentrantLock() {
        sync = new NonfairSync(); //默认非公平锁
    }
 
    public ReentrantLock(boolean fair) {
        sync = (fair)? new FairSync() : new NonfairSync();//公平锁
    }

公平锁就是先等待的线程先获取锁;非公平锁是看操作系统的调度,有不确定性,所以性能好;

Sync,是ReentrantLock的内部类

ThreadFactory

线程工厂,用来创建线程,只有一个方法

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

实现ThreadFactory只有一个地方,Executors中的静态内部类DefaultThreadFactory,统一给线程池中的线程设置线程组,线程前缀名,优先级

ThreadLocal

在高并发场景下,如果只考虑线程安全,而不考虑延迟性,数据共享,那么就使用ThreadLocal

使用ThreadLoca维护变量时,会为每个使用该变量的线程提供独立的副本,这样线程之间互不影响

每个Thread中都有一个ThreadLocal.ThreadLocalMap。其中Key为ThreadLocal这个实例,value为每次initialValue()得到的变量

ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝

ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用ThreadLocal至关重要。

ThreadGroup

线程组,一旦一个线程归属到一个线程组中,就不可更改其线程组。线程组可以方便统一管理

Callable

public interface Callable<V> {
    V call() throws Exception;
}

FutureTask是Future的一个实现,并实现了Runnable,所以可以通过Executors来执行,也可以传递给Thread。如果在主线程中需要执行耗时操作,又不想阻塞主线程,可以把作业交给Future在后台完成操作。当主线程将来需要时,就可以通过Future对象获得后台作业的结果。Executors框架利用FutureTask来完成异步任务。(如果有返回结果,一定要注入一个Callable对象)

completionService
ExecutorService executorService = Executors.newCachedThreadPool();
        List<FutureTask<Integer>> result = new ArrayList<>();
        for(int i=0; i<10; i++) {
            result.add((FutureTask<Integer>) executorService.submit(new MyFuture(i)));
        }
        executorService.shutdown();

        for(int i=0; i<10; i++) {
            System.out.println(result.get(i).get());
        }

任务放入线程池中执行,需要获取返回结果,只能通过一个线程一个线程执行完,按顺序获取;

ExecutorService executorService2 = Executors.newCachedThreadPool();
        // 构建完成任务
        CompletionService<Integer> completionService = new ExecutorCompletionService<>(executorService2);
        for(int i=0; i<10; i++) {
            completionService.submit(new MyFuture(i));
        }
        executorService.shutdown();

        for(int i=0; i<10; i++) {
            System.out.println(completionService.take().get());
        }

对于CompletionService,构造参数是线程池

假设我们开了100个线程,放入到线程池中进行执行,而又需要返回结果。线程执行时间未知,我们只能通过不同遍历线程池中的线程来获取结果。有的线程执行完,而我们不知道。针对此问题,CompletionService诞生了。

它是将一组线程的执行结果放入到一个BlockingQueueing中,其顺序只和线程执行时间有关,与启动速度无关。

线程池

newCachedThreadPool();缓存线程池;有线程就复用,没有就新建线程并加入池中;通常用于执行时间短的异步任务;如果在一定时间内(默认60秒)没有任务执行,将会进行销毁;(线程可以被循环复用)

newFixedThreadPool();固定数量线程池;只有固定数量的活动线程存在;如果有新的线程要执行,将任务放入队列中进行等待,直到某个线程任务完毕进行调用;线程数量固定,不会被销毁;

SingleThreadPool();单线程池;只有一个线程;且不会被销毁;

ScheduledThreadPool();调度线程池

shutdown();任务都执行完才关闭线程池;

shutdownNow();舍弃任务,直接关闭;

 

 

 

 

 

 

 

 

 

原文:

https://mp.csdn.net/postedit

https://www.jianshu.com/p/b8197dd2934c

https://blog.csdn.net/chenruijia170707/article/details/78505351

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值