Java 线程基础篇

  • sleep(),yeild(),wait()方法有什么区别?
    • 线程调度
    • sleep(),这个方法能够让正在执行的线程在指定的时间内暂停执行,从而进入阻塞状态,给其他线程运行机会,且不考虑其他线程优先级(区别于 yeild)。时间结束之后,从阻塞态/等待态进入就绪态。需要注意的是这个方法是不会释放锁的,即如果有同步块的话,就算调用了 sleep 方法,其他线程仍然访问不了共享数据,仅仅只是让出了 CPU 罢了。
    • yeild(),该方法会让正在执行的线程暂停,但不阻塞,即线程状态由【运行态 → 就绪态】。之后该让哪个线程运行,由 CPU 进行调度,因此有可能下个运行的还是那个礼让的线程。但是只能礼让给优先级一样或者是优先级更高的线程。同样的,这个方法也不会释放锁
    • wait(),使当前线程暂停执行并释放对象锁标志,并且把该线程加入到这个锁的等待队列中。只有调用了notify(随机唤醒一个 wait 线程)或者是notifyAll(唤醒全部的 wait 线程),将唤醒的线程调至对象的锁池中,而该锁池的线程才能去竞争该对象的锁。
  • Java 中创建线程有几种不同的方式?你最喜欢用哪种,为什么?
    • 三种方式:
      • 继承(extends)Thread 类
      • 实现(implements)Runnable 接口
      • 应用程序可以使用 Executor 框架创建线程池,进而创建线程
    • 我最喜欢用 Runnable 接口方式,因为这不需要继承 Thread 类,因为如果继承了别的对象的话,就不能再继承对象了,只能靠实现接口去间接实现多继承了。同时,线程池我觉得也非常高效,减少了很多不必要的线程创建销毁开销,提升了效率。
  • 怎么保证线程安全?
    • 加锁:使用 JVM 提供的锁(synchronized)或者 JDK 提供的基于 Lock 的各种锁
  • Runnable 和 Callable 区别?
  • 线程状态有几种?是怎么进行转换的?(Java 层面)
    • 线程状态:Runnable(可运行)、Blocked(锁阻塞)、Waiting(无限等待)、Timed_Waiting(计时等待)和 Terminatd(结束状态)
  • 怎么获取线程的返回值?

锁篇

  • 悲观锁、乐观锁的区别是什么?

    • 悲观锁:
      • 读数据的时候总认为会被别人修改,因此每次读数据的时候还会上锁,阻塞其他人。
      • 用共享资源和线程去理解就是:共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
      • Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。
    • 乐观锁:
      • 每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据
      • 使用版本号机制和 CAS 算法实现。
      • 在 Java 中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
    • 区别:
      • 乐观锁适用于写比较少的情况下(多读场景),因为可以减少上锁解锁的开销,加大系统的吞吐量
      • 悲观锁适用于写比较多的情况下(多写场景),因为一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能。
  • Lock 和 synchronized 有什么区别?

类别 synchronized lock
存在层次 Java 关键字,在 JVM 层面 是一个类
锁的释放 1、已获取锁的线程执行完同步代码,释放锁
2、线程执行发生异常,JVM 会让线程释放锁
try catch finally 语句中的 finally 中必须释放锁,不然容易造成线程死锁
锁的获取 假设 A 线程获得锁,B 线程等待
如果 A 线程阻塞,B 线程会一直等待
分情况而定,lock 有多个锁获取的方法,可以尝试获得锁,线程可以不用一直等待
锁的状态 无法判断 可以判断
锁的类型 可以重入、不可以中断、非公平 可以重入 可以判断 可公平
性能 少量同步 大量同步
  • ReentrantLock 和 synchronized 的区别?

    • Synchronized是 java 语言关键字,是 JVM 层面的,需要monitor对象实现,ReentrantLock是基于 API 层面的锁,本质上是基于AQS的同时,实现Lock 接口的一个锁,需要lock()和unlock()方法配合try - catch - finally语句块来完成;

    • ReentrantLock是一个类,比synchronized多了许多灵活的特性:可响应中断、可轮回, 为处理锁的不可用性提高了灵活性

      即:synchronized是不可中断的锁,而RenntranLock是可以中断的

    • ReentrantLock需要unlock方法去解锁,不然可能会造成死锁;而synchronized则会在发生异常的时候会自动地释放锁资源;

    • synchronized不能绑定条件,而ReentranLock可以通过绑定condition结合await()/signa()方法对线程进行精准唤醒

  • ReentrantLoc 底层怎么实现的?

  • Synchronized 原理是什么?

    • synchronized 底层原理是与monitor,即监视器锁有关,每一个对象都有一个关联的 monitor 监视器锁,而 monitor 被占用了,该对象就会处于锁定状态,而 monitor 里面有个计数器,初始值是从 0 开始的。

      当 synchronized 获得了 monitor 对象所有权后会进行两个关键的指令【加锁指令 monitorenter】【释放锁指令 monitorexit】

    • monitorenter 加锁执行过程:

      1. 首先判断它计数器是不是 0,如果是 0 的话,说明没人获取锁,那么该线程就可以获取锁,将计数器+1
      2. 而假如不是 0,但是是本线程已经占有过的(重入),那么就把计数器+1
      3. 假如不是 0 且是为其他线程占用了 monitor 的话,那么本线程就会进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试,获取 monitor 的所有权。
    • monitorexit 解锁执行过程:

      1. 拥有该 monitor 的线程执行该指令后,计数器-1。如果-1 后的计数器为 0,那么线程就不再占有此 monitor 了,其他被这个 monitor 锁阻塞的线程就可以尝试去获取该 monitor 的所有权;
      2. 如果计数器不是 0,该线程就进入阻塞等待状态,直到为 0 为止。
  • 自旋锁和轻量级锁有什么区别?

  • 什么是公平锁,用什么数据结构实现

    • 公平锁:每次获取到锁的线程为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,即按照请求锁的顺序分配,拥有稳定获得锁的机会,但是性能比非公平锁要低;
    • 实现数据结构:同步队列 CLH,本质上是一种双向队列,通过链表实现。详细来说就是通过
  • CAS 是不是线程安全的?为什么

    • 先说回答:是线程安全的
    • 再说实现:
  • CAS 是怎么解决 ABA 问题的?

    • 问题:ABA 问题是指一个线程 1 和 2 读取了变量 A,接着线程 2 通过 CAS 将 A 改为 B,此时进来了一个线程 3,再通过 CAS 将 B 改回 A。最后 A 想要将 A 改为 B,于是用 CAS 去内存中判断 A 的值,发现“没改变”,于是改为 B;这就叫 ABA 问题

    • 解决思路:通过在变量面前加上版本号,变量更新的时候就把该版本号+1,则:A-B-A 变为 1A - 2B-3A。

    • 具体解决办法:java 1.5 后的原子 anomic 包提供了类AtomicStampedReferencecompareAndSet方法来实现上述思路:

      1
      2
      update table set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision}
      // 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样
  • 谈谈你对 AQS 的理解?

    • AQS 是 Java 线程同步的一个【框架】, 用于实现 JDK 中的各种锁的
    • AQS 中维护了一个【信号量 state】和【线程】组成的双向队列。队列用于存放线程,进行排队;而信号量用于控制线程等待和放行等操作。
  • AQS 如何实现可重入锁的?

    • 上面说到 AQS 本质是维护信号量与双向队列的一个组件,那么此时信号量 state 就表示加锁的次数,0 表示无锁,如果有线程获取锁了,state 就+1(重复的线程获取),如果当前线程释放了锁,state 就-1。
  • 多线程的死锁是什么?怎么解决?

  • 两个方法加 synchronized,一个线程进去 sleep,另一个线程可以进入到另一个方法吗?

    • 不能,方法上加synchronized相当于给当前的实例对象加锁,现在已经有一个线程获得了对象锁,由于 sleep 方法不释放锁,另一个线程想要访问另一个synchronized修饰方法,必须先获得对象锁,现在获取不了,因此不能进入另一个方法。

Java 并发底层篇

  • volatile 的作用和原理是什么?能代替锁吗

    • 作用:
      • 保证了有序性:禁止指令重排序优化,普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。
      • 实现可见性:保证此变量对所有线程的可见性,当一条线程修改了这个变量的值,修改的新值对于其他线程是可见的(可以立即得知的);
      • 保证单次读写的原子性
    • 原理:
      • 保证有序性:通过禁止指令重排序保证有序性,具体是靠插入内存屏障去实现的
      • 实现可见性:lock 前缀指令 + MESI 缓存一致性协议
  • volatile 是如何实现可见性的呢?

    • volatile修饰的字符会在其对应的汇编指令上加个Lock 前缀指令,在堆共享变量进行了写操作后,会马上被写回主存中,其他的 CPU 通过总线嗅探机制进行监听,一旦发现修改后,就会认为自己缓存中的数据的过期了的,立即将该值置为失效状态,重新从主内存中获取最新的值。
  • 说一下什么是 ThreadLocal 吧

    • ThreadLocal 是Java所提供的【线程本地存储机制】,我们可以利用该机制将数据/对象存储在某个线程的内部,该线程可以通过 get 方法获取到缓存的数据
    • 底层是通过 ThreadLocalMap 去实现的,每个线程(Thread)对象都存在一个 ThreadLocalMap 对象,既然是 Map,那么就有键值对:其中 key 是 ThreadLocal 对象,value 则是需要存储的值
  • 为什么 ThreadLocal 会发生内存泄漏呢?

    • 案例情景 1:比如说在使用了线程池的情况,线程执行完一个任务后并不会被销毁,而是回到线程池中。这有可能导致线程中的 ThreadLocalMap、ThreadLocal、数据都一直存在。这样的情况如果出现很多就会占满 JVM 内存,出现内存泄漏情况。

      ![Thread引用关系](

      https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20210907204149984.png)

    • 案例情景 2:因为ThreadLocalMap中的 key 是ThreadLocal 的弱引用,而 value 是强引用。当ThreadLocal没有被强引用时,在 GC 的时候会发生:key 被清理,而 value 不被清理的情况。那么此时如果不做任何的处理的话,value 就永远无法被回收掉,这样就发生内存泄漏了

  • 如何解决 ThreadLoacl 的内存泄漏呢?

    • 在使用完ThreadLocal后手动调用remove()方法,就可以把 ThreadLocalMap 中的 key 和 value 都置为 null;
  • 既然 ThreadLocal 用弱引用会发生内存泄漏,那为什么不用强引用,而继续用弱引用呢?

    • 原因:因为如果是强引用的话,也会发生内存泄漏。如果现在作为 key 的 ThreadLocal 引用为强引用,引用 ThreadLocal 对象被回收了,但 ThreadLocalMap 仍然保留有 ThreadLocal 的强引用,那么就无法在 GC 中被回收掉。如果不手动去删除,就会发生内存泄漏问题了。

      而如果是弱引用的话,起码能保证在 GC 时能回收 ThreadLocal,而至于 Map 中的 Value 就需要在调用 set get remove 方法去清除。

      综上,用弱引用比较好。

线程池篇

  • Java 的线程池大概有哪几种?
  • 线程池的七大参数分别是什么?
  • 线程池参数该如何设计?
  • 拒绝策略有哪些?

并发容器篇

  • concurrenthashmap 实现原理说一下

JUC 篇