多线程篇学习面试

发布于:2025-02-25 ⋅ 阅读:(14) ⋅ 点赞:(0)

多线程

1.乐观锁、CAS思想

java乐观锁机制:

​ 乐观锁体现的是悲观锁的反面。它是一种积极的思想,它总是认为数据是不会被修改的,所以是不会对数据上锁的。但是乐观锁在更新的时候会去判断数据是否被更新过。乐观锁的实现方案一般有两种(版本号机制和CAS)。乐观锁适用于读多写少的场景,这样可以提高系统的并发量。在Java中 java.util.concurrent.atomic下的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
​ 乐观锁,大多是基于数据版本 (Version)记录机制实现。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

CAS思想:

​ CAS就是compare and swap(比较交换),是一种很出名的无锁的算法,就是可以不使用锁机制实现线程间的同步。使用CAS线程是不会被阻塞的,所以又称为非阻塞同步。CAS算法涉及到三个操作:
需要读写内存值V;进行比较的值A;准备写入的值B
当且仅当V的值等于A的值等于V的值的时候,才用B的值去更新V的值,否则不会执行任何操作(比较和替换是一个原子操作-A和V比较,V和B替换),一般情况下是一个自旋操作,即不断重试。

​ ABA问题和高并发的情况下,很容易发生并发冲突,如果CAS一直失败,那么就会一直重试,浪费CPU资源

2.synchronized

使用方法:主要的三种使⽤⽅式
修饰实例⽅法: 作⽤于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁
修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例,因为静态成员不属于任何⼀个实例对象,是类成员。
修饰代码块: 指定加锁对象,对给定对象加锁,进⼊同步代码库前要获得给定对象的锁。
总结:synchronized锁住的资源只有两类:一个是对象,一个是类。

JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁.

3.ReenTrantLock

和synchronized区别:

  1. **底层实现 **:synchronized 是JVM层面的锁,是Java关键字,ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。
  2. 实现原理:synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁;ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
  3. 是否可手动释放:synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用;ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。
  4. **是否可中断:**synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成;ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
  5. **是否公平锁:**synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁,公平锁性能非常低。

4.公平锁和非公平锁区别

公平锁
公平锁自然是遵循FIFO(先进先出)原则的,先到的线程会优先获取资源,后到的会进行排队等待。
优点:所有的线程都能得到资源,不会饿死在队列中。适合大任务。

公平锁效率低原因:
公平锁要维护一个队列,后来的线程要加锁,即使锁空闲,也要先检查有没有其他线程在 wait,如果有自己要挂起,加到队列后面,然后唤醒队列最前面线程。这种情况下相比较非公平锁多了一次挂起和唤醒。
线程切换的开销,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。

非公平锁
多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁。

5.ThreadLocal

ThreadLocal简介
通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的。
专属本地变量该如何解决呢?JDK中提供的 ThreadLocal 类正是为了解决这样的问题。类似操作系统中的TLAB。

原理
首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。
最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。

ThreadLocal内存泄漏的场景

​ 实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,⽽ value 是强引⽤。弱引用的特点是,如果这个对象持有弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
​ 所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。假如我们不做任何措施的话,value 永远⽆法被GC 回收,如果线程长时间不被销毁,可能会产⽣内存泄露。

​ ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。因此使⽤完ThreadLocal ⽅法后,最好⼿动调⽤ remove() ⽅法。
在这里插入图片描述

6.HashMap线程安全

死循环造成 CPU 100%
HashMap 有可能会发生死循环并且造成 CPU 100% ,这种情况发生最主要的原因就是在扩容的时候,也就是内部新建新的 HashMap 的时候,扩容的逻辑会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,由于 HashMap 并非线程安全的,所以如果两个线程同时反转的话,便可能形成一个循环,并且这种循环是链表的循环,相当于 A 节点指向 B 节点,B 节点又指回到 A 节点,这样一来,在下一次想要获取该 key 所对应的 value 的时候,便会在遍历链表的时候发生永远无法遍历结束的情况,也就发生 CPU 100% 的情况。

所以综上所述,HashMap 是线程不安全的,在多线程使用场景中推荐使用线程安全同时性能比较好的 ConcurrentHashMap。

7.String不可变原因

  1. 可以使用字符串常量池,多次创建同样的字符串会指向同一个内存地址;
  2. 可以很方便地用作 HashMap 的 key。通常建议把不可变对象作为 HashMap的 key;
  3. hashCode生成后就不会改变,使用时无需重新计算;
  4. 线程安全,因为具备不变性的对象一定是线程安全的;

8.volatile

作用:
保证数据的“可见性”:被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
禁止指令重排:在多线程操作情况下,指令重排会导致计算结果不一致。

9.死锁条件、解决方式

死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象;

死锁的条件:

  • 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待至占有该资源的进程释放该资源。
  • 请求与保持条件:进程获得一定的资源后,又对其他资源发出请求,阻塞过程中不会释放自己已经占有的资源。
  • 非剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放。
  • 循环等待条件:系统中若干进程组成环路,环路中每个进程都在等待相邻进程占用的资源。

**解决方法:**破坏死锁的任意一条件。

  • 乐观锁,破坏资源互斥条件,CAS。
  • 资源一次性分配,从而剥夺请求和保持条件、tryLock。
  • 可剥夺资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件,数据库deadlock超时。
  • 资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,从而破坏环路等待的条件,转账场景。