栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

5.【多线程面试篇】

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

5.【多线程面试篇】

线程基础 Q:什么是上下文切换?

首先CPU再同一个时刻只能执行一个线程(它会为每个线程分配CPU时间片,每个时间片非常短,让人理解为同事执行。)cpu通过分配时间片来循环执行任务,线程A执行一个时间片后,会切换到线程B(不一定是A结束),但是切换前会保存A线程的状态,以便下次切换回A。所以:线程A从保存到再次加载执行就是一次上下文切换。

例子:同时读两本书(肯定是换着读),当我们读一本英文书,发现某个单次不认识,于是打开词典查询,查询前肯定要标记现在读到哪里了,查完词典,接着读下面的内容,切换是影响读书效率,同样上下文切换也是影响多线程的执行速度的。

如何减少上下文切换?

无锁并发编程,CAS算法,使用最少线程和使用协程。

线程启动:

    怎么实现线程?

    实现Runnable接口和Thread类,还有线程池。核心调用run()方法

    一个线程调用两次start()方法?

    会抛出异常,因为会先检查线程的状态是不是初始化(0)
    if (threadStatus != 0)
                throw new IllegalThreadStateException();
    

    如何停止线程?

    停止线程只能用中断,interrupt方法中断线程,还有一种不推荐的stop方法,这种是过期作废的。

    线程的生命周期?

new(新建):调用start()方法之前

Runnable(运行):调用start()之后

Blocked(阻塞状态):加锁,synchronized,lock,解锁,锁释放掉,回到运行状态

Waiting(等待):wait(),join()方法,如何回到运行状态呢notify方法

Timed_Waiting(计时器等待):sleep(),wait(time),join(time)。notify也是回到运行状态

Terminated(终止状态):执行完毕

wait,join,notify,notifyAll方法

wait释放锁,线程进入等待(waiting)这里要区别sleep(计时器等待状态)join线程等待,此时的状态也是waiting,线程B中调用了线程A的join方法,A插队执行,B等A执行完成,才会执行。a.join,a插队,别人都要等a。notify唤醒正在等待的线程notifyAll唤醒所有等待的线程

线程的属性?

线程Id,线程名字,是否是守护线程(不要设置为守护线程,守护线程跟jvm有关,如果设置守护线程,线程结束,jvm也就结束了,会导致在一个操作中退出,没有执行完整的单元),线程优先级

volatile和synchronized

volatile具有线程可见性,禁止指令重排序,没有原子性,a++会出现错误数据

synchronized,具有原子性,禁止指令重排序,可见性,同步方法,也就是锁的意思。

happens-before原则

A在B之前,线程A操作在B之前执行

加锁:同一把锁的解锁操作必须在加锁之前执行对volatile变量的写入操作必须在对改变量的读操作之前。(写在读之前)线程启动原则:线程的所有操作必须在Thread.start()之后。(线程未开始,其他的都给我等着)结束原则:线程所有操作必须在线程结束之前执行。(就是不能让线程结束了,你还没干完)中断原则:对线程interrupt()方法的调用happens-before被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。传递性:A在B之前,B在C之前,那么A一定在C之前执行

死锁和活锁?

死锁就是两个或者两个以上的线程在执行过程中,互相等待的一种现象,你等我,我等你。

活锁就是:任务没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,重试,失败。

如何避免死锁:尽量不要几个功能公用同一把锁,设置超时时间,避免多次锁定

死锁的例子代码:

public class MustDead {
   public static String str1 = "str1";
   public static String str2 = "str2";
   public static void main(String[] args) {
      Thread a = new Thread(()->{
         synchronized (str1){
            System.out.println(Thread.currentThread().getName()+"锁住str1");
            try {
               Thread.sleep(1000);
               synchronized (str2){
                  System.out.println(Thread.currentThread().getName()+"锁住str2");
               }
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      });
      Thread b = new Thread(()->{
         synchronized (str2){
            System.out.println(Thread.currentThread().getName()+"锁住str1");
            try {
               Thread.sleep(1000);
               synchronized (str1){
                  System.out.println(Thread.currentThread().getName()+"锁住str2");
               }
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      });
      a.start();
      b.start();
   }
}
JUC包 什么是CAS?

乐观锁(每次不加锁,假设没有冲突就去完成某种操作,如果发生冲突失败就重试,直到成功为止)

用的机制就是CAS,比较并交换。cas操作包含3个操作数-----内存位置V,预期原值A,和新值B。如果V和A相等,那么处理器会自动将改位置的值更新为B。否则不做任何操作。

会出现ABA问题?cas需要在操作值的时候检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,后来变为B,又变成A,那么CAS进行检查时发生它的值咩有发生变化,但是实际上却变化了,ABA解决思路就是在变量面前加版本号,每次变量更新,版本号+1.

只能保证一个共享变量的原子性操作。

JUC包 什么是CAS?

乐观锁(每次不加锁,假设没有冲突就去完成某种操作,如果发生冲突失败就重试,直到成功为止)

用的机制就是CAS,比较并交换。cas操作包含3个操作数-----内存位置V,预期原值A,和新值B。如果V和A相等,那么处理器会自动将改位置的值更新为B。否则不做任何操作。

会出现ABA问题?cas需要在操作值的时候检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,后来变为B,又变成A,那么CAS进行检查时发生它的值咩有发生变化,但是实际上却变化了,ABA解决思路就是在变量面前加版本号,每次变量更新,版本号+1.

只能保证一个共享变量的原子性操作。

ThreadLocal

线程变量,每个线程有自己的变量。

常用方法:initialValue和set设置变量值,get获取该线程的变量值,remove删除值。

内存泄露:当某个对象不存在,但是占用的内存不能被回收。怎么避免呢,使用完ThreadLoca之后,应该调用remove方法。

Lock锁

注意:不要在获取锁的过程中写在try块中,如果获取锁发生了异常,也会导致锁无故释放。

四个方法获取锁

lock:获取锁,如果被其他线程获取,则进行等待。lock不会像synchronized一样发生异常自动释放锁。在finally中释放锁就好

tryLock():尝试获取锁,返回true代表没有被其他线程占用,获取锁成功,返回false获取锁失败。

tryLock(Long time,TimeUnit):超时就放弃。

lockInterruptibly(): 相当于trylock 把超时时间设置为无限,在等锁的过程中,线程可以被中断。

unlock() 解锁,一定在try finally 后加

ReentrantLock(true):公平锁,重入锁,

LockSupport:

park(阻塞,释放锁),unpark(唤醒),和synchronized的wait,notify,一样。但是可以不用锁,直接在线程里使用。

public static void main(String[] args) throws InterruptedException {
   Thread a= new Thread(new Runnable() {
      @Override
      public void run() {
         System.out.println(Thread.currentThread().getName()+"----------线程启动");
         LockSupport.park();
         System.out.println(Thread.currentThread().getName()+"-----线程结束");
      }
   },"A");

   Thread b = new Thread(new Runnable() {
      @Override
      public void run() {
         System.out.println(Thread.currentThread().getName()+"---------通知");
         LockSupport.unpark(a);

      }
   },"B");

   a.start();
   TimeUnit.SECONDS.sleep(1);
   b.start();
}

源码解析:unpark生产许可证,最多只有一个。多次调用unpark也就只有1个。park是消耗许可证,调用park先尝试是否拿到许可证,如果_counter>0,不阻塞,继续执行代码,消耗一个许可证,_counter=0,并返回,下个线程进来会阻塞。

注意:不要在获取锁的过程中写在try块中,如果获取锁发生了异常,也会导致锁无故释放。

并发工具类(流程控制)

CountDownLatch:

自我理解:坐大巴车,人满了才发车,拼团,人满了才发货

流程:倒数结束之前,一直处于等待状态,如果倒计时结束了,此线程才能继续工作。

count=0才会执行,作用域事件

源码:

//参数count为计数值
public CountDownLatch(int count) {  };  
//调用await()方法的线程会被挂起,它会等待count=0才会继续执行
public void await() throws InterruptedException { };  
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1
public void countDown() { };

需求代码:模拟100m跑路,5名选手+1名裁判,裁判发令,选手开始跑步,选手全部跑完,裁判宣布结束

public class CounDownLatchDemo {
   
   public static void main(String[] args) throws InterruptedException {
      CountDownLatch judge = new CountDownLatch(1);//1裁判
      CountDownLatch runner = new CountDownLatch(5); //5名运动员
      ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,0L,
            TimeUnit.SECONDS,new SynchronousQueue<>());

      for (int i = 0; i < 5; i++) {
         final int no = i+1;
         Runnable runnable = new Runnable() {
            @Override
            public void run() {
               try {
                  System.out.println("NO."+no+"准备完毕,等待发令");
                  judge.await();
                  System.out.println("No."+ no + "开始跑步");
                  Thread.sleep((long)Math.random()*1000);
                  System.out.println("No."+ no + "跑完了");
               } catch (InterruptedException e) {
                  e.printStackTrace();
               }finally {
                  runner.countDown();
               }
            }
         };
         executor.submit(runnable);
      }
      Thread.sleep(1000);//裁判做准备
      System.out.println("发令枪响,比赛开始");
      judge.countDown(); //裁判
      runner.await();//运动员
      System.out.println("所有人到达终点,比赛结束");
      executor.shutdown();

   }
}

CyclicBarrier:循环栅栏

个人理解:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达,屏障才会开门。

和CountDownLatch的区别,它作用于线程,等固定的线程(比如5个人坐车,全到了才会发车)到达栅栏位置才能继续执行。它的计时器可以重置。

代码:需求,5个线程都到齐了,才会执行发车指令

public class CyclicBarrierDemo {
   public static void main(String[] args) {
      //5个线程都来到这个点,才会执行逻辑
      CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
         @Override
         public void run() {
            System.out.println("所有人都到场了,大家统一出发--------");
         }
      });
      ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,0L, TimeUnit.SECONDS,new SynchronousQueue<>());
      for (int i = 0; i < 5; i++) {
         executor.submit(new Task(i,cyclicBarrier));
      }
   }
   static class Task implements Runnable{
      private int i;
      private CyclicBarrier cyclicBarrier;

      public Task(int i, CyclicBarrier cyclicBarrier) {
         this.i = i;
         this.cyclicBarrier = cyclicBarrier;
      }
      @Override
      public void run() {
         try {
            System.out.println("NO"+i+"来到集合点,等待他人");
            cyclicBarrier.await();
         } catch (InterruptedException e) {
            e.printStackTrace();
         } catch (BrokenBarrierException e) {
            e.printStackTrace();
         }
      }
   }
}

Semaphore:信号量,控制线程访问个数。

个人理解:是用来做流程控制的,比如有100个线程访问服务,我们限制只能有3个线程进来,那就使用acquire(3),剩下的就不能访问,这3个线程执行完毕,调用release释放,其他的线程就可以访问了

代码案例:10个线程,3个线程执行,后面的等着

public class SemaphoreDemo {
   public static void main(String[] args) {
      // 这里的布尔值是否要使用公平策略。如果为true,就会把等待的线程放入到队列中。
      Semaphore semaphore = new Semaphore(3,true);
      ExecutorService executorService = Executors.newFixedThreadPool(10);
      for (int i = 0; i < 20; i++) {
         executorService.submit(()->{
            try {
               // 可以响应中断,拿到许可证
               semaphore.acquire();
               System.out.println(Thread.currentThread().getName()+"拿到了许可证");
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
            try {
               Thread.sleep(1000);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"释放了许可证");
            // 归还许可证
            semaphore.release();
         });
      }
      executorService.shutdown();
   }
}

Condition接口(条件对象)

定义了等待/通知两种类型的方法。调用这些方法要提前获取Condition对象关联的锁Lock。

Synchronized里的wait,notify,但是Lock锁里是要用Condition的await(等待),signal(唤醒)。

原子操作类

AtomicInteger:原子更新整型。内部原理CAS

compareAndSet(int expect,int update) 如果当前的数值等于预期值,则以原子方式将该值设置为输入值(update)

private static final AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.set(2000)
atomicInteger.compareAndSet(2000,600);
System.out.println(atomicInteger.get());  //输出600
线程池
new ThreadPoolExecutor(5,10,0L, TimeUnit.SECONDS,new SynchronousQueue<>(),threadFactory);
解析:new ThreadPoolExecutor(核心线程数,最大线程数,线程活动保持时间,时间单位,任务队列,用于创建线程的工厂);

线程池的实现原理:

如果当前运行的线程少于corePoolSize,则创建新的线程来执行任务(必须获取全局锁)如果运行的线程>=corePoolSize,则将任务加入BlockingQueue(任务队列)。如果任务无法加入任务队列(队列满了),则创建新的线程来处理任务。如果创建的新线程将当前运行的线程超出maxPoolSize(最大线程数),任务将被拒绝。交给饱和策略来处理这个任务。
-流程图

向线程池提交任务

execute():不提供返回值

submit()方法:提供返回值Future对象,通过get()方法会阻塞当前线程直到任务完成。

关闭线程池:shutdown和shutdownNow,原理就是遍历线程池中的工作先吃,然后逐个调用线程的interrupt方法来中断线程。无法响应中断的任务可能无法停止。

Executor框架

有三个组成部分:1.任务(Runnable和Callable接口),2.任务的执行(Executor,和它的继承接口。),3.任务的结果(Future和实现Future接口的类)

Future接口:获取线程结果

get()方法:获取结果,但是会阻塞,会一直等任务执行完毕才返回

isDone:表示任务已经完成,若完成返回true

Callable接口

类似于Runnable,有call方法,有返回值,可以抛出异常。

FutureTask

类似于Runnable,有返回值。有get方法,也会阻塞

启动线程

public static void main(String[] args) throws ExecutionException, InterruptedException {
   new Thread(new FutureTask(new Callable() {
      @Override
      public String call() throws Exception {
         return "sss";
      }
   })).start();
}

ThreadPoolExecutor:创建线程池的主要类

FixedThreadPool:可重用的固定长度的线程池

Executor executors = Executors.newFixedThreadPool(5);

SingleThreadExecutor:单线程线程池,核心线程=1,最大线程=1

CachedThreadPool:核心线程=0,最大线程=int的最大值

ScheduledThreadPoolExecutor:定时器线程池

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10); executorService.schedule(new Task(),5, TimeUnit.SECONDS);//task任务延时5s executorService.scheduleAtFixedRate(new Task(),1,3,TimeUnit.SECONDS);//task任务初始1s,3s不停的重复
阻塞队列BlockingQueue

首先它是一个队列,其次有阻塞的功能。

主要的类:

ArrayBlockingQueue:数组实现的有界阻塞队列,先进先出对元素排序linkedBlockingQueue:链表实现的有界阻塞队列,先进先出,默认和最大长度为Int的最大值SynchronousQueue:不存储元素的阻塞队列,每个月put操作必须等待一个take操作,否则不能添加元素。

主要的方法put,take:

put—直接队列中插入元素,当无可用空间,阻塞等待take–获取并移除头部元素,无元素时,阻塞等待。

add,remove,element方法:

add—立即将元素插入此队列,成功返回true,无可用空间,抛出异常。remove—移除并返回队头,队列为空,抛出异常element—在不移除的情况下返回队头,当队头为空,抛出异常

offer,poll,peek

offer—向队列添加一个元素poll—移除并且返回队头,在队列为空返回nullpeek—不移除的情况下返回队头,队头为空返回null 什么是AQS?

AQS英文AbustactQueuedSynchronizer的简称,中文意思,抽象队列同步(抽象+队列+同步-锁),它是一个用来构建锁和同步器的框架。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/769802.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号