面试宝典-【并发】

目录

1.说说什么是进程和线程?

2.说一说协程?

3.什么是线程的共享内存? 

4.说一下并行和并发?

5.说一下串行? 

6.什么是线程上下文切换?

7.说一下创建线程的方式? 

8.线程常用的方法?

9.start()方法和run()方法的区别?

10.调用 start()方法时会执行 run()方法,那怎么不直接调用 run()方法?

11.守护线程是什么?

12.线程有几种状态?

13.什么是线程池?

14.说一下常见的线程池?

15.线程池怎么关闭知道吗?

16.线程池提交 execute 和 submit 有什么区别?

17.简单说一下线程池的工作原理?

18.说一下线程池主要参数有哪些?

19.说一下拒绝策略?

20.说一下常见的阻塞队列? 

21.说一下线程池的状态?

22.线程池中执行任务的线程异常?

23.说一下原子性、可见性、有序性?

24.对java内存模型了解多少?

25.那说说as-if-serial 是什么?

26.什么是指令重排? 

27.ThreadLocal 是什么?

28. ThreadLocal底层实现原理?

29.ThreadLocal常用API?

30.Thread, ThreadLocal, ThreadLocalMap关系? 

31.ThreadLocalMap 怎么解决 Hash 冲突的?

32.说一下ThreadLocalMap 扩容机制?

33.说一下强/软/弱/虚引用?

34.说一下ThreadLocal 内存泄露?

35.如何解决 ThreadLocal 内存泄露?

36.父子线程怎么共享数据?

37. volatile关键字的两个作用?

38. volatile关键字底层原理?

39. 了解缓存一致性协议(MESI)吗?

40.原子类了解过吗?

41.CAS是什么?

42.锁的分类?

43.synchronized 的刨析?

44.synchronized的使用及其原理?

45.为什么任何一个对象都可以成为一个锁?

46.说说 synchronized 和 ReentrantLock 的区别?

47.线程死锁了解吗?

48.那死锁问题怎么排查呢?

49.CAS存在的问题?

50.wait和sleep的区别? 


1.说说什么是进程和线程?

  • 进程:操作系统资源分配的最小单位,简单点就是我们在电脑上启动的一个个应用,比如我们启动一个浏览器,就会启动了一个浏览器进程。
  • 线程:线程是比进程更小的执行单位,它是在一个进程中独立的控制流,一个进程可以启动多个线程,每条线程并行执行不同的任务

一个进程可以包含多个线程,每个线程都可以独立执行不同的任务,多个线程共用进程的堆和方法区,但是每个线程都会有自己的程序计数器

2.说一说协程?

  • 协程通常被视为比线程更轻量级的并发单元,它们主要在一些支持异步编程模型的语言中得到了原生支持 
  • 协程可以在一个线程内部创建多个协程,这些协程之间可以共享同一个线程的资源

3.什么是线程的共享内存? 

  • 线程之间想要进行通信,可以通过消息传递共享内存两种方法来完成,Java 采用的是共享内存的并发模型。
  • 这个模型被称为 Java 内存模型,也就是 JMM,JMM 决定了一个线程对共享变量的写入何时对另外一个线程可见

  • 线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了共享变量的副本

4.说一下并行和并发?

  • 并行:多个处理器同时执行多个任务每个核心实际上可以在同一时间独立地执行不同的任务。 
  • 并发:系统有处理多个任务的能力,但是任意时刻只有一个任务在执行
    • 单核处理器上,多个任务是通过时间片轮转的方式实现的。这种切换非常快,给人感觉是在同时执行。

5.说一下串行? 

  • 串行同一时刻只能运行一个程序,如果存在多个程序,需要按照先后顺序执行。

6.什么是线程上下文切换?

  • 让用户感觉多个线程是在同时执行的,CPU 资源的分配采用了时间片轮转也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换。

7.说一下创建线程的方式? 

创建线程有四种方式:继承 Thread 类实现 Runnable 接口实现 Callable 接口线程池

  • 继承 Thread 类,重写run方法,调start()开启线程
    •  缺点:java只能单继承,所以如果类已经继承了另一个类,就不能使用这种方法了。
public class MyThread extends Thread {
    public static void main(String[] args) {

        MyThread main = new MyThread();
        main.start();
    }

    @Override
    public void run() {
        System.out.println("新线程");
    }
  • 实现 Runnable 接口,重写run方法,调start()开启线程
    • 优点:避免 Java 的单继承限制,并且更符合面向对象的编程思想
public class MyRunnable implements Runnable {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }

    @Override
    public void run() {
        System.out.println("新线程");
    }
}
  • 实现 Callable 接口,重写 call() 方法,创建 FutureTask 对象,参数为 Callable 对象,创建 Thread 对象,参数为 FutureTask 对象,调用 start() 方法启动线程。
    • 优点:可以获取线程的执行结果。
public class MyCallable implements Callable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask task = new FutureTask<>(myCallable);
        Thread thread = new Thread(task);
        thread.start();
        System.out.println(task.get());
    }

    @Override
    public String call() throws Exception {
        return "新线程";
    }
  • 线程池创建,向线程池提交任务(Runnable或Callable对象),线程池会自动创建线程来执行任务,并管理线程的复用和销毁。
public class MyExecutor implements Runnable{

    public static void main(String[] args) {
        //创建对象实例
        MyExecutor myExecutor = new MyExecutor();
        //获取ExecutorService实例,生产禁用,需要手动创建线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //提交任务
        executorService.submit(myExecutor);
    }
    @Override
    public void run() {
        System.out.println("线程池");
    }
}

8.线程常用的方法?

  • start:启动新的线程。
  • getPriority:获取线程优先级,线程默认优先级为5。
  • setPriority:设置线程优先级
  • interrupt:告诉线程,应该中断了,具体到底中断还是继续运行,由被通知的线程自己处理。interrupt() 并不能真正的中断线程
    • 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。

    • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true。不过,被设置中断标志的线程可以继续正常运行,不受影响。

  • interrupted:获取打断标记,并清除打断

  • join:等待其他线程终止。在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻塞状态,继续执行。
  • yield:Thread 类中的静态方法,使当前线程请求让出自己的 CPU使用权。
  • 否让出CPU资源,还是由调度器决定。
  • sleep:Thread 类中的静态方法,使当前线程让出指定时间cpu的使用权。 
  • wait:当一个线程 A 调用一个共享变量的 wait() 方法时,线程 A 会被阻塞挂起,直到发生下面几种情况才会返回 :
    • 线程 B 调用了共享对象 notify()或者 notifyAll() 方法;
    • 其他线程调用了线程 A 的 interrupt() 方法,线程 A 抛出 InterruptedException 异常返回。
  • notify:一个线程 A 调用共享对象的 notify() 方法后,会唤醒一个在这个共享变量上调用 wait 系列方法后被挂起的线程。
  • notifyAll::不同于在共享变量上调用 notify() 方法会唤醒被阻塞到该共享变量上的一个线程,notifyAll 方法会唤醒所有在该共享变量上调用 wait 系列方法而被挂起的线程。

9.start()方法和run()方法的区别?

  • run():
    • 同步的方式执行,不启动新的线程,run方法可以被执行无数次
  • start():
    • 异步的方式执行,启动新的线程,star方法只能被执行一次

10.调用 start()方法时会执行 run()方法,那怎么不直接调用 run()方法?

  • 调用start()方法时,会启动一个新的线程,并让这个新线程调用run()方法
  • 如果直接调用run()方法,那么run()方法就在当前线程中运行没有新的线程被创建,也就没有实现多线程的效果。

start() 方法的调用会告诉 JVM 准备好所有必要的新线程结构,分配其所需资源,并调用线程的 run() 方法在这个新线程中执行。 

11.守护线程是什么?

  • Java 中的线程分为两类,分别为 daemon 线程(守护线程) user 线程(用户线程)
  • 守护线程:是运行在后台的一种特殊进程
  • 守护线程和用户线程有什么区别:
    • 当最后一个非守护线程束时, JVM 会正常退出,而不管当前是否存在守护线程,也就是说护线程是否结束并不影响 JVM 退出。换而言之,只要有一个用户线程还没结束,正常情况下 JVM 就不会退出

12.线程有几种状态?

在 Java 中,线程共有六种状态:

  • 1.NEW:初始状态:线程被创建,但还没有调用 start()方法
  • 2.RUNNABLE:运行状态:Java 线程将操作系统中的就绪和运行两种状态笼统的称作“运行”
  • 3.BLOCKED:阻塞状态:表示线程阻塞于锁
  • 4.WAITING:等待状态:表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
  • 5.TIME_WAITING:超时等待状态:该状态不同于 WAITIND,它是可以在指定的时间自行返回的
  • 6.TERMINATED: 终止状态:表示当前线程已经执行完毕

13.什么是线程池?

  • 线程池,简单来说,就是一个管理线程的池子。
  • 好处:
    • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    • 提高响应速度。当任务到达时,可以不需要等到线程创建就能立即执行。
    • 提高线程的可管理性。统一管理线程,避免系统创建大量同类线程而导致消耗完内存。

14.说一下常见的线程池?

  • newFixedThreadPool (固定数目线程的线程池)

    • 核心线程数和最大线程数大小一样

    • 没有所谓的非空闲时间,即 keepAliveTime 为 0
    • 阻塞队列为无界队列 LinkedBlockingQueue,可能会导致 OOM
  • newCachedThreadPool (可缓存线程的线程池)

    • 最大线程数为 Integer.MAX_VALUE,也有 OOM 的风险

    • 阻塞队列是 DelayedWorkQueue
    • keepAliveTime 为 0
    • scheduleAtFixedRate() :按某种速率周期执行
    • scheduleWithFixedDelay():在某个延迟后执行
  • newSingleThreadExecutor (单线程的线程池)

    • ​​​核心线程数为 1

    • 最大线程数也为 1
    • 阻塞队列是无界队列 LinkedBlockingQueue,可能会导致 OOM
    • keepAliveTime 为 0
  • newScheduledThreadPool (定时及周期执行的线程池)

    • ​​​​​​核心线程数为 0

    • 最大线程数为 Integer.MAX_VALUE,即无限大,可能会因为无限创建线程,导致 OOM
    • 阻塞队列是 SynchronousQueue
    • 非核心线程空闲存活时间为 60 秒

15.线程池怎么关闭知道吗?

关闭线程池的两种方法:shutdownshutdownNow 

  • shutdown():
    • shutdown() 将线程池状态置为 shutdown,并不会立即停止
      • 1.停止接收外部 submit 的任务
      • 2.内部正在跑的任务和队列里等待的任务,会执行完
      • 3.等到第二步完成后,才真正停止
    • shutdown()只是关闭了提交通道,用 submit()是无效的;而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池。
  • shutdownNow():
    • shutdownNow() 将线程池状态置为 stop。一般会立即停止,事实上不一定
      • 1.和 shutdown()一样,先停止接收外部提交的任务
      • 2.忽略队列里等待的任务
      • 3.尝试将正在跑的任务 interrupt 中断
      • 4.返回未执行的任务列表
    • shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。
//判断线程池的状态, SHUTDOWN ,再看队列是否为空,在为null;STOP,直接返回null
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

16.线程池提交 execute 和 submit 有什么区别?

  • execute:
    • 返回值:只能提交Runnable类型的任务,无返回值
    • 异常:execute遇到异常会直接抛出,在子线程中抛出异常,在主线程捕捉不到
    • 接口:execute所属顶层接口是Executor,实现类ThreadPoolExecutor重写了execute方法
  • submit:
    • 返回值:submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null
    • 异常:submit遇到异常不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常
    • 接口:submit所属顶层接口是ExecutorService,抽象类AbstractExecutorService重写了submit方法

17.简单说一下线程池的工作原理?

  • 1.创建线程池。
  • 2.调用线程池的 execute()方法,提交任务
    • 如果正在运行的线程数量小于 核心线程数,那么线程池会创建一个新的线程来执行这个任务;
    • 如果正在运行的线程数量大于或等于 核心线程数,线程池会将这个任务放入等待队列
    • 如果等待队列满了而且正在运行的线程数量小于最大线程数,那么线程池会创建新的线程来执行这个任务
    • 如果等待队列满了而且正在运行的线程数量大于或等于最大线程数,那么线程池会执行拒绝策略
  • 3.线程执行完毕后线程并不会立即销毁,而是继续保持在池中等待下一个任务。
  • 4.当线程空闲时间超出指定时间且当前线程数量大于核心线程数时,线程会被回收。

注意:在没达到核心线程数时,都会优先创建线程,而不是复用。

18.说一下线程池主要参数有哪些?

线程池有 7 个参数:

  • corePoolSize:核心线程数
    • 即使这些线程处于空闲状态,它们也不会被回收。这是线程池保持在等待状态下的线程数
  • maximumPoolSize:最大线程数
    • 线程池允许的最大线程数量。当工作队列满了之后,线程池会创建新线程来处理任务直到线程数达到这个最大值
  • keepAliveTime:非核心线程存活时间
    • 如果线程池中的线程数量超过了 corePoolSize,那么这些多余的线程空闲时间超过 keepAliveTime 时会被终止
    • 超过核心线程后且线程都空闲,则阻塞keepAliveTime后发现任没有任务要执行,则这些线程会争抢消亡的机会,直到总的线程数小于核心线程数~
    • //所有线程进行争抢,抢到了,就置为null
      if (compareAndDecrementWorkerCount(c))
             return null; //返回null,线程就会销毁
  • handler:饱和拒绝策略
    • 定义了当线程池和工作队列都满了之后对新提交的任务的处理策略
  • workQueud:等待队列
    • 用于存放待处理任务的阻塞队列。当所有核心线程都忙时,新任务会被放在这个队列里等待执行。
  • threadFactory:创建线程的工厂
    • 它用于创建线程池中的线程。可以通过自定义 ThreadFactory 来给线程池中的线程设置有意义的名字,或设置优先级等。
  • unit:非核心线程存活时间单位
    • keepAliveTime 参数的时间单位:

19.说一下拒绝策略?

  • 1.AbortPolicy :默认拒绝策略,该策略会抛出一个 RejectedExecutionException 异常
  • 2.CallerRunsPolicy:该策略不会抛出异常,而是会让提交任务的线程(即调用 execute 方法的线程)自己来执行这个任务
  • 3.DiscardOldestPolicy:策略会丢弃队列中最老的一个任务(即队列中等待最久的任务),然后尝试重新提交被拒绝的任务
  • 4.DiscardPolicy:策略会默默地丢弃被拒绝的任务,不做任何处理也不抛出异常

如果想实现自己的拒绝策略,实现 RejectedExecutionHandler 接口即可。

20.说一下常见的阻塞队列? 

  • 1.ArrayBlockingQueue:一个有界先进先出的阻塞队列,底层是一个数组,适合固定大小的线程池。
  • 2.LinkedBlockingQueue:底层是一个是链表,默认大小是 Integer.MAX_VALUE,相当于一个无界队列
  • 3.PriorityBlockingQueue:一个支持优先级排序无界阻塞队列。任务按照其自然顺序或通过构造器给定的 Comparator 来排序。
  • 4.DelayQueue:类似于 PriorityBlockingQueue,由二叉堆实现的无界优先级阻塞队列
  • 5.SynchronousQueue:实际上它不是一个真正的队列,因为没有容量。每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都必须等待另一个线程的插入操作。

21.说一下线程池的状态?

  • 1.RUNNING
    • 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
    • 调用线程池的 shutdown()方法,可以切换到 SHUTDOWN 状态;
    • 调用线程池的 shutdownNow()方法,可以切换到 STOP 状态;
  • 2.SHUTDOWN
    • 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
    • 队列为空,并且线程池中执行的任务也为空,进入 TIDYING 状态;
  • 3.STOP
    • 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
    • 线程池中执行的任务为空,进入 TIDYING 状态;
  • 4.TIDYING
    • 该状态表明所有的任务已经运行终止,记录的任务数量为 0。
    • terminated()执行完毕,进入 TERMINATED 状态
  • 5.TERMINATED 
    • 该状态表示线程池彻底终止

22.线程池中执行任务的线程异常?

  • 执行任务的时候线程出现异常时,会消亡,但在消亡前会创建一个新的线程,为了保持有指定的核心线程数
  • 详解:在使用线程池处理任务的时候,任务代码可能抛出 RuntimeException,抛出异常后,线程池可能捕获它,也可能创建一个新的线程来代替异常的线程,我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。

23.说一下原子性、可见性、有序性?

  • 1.原子性:
    • 原子性指的是一个操作是不可分割、不可中断的,要么全部执行并且执行的过程不会被任何因素打断,要么就全不执行。
    • 如果要保证一个代码块的原子性,需要使用synchronized
  • 2.可见性:
    • 可见性指的是一个线程修改了某一个共享变量的值时,其它线程能够立即知道这个修改
    • Java 是利用volatile关键字来保证可见性的,finalsynchronized也能保证可见性。
  • 3.有序性: 
    • 有序性指的是对于一个线程的执行代码,从前往后依次执行,单线程下可以认为程序是有序的,但是并发时有可能会发生指令重排。
    • synchronized或者volatile都可以保证多线程之间操作的有序性。

24.对java内存模型了解多少?

  • 1.Java 内存模型(Java Memory Model)是一种抽象的模型,简称 JMM
  • 2.线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)本地内存中存储了共享变量的副本,用来进行线程内部的读写操作。 
    • 当一个线程更改了本地内存中共享变量的副本后,它需要将这些更改刷新到主内存中,以确保其他线程可以看到这些更改。
    • 当一个线程需要读取共享变量时,它可能首先从本地内存中读取。如果本地内存中的副本没有,线程将从主内存中重新加载共享变量的最新值到本地内存中。

25.那说说as-if-serial 是什么?

  •  不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变
  • 为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。

26.什么是指令重排? 

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序

重排序分 3 种类型

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

27.ThreadLocal 是什么?

  • ThreadLocal是Java中提供的一种用于实现线程局部变量的工具类。
  • 它允许每个线程都拥有自己的独立副本,从而实现线程隔离,用于解决多线程中共享对象的线程安全问题。 

28. ThreadLocal底层实现原理?

  • ThreadLocal 本身并不存储任何值,它只是作为一个映射,来映射线程的局部变量。当一个线程调用 ThreadLocal 的 set 或 get 方法时,实际上是访问线程自己的 ThreadLocal.ThreadLocalMap。
  • ThreadLocal底层是通过ThreadLocalMap来实现的
  • ThreadLocal类中都存在一个静态的ThreadLocalMap, Map的keyThreadLocal对象, Map的value需要缓存的值

29.ThreadLocal常用API?

  • 1.get ( ):返回当前线程的此线程局部变量副本中的值。
  • 2.initialValue():返回此线程局部变量的当前线程的“初始值”。
  • 3.remove ():删除此线程局部变量的当前线程值。
  • 4.set():将此线程局部变量的当前线程副本设置为指定值。
  • 5.withInitia():创建一个线程局部变量。

30.Thread, ThreadLocal, ThreadLocalMap关系? 

  • 每个Thread都会有一个ThreadLocal,每个ThreadLocal都会有一个静态的ThreadLocalMap
  • 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放

ThreadLocalMap

  • ThreadLocalMap 结构和 HashMap 比较类似的,主要关注的是两个要素:元素数组散列方法
    • 元素数组一个 table 数组,存储 Entry 类型的元素,Entry 是 ThreaLocal 弱引用作为 key,Object 作为 value 的结构。
    • 散列方法散列方法就是怎么把对应的 key 映射到 table 数组的相应下标,ThreadLocalMap 用的是哈希取余法取出 key 的 threadLocalHashCode,然后和 table 数组长度减一&运算(相当于取余)

31.ThreadLocalMap 怎么解决 Hash 冲突的?

  • ThreadLocalMap 它用的是——开放定址法这个坑被人占了,那就接着去找空着的坑。
  • 在 get 的时候,也会根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置,然后判断该槽位 Entry 对象中的 key 是否和 get 的 key 一致,如果不一致,就判断下一个位置

32.说一下ThreadLocalMap 扩容机制?

  • 1.ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后未清理到任何数据且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑: 
if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
  • 2.再着看 rehash()具体实现:这里会先去清理过期的 Entry,然后还要根据条件判断size >= threshold - threshold / 4 也就是size >= threshold* 3/4来决定是否需要扩容。
 private void rehash() {
            //清理过期的ENtry
            expungeStaleEntries();

            // 扩容
            if (size >= threshold - threshold / 4)
                resize();
        }
  • 3.开始扩容resize():扩容后的newTab的大小为老数组的两倍然后遍历老的 table 数组,散列方法重新计算位置开放地址解决冲突,然后放到新的newTab,遍历完成之后,oldTab中所有的entry数据都已经放入到newTab中了,然后 table 引用指向newTab

33.说一下强/软/弱/虚引用?

  • 强引用:
    • 编程中,通过new关键字创建的对象引用即为强引用。
    • 如果一个对象具有强引用,那么垃圾回收器(GC)绝不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会回收具有强引用的对象。
  • 软引用:
    • 软引用用于描述一些非必需但仍有用的对象。
    • 如果内存空间足够,垃圾回收器就不会回收软引用所指向的对象;但如果内存空间不足,就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存。
  • 弱引用:
    • 弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。
    • 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。弱引用可用于解决循环引用的问题。
  • 虚引用: 
    • 虚引用也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个。
    • 虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,随时可能会被回收。虚引用主要用来跟踪对象被垃圾回收的活动。
  • 强度强引用 > 软引用 > 弱引用 > 虚引用
  • 回收时机:强引用不会被回收,软引用在内存不足时会被回收;弱引用在任何时候都可能被回收;虚引用不决定对象的生命周期。

34.说一下ThreadLocal 内存泄露?

  • 随着线程 Thread 的结束,其内部的 ThreadLocalMap 也会被回收,从而避免了内存泄漏。
  • 如果一个线程一直在运行,并且其 ThreadLocalMap 中的 Entry.value 一直指向某个强引用对象,那么这个对象就不会被回收,从而导致内存泄漏。
  • 存储在ThreadLocal中的value是强引用,不会被垃圾回收掉

35.如何解决 ThreadLocal 内存泄露?

  • 使用完 ThreadLocal 后,及时调用 remove() 方法释放内存空间。
    • remove() 方法会将当前线程的 ThreadLocalMap 中的所有 key 为 null 的 Entry 全部清除,这样就能避免内存泄漏问题。
try {
    threadLocal.set(value);
    // 执行业务操作
} finally {
    threadLocal.remove(); // 确保能够执行清理
}

36.父子线程怎么共享数据?

  • 在主线程的 InheritableThreadLocal 实例设置值,在子线程中就可以拿到
  • InheritableThreadLocal是ThreadLocal子类, 

37. volatile关键字的两个作用?

  1. 保证变量的内存可见性
  2. 禁止进行指令重排序

38. volatile关键字底层原理?

  • 1.当线程对 volatile 变量进行写操作时,会强制将本地内存中的变量值刷新到主内存中
  • 2.由于缓存一致性协议,每个处理器通过嗅探在总线上传播的数据检查自己的缓存是不是过期了
    • 当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行置为无效状态
    • 当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中

39. 了解缓存一致性协议(MESI)吗?

  • 多个cpu从主内存读取同一个数据各自的高速缓存,当其中某个cpu修改了缓存里的数据该数据会马上同步回主内存其它cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效

40.原子类了解过吗?

  • 使用原子的方式更新基本类型:
    • AtomicInteger:整型原子类
    • AtomicLong:长整型原子类
    • AtomicBoolean :布尔型原子类
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值
  • 使用原子的方式更新数组里的某个元素:
    • AtomicIntegerArray:整形数组原子类
    • AtomicLongArray:长整形数组原子类
    • AtomicReferenceArray :引用类型数组原子类

        

public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
  • 引用类型原子类:
    • AtomicReference:原子更新引用类型。

    • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

    • AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。

41.CAS是什么?

  • 1.CAS(Compare-and-Swap)是一种乐观锁的实现方式,全称为“比较并交换”,是一种无锁的原子操作。 
  • CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。
  • 2.我们可以使用 synchronized关键字和CAS来实现加锁效果。
  • 3.CAS 是乐观锁,线程执行的时候不会加锁,它会假设此时没有冲突,然后完成某项操作;如果因为冲突失败了就重试,直到成功为止。
  • CAS算法涉及到三个操作数:
    • V:要更新的变量(var)
    • E:预期值(expected)
    • N:新值(new)
  • 只有当V的值等于E时,才会使用原子方式用新值N来更新V的值,否则会继续重试直到成功更新值。
    • 这个比较和替换的操作是原子的,即不可中断,确保了数据的一致性。

42.锁的分类?

  • 1.悲观锁与乐观锁 
    • 悲观锁每次访问资源都会加锁,执行完同步代码释放锁
      • synchronizedReentrantLock属于悲观锁。
      • 悲观锁适合写操作多的场景
    • 乐观锁:不会锁定资源,所有的线程都能访问并修改同一个资源,如果没有冲突就修改成功并退出,否则就会继续循环尝试
      • 乐观锁最常见的实现就是CAS
      • 乐观锁适合读操作多的场景,不加锁可以提升读操作的性能
  • 2.公平锁与非公平锁
    • 公平锁是指多个线程按照申请锁的顺序来获取锁
      • Lock lock = new ReentrantLock(true);//true表示公平锁,先来先得
    • 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,
      • Lock lock = new ReentrantLock(false);//false表示非公平锁,后来的也可能先获得锁
    • synchronized是非公平锁,Lock默认是非公平锁,可以设置为公平锁,公平锁会影响性能。
  • 3.共享式与排他锁
    • 排他锁:排它锁又称独占锁,获得了以后既能读又能写,其他没有获得锁的线程不能读也不能写)典型的tynchronized就是排它锁
    • 共享锁:共享锁又称读锁,获得了共享锁以后可以查看但无法修改和删除数据,其他线程也能获得共享锁,也可以查看但不能修改和删除数据
  • 4.可重入锁
    • 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
    • ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
    • 隐式锁:(即synchronized关键字使用的锁)默认是可重入锁
    • 显示锁:(即Lock)也有ReentrantLock这样的可重入锁。

43.synchronized 的刨析?

  • 类锁与对象的锁~
  • 1.一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内只能有唯一的一个线程去访问这些synchronized方法锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
  • 2.加个普通方法后发现和同步锁无关,换成两个对象后,不是同一把锁了,情况立刻变化。
  • 3.对于普通同步方法,锁的是当前实例对象,通常指this,所有的普通同步方法用的都是同一把锁一>实例对象本身,对于静态同步方法,锁的是当前类的Class对象
  • 4.所有的普通同步方法用的都是同一把锁—实例对象本身,就是new出来的具体实例对象本身,本类this,也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁
  • 所有的静态同步方法用的也是同一把锁—类对象本身,就是我们说过的唯一模板Class具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有静态条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁

44.synchronized的使用及其原理?

synchronized 可以用在实例方法、代码块、静态方法

  • 实例方法:作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁
    •  采用ACC_SYNCHRONIZED标记符来实现同步,这个标识指明了该方法是一个同步方法。
    • 原理:调用指令将会检查方法的ACC SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor锁,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor
  • 代码块:作用于代码块,对括号里配置的对象加锁。
    • 原理:实现使用的是monitorenter和monitorexit指令,monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指向同步代码块的结束位置。
    • 一般是一个monitorenter对应两个monitorexit
  • 静态方法:作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
    • 原理:ACC_STATIC, ACC_SYNCHRONIZED访i标志区分该方法是否静态同步方法

45.为什么任何一个对象都可以成为一个锁?

  • 每个对象大生都带着一个对象监视器每一个被锁住的对象都会和Monitor关联起来
  • monitorenter、monitorexit 或者 ACC_SYNCHRONIZED 都是基于 Monitor 实现的。
  •  Monitor 其实是一种同步工具,也可以说是一种同步机制
  • 在 Java 虚拟机(HotSpot)中,Monitor 是由ObjectMonitor 实现的,可以叫做内部锁,或者 Monitor 锁。
    • monitorenter,在判断拥有同步标识 ACC_SYNCHRONIZED 抢先进入此方法的线程会优先拥有 Monitor 的 owner ,此时计数器 +1。
    • monitorexit,当执行完退出后,计数器 -1,归 0 后被其他进入的线程获得。

46.说说 synchronized 和 ReentrantLock的区别?

  • synchronized:
    • synchronized是一个关键字
    • synchronized 可以直接在方法上加锁,也可以在代码块上加锁(无需手动释放锁,锁会自动释放)
  • ReentrantLock:
    • Lock 属于一个接口,其实现类主要有 ReentrantLock
    • ReentrantLock 必须手动声明来加锁和释放锁。
    • ReentrantLock 可以指定是公平锁还是非公平锁。

47.线程死锁了解吗?

  • 死锁:死锁发生在多个线rrrr程相互等待对方释放锁资源,导致所有线程都无法继续执行。 

48.那死锁问题怎么排查呢?

  • 1.命令行
    • jps -l  查看进程编号
    • jstack 进程编号   检查故障编号
  • 2.可视化的性能监控工具
    • VisualVM,
    • JConsole(运行打开查看)

49.CAS存在的问题?

  • 1.ABA问题:        
    • ​​​​​​​如果一个位置的值原来是 A,后来被改为 B,再后来又被改回 A,那么进行 CAS 操作的线程将无法知晓该位置的值在此期间已经被修改过。
    • 解决:AtomicStampedReference类来解决ABA问题,原子更新带有版本号的引用类型。
  • 2.循环性能开销:
  • 3.只能保证一个变量的原子操作: 

50.wait和sleep的区别? 

  • wait:
    • wait是Object中的方法
    • wait可以不设置时间
    • wait释放lock
    • wait需要依赖synchronized关键字
    • wait()方法需要依靠notify()唤醒线程。notifyAll()方法或者wait(方法中指定的等待时间到期来
    • 用无参数的 wait 方法,线程会进入 WAITING 无时限等待状态。
  • sleep:
    • sleep是线程中的方法  
    • sleep必须设置参数时间
    • sleep方法不会释放lock
    • sleep方法不依赖于同步器synchronized
    • sleep()方法在指定的时间过后,线程会自动唤醒继续执行
    • 调用sleep方法线程会进入TIMED_WAITING有时限等待状态 
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会敲代码的小张

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值