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引用关系

    • 案例情景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篇