java多线程知识点和面试常见问题

2022-07-28 09:09:58

前言

最近我在换工作,深刻体会到面试准备和日常学习是两回事。学习是把一门知识从概括到具体,从主要特性到全部特性逐步理解和掌握的过程。而面试题解答则是把你学习的知识灵活运用的过程,并且需要加入你的思考。

在准备面试的过程中,我把面试可能涉及到的知识面做了一个梳理,再根据知识面总结面试题和知识点。把这些问题彻底搞懂。本文并不是你面试的宝典,文章所有的问题及解读,单独去看都是没有意义。就拿多线程来说,你如果只是背诵题目和答案,对你没有任何帮助,不但难于记忆,而且无法灵活应对。本文是建立在你对该门技术已经充分学习的情况下,来帮助你应对面试中的问题。

问题基于我个人的收集,答案也是我自己的理解加上各类文章的学习得出,我尽量做到深入到源代码或者原理层面,但也仅供参考。大家准备面试不要仅限于此。能继续深挖问题根源的要继续深挖。知识就是这样,你越挖的深,掌握的就越牢固。

---------------------------------------------------------------------

多线程知识点和面试常见问题

1、什么是线程安全性?

当多线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类时线程安全的。

2、什么是最低安全性?

在没有同步的情况下,读取变量可能会得到一个失效的值,但是至少这个值是之前某个线程设置的。这称之为最低安全性,一般情况下都可以满足最低安全性。

例外情况,非volatile类型的64位数值变量(double,long)。由于jvm允许64位读写分为两个32位操作,那么可能造成高低位只有一处进行了更相信,导致读取出错误的值。多线程中使用共享且可变的long和double,必须用volatile修饰。

3、wait、sleep区别

  • sleep,线程暂停指定时间长度,然后恢复。线程在此期间并不会释放锁
  • wait,此线程放弃对象锁,调用notify后,才能进入运行状态。
  •  wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

4、实现同步的机制有哪些?还有什么保证线程安全的方法?

主要同步机制是synchronized。此外还包括volatile类型变量,显示锁,以及原子变量。

另外通过不可变对象、实例封闭、threadLocal也可以保证线程安全。

5、Atomic

Atomic提供原子操作,在并发的情况下,保证这些操作是线程安全的。

atomic原子操作,通过CAS实现(比较刷新,通过期望的内存值(expect)和实际值比较,如果一致则进行数据交换,交换为new值,本质上这是一个乐观锁)。取值并增加的示例代码如下:

public final int getAndAddInt(Object obj, long valueOffset, int var) {
        int expect;
        // 利用循环,直到更新成功才跳出循环。
        do {
            // 获取value的最新值
            expect = this.getIntVolatile(obj, valueOffset);
            // expect + var表示需要更新的值,如果compareAndSwapInt返回false,说明value值被其他线程更改了。
            // 那么就循环重试,再次获取value最新值expect,然后再计算需要更新的值expect + var。直到更新成功
        } while(!this.compareAndSwapInt(obj, valueOffset, expect, expect + var));

        // 返回当前线程在更改value成功后的,value变量原先值。并不是更改后的值
        return expect;
    }

获取期望的内存中数值(expect),计算add后的值(new)。compareAndSwapInt时候如果内存中的值已经和expect不等,则返回false,重复之前逻辑,获取expect值,重新计算再次compareAndSwapInt,直至成功。

CAS是一个cpu级别的操作,执行前会判断是否为多CPU,多CPU会加锁,单CPU不加锁。

CAS缺点如下:

  • 只能保证一个共享变量的原子操作。有多个共享变量要做原子操作时,Atomic并不适用。
  • 循环时间长开销很大。从代码可以看到,如果CAS失败,会不断的尝试。
  • ABA问题,是否A更新为B后再更新回A时,认为内存的值没有变,可以进行更新。这是CAS的漏洞,如果你的逻辑需要考虑这个问题可以使用AtomicStampedReference或者其他同步机制。

CAS相关知识参考:https://blog.csdn.net/v123411739/article/details/79561458

6、volatile

volatile修饰的变量,会立即更新到主存,变量不会放到寄存器等地方,也不会和其他内存操作一起重排序(重排序是JVM的特性,为了提升性能,改变操作的顺序)。保证别的线程能立即获取到最新值。和Synchronized比起来是更轻量的同步机制。不会阻塞线程。volatile通常用于当变量用于状态判断(while(!status){},里面的status),确保该状态的可见性,可以通过volatile简化代码。

7、Synchronized和Lock区别

从特性上看两者区别如下:

  • lock可被打断,不让等待的程序一直等待下去,
  • 有时候读操作不会发生冲突,通过lock可以实现。
  • lock可以知道线程是否获取了锁
  • lock必须要手动释放锁
  • synchronized为非公平锁。ReentrantLock可以设置为公平锁

从实现上看区别如下:

Synchronized:

synchronized实现,JVM中对象对等体的对象头中保存锁状态,指向ObjectMonitor。锁对象的ObjectMonitor里面有当前获得锁的线程的引用,EntryList中保存尝试获取锁的线程,waitSetLcok保存wait的线程。

加了synchronized关键字后,字节码多出monitorenter,还有monitorexit。执行monitorenter时尝试获取objectmonitor对象,如果已经有锁,自己加入waitset。本质是依赖底层的操作系统的Mutex Lock(互斥锁)。" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。没能获取mutex,那么自旋等待。

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”:锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。

参考:http://baijiahao.baidu.com/s?id=1580364922566928882&wfr=spider&for=pc

Lock:

Lock底层机制是双向链表+锁状态

双向链表存储等待锁的线程,锁状态代表锁是否已经被获取

通过CAS+volatile来保证链表操作和锁状态操作的线程安全性。

代码中流程如下:

1、尝试通过CAS方式更新锁状态,成功的话获得锁

2、失败的话再次尝试获取锁,如果失败则把自己加入等待双向链表。

3、然后通过自旋,不断判断自己是否到达队列顶端,如果到达顶端则尝试再去获取锁。获取成功,则把自己从等待链表头部移除

参考:https://yq.aliyun.com/articles/640868

8、hashTable , concurrentHashMap, HashMap

关键区别是HashMap线程不安全,hashTable线程安全,但是锁整个数组,效率很低。

concurrentHashMap,采用分段锁,在损失很小的单线程的性能时,极大的提升并发访问的吞吐量。内部把散列桶分为16份,每个锁保护1/16.

9、闭锁

延迟线程的进度直到其到达终止状态。

CountDownLatch是一种灵活的实现。比如启动门,所有的线程通过启动门等待,主线程countDown后,所有线程同时启动。

10、栅栏

类似闭锁,阻塞一组线程,直到某个事件发生。栅栏是等待其他线程。CyclicBarrier,每个线程调用CyclicBarrier.await().直到满足初始化CyclicBarrier时传入的线程数量,才释放线程。

此外CyclicBarrier还有一个构造函数可以传入Runnable barrierAction,释放前执行前置逻辑。

13、notify()给notifyAll()的区别

notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中, 而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列, 被移动的线程状态由 WAITING变为BLOCKED。

14、Join()

如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

15、ThreadLocal

每个线程有自己的副本,保证访问的变量都是自己线程的。一般用于不需要共享,但也不想重复创建和销毁,可以通过threadlocal优雅的实现每个线程复制一份自己的副本。ThreadLocal内部有个ThreadLocalMap对象,线程为key,值为value。所以不同线程能分别取到自己的值

  • 作者:爱码叔(稀有气体)
  • 原文链接:https://blog.csdn.net/liyiming2017/article/details/88417352
    更新时间:2022-07-28 09:09:58