# interview2019 **Repository Path**: tonghlsoft/interview2019 ## Basic Information - **Project Name**: interview2019 - **Description**: No description available - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2019-06-28 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 本项目是对Java面试的总结,本项目适合3到5年开发经验的后端开发人员 面试地点:北京/武汉 学习方式:简单的理论+代码+小总结+*扩展* 项目结构: java | |---base | |---design | |---juc | |---CAS_study | |---container_study | |---lock_study | |---queue_study | |---volatile_study 包结构说明 base:java基础相关 design:设计模式相关 juc:并发包(java.util.concurrent) code: 代码示例 image:原理图片展示 CAS_study:CAS学习 container_study:容器相关学习 lock_study:锁相关学习 queue_study:队列相关学习 volatile_study:关键字volatile学习 **4.JMM内存模型** JMM本省是一种抽象的概念并不真实存在,它描述的是一种规范,这种规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素) 的访问方式。 JMM关于同步的规定: 1. 线程解锁前:必须把共享变量的值刷新回主内存。 2. 线程加锁前:必须读取主内存的最新值读到自己的工作内存 3. 加锁解锁是同一把锁 由于JVM运行的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存,工作内存是每个线程私有的数据区域,而JAVA内存模型中规定所有变量 都存储在主内存中,主内存是共享内存区域,所有的线程都是可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行。首先要将变量从主 内存中拷贝到自己的工作内存中,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存存储着 主内存中的变量拷贝副本,因此不同的线程间无法方法对方的工作内存,线程间的通讯必须通过主内存完成,这种机制称为可见性。 **5.谈谈什么是CAS?** CAS的全称为Compare-And-Swap,他是一条CPU并发原语, 它的功能是判断内存中某个位置的值是否为预期值,若是则修改为预期值,这个过程是原子的。 CAS并发原语体现在JAVA语言就是sun.misc.Unsafe类中的各个方法,调用Unsafe类中的CAS方法,JVM会帮助我们实现出CAS汇编指令,这是一种完全依 赖硬件的功能,通过它实现原子类的操作,再次强调,由于CAS是一种系统原语,原语属于操作系统用语的范畴,是有若干的指令组成的,用于完成某个功能 的一个过程。并且原语的执行必须是连续的,在执行的过程中不允许中断,也就是说CAS是一种CPU的原子指令,不会造成所谓的数据不一致的问题。 可以看出使用了Unsafe: 1. Unsafe 1.是CAS的核心类,此类存在于rt.jar包中sun.misc中,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一 个后门,基于该类可以直接操作特定的内存的数据,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于UnSafe类的方法。 注意:Unsafe类中的所有方法都是native修饰的。也就是说UnSafe类中的方法都直接调用操作系统底层资源执行相应任务。 2.变量valueoffset,表示变量值在内存地址中的偏移量,因为Unsafe就是根据内存便宜地址获取数据的。 2. 变量value用volatile修饰,保证了多线程之间的内存可见性。 其中这幅图这就是使用CAS的原理。(Unsafe类+自旋) 假设有A、B两个线程再同时执行getAndAddInt操作(分别在不同的cpu上执行); 1. AtomicInteger里面的value的原始值为3,即主内存中的AtomicInteger的值为3,根据JMM内存模型,线程A,B各自持有一份值为3的value的 副本拷贝到自己的工作内存中。 2. 线程A通过getIntVolatile(var1,var2)拿到value值3,这时线程A被挂起。 3. 线程B也通过getIntVolatile(var1,var2)拿到value值3,此时线程B刚好没有被挂起,并且执行了compareAndSwapint()方法去和主内存中的 值3比较,发现相等,那么修改其中的值为4,线程B改完收工,一切OK, 4. 这个时候线程A恢复了,也去执行compareAndSwapint()方法,发现自己工作内存中的值3和主内存中的值4不相等,说明该值已经被其他线程修改了, 那么本次线程A的修改失败,只能重新读取重新来一遍了。 5. 线程A重新获取该value的值,因为变量value被volatile修饰,所以其他线程对它的修改,线程A总是能看见,线程A继续执compareAndSwapint 进行比较替换,知道成功。 **6.CAS中的ABA问题** CAS算法实现一个重要前提需要取出内存中的某个时刻的数据并在当下时刻进行比较替换,那么在这个时间差类会导致数据的变化。 如:线程T1从主内存中取出A,这个时候线程T2也从主内存中取出A,并且线程T2进行了一些操作将值修改为B,然后线程T2又将数据修改为A,这个时候T1 进行CAS操作发现内存中仍然是A,然后线程T1操作成功。 尽管线程T1操作成功,但不代表这个过程是没有问题的 **ABA问题怎么解决呢?** 使用AtomicReference原子应用类,可以保证你在修改对象引用时的线程安全性,但是没有解决ABA问题。 使用AtomicStampedReference类 AtomicStampedReference它内部不仅维护了对象值,还维护了一个时间戳(我这里把它称为时间戳,实际上它可以使任何一个整数,它使用整数来表示 状态值 **8.JAVA之锁** 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到 并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁,默认是非公平锁 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁在高并发的情况下,有可能会造成优先级 反转或者饥饿的现象 非公平锁比较粗鲁,上来就尝试直接占用锁,如果尝试失败,就采用类似公平锁的那种方式 关于两者的区别: 公平锁:就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待对列,如果为空或者当前线程是等待队列的第一个,就占用 锁,否则就会加入到等待队列中,以后就按照FIFO的规则(先进先出)从队列中取到自己。 就Reentrantlocker而言, 通过构造函数指定该锁是否是公平锁,默认是非公平锁,非公平锁的优点在于吞吐量比公平锁大。 对于Synchronized而言,它也是一种非公平锁。 可重入锁(也叫递归锁) 线程可以进入任何一个它已经拥有的锁所有同步着的代码块 Reentrantlocker/Synchronized就是典型的可重入锁 可重入锁的最大作用是避免死锁 代码案例:ReentrantlockDemo 自旋锁(spinlock) 是指尝试获取锁的线程不会阻塞,而是采用循环的方式尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。如CAS 读占锁: 指该锁一次只能被一个线程持有。对ReentrantLock和Synchronized而言都是独占锁 共享锁: 指多个线程可以持有该锁 对ReentrantReadWriteLock其读锁是共享锁,写锁是独占锁 **9.CountDownLatch/CyclicBarrier/Semaphore使用过吗?** CountDownLatch: (做减法) 让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒 CountDownLatch主要有两个方法,当一个或者多个线程调用await方法时,调用线程会被阻塞。其他线程调用 countDown方法会将计数器减一 (调用countDown方法的线程不会阻塞),当计数器的值变为零时,因调用await()方法被阻塞的线程会被唤醒,继续执行 详情参见CountDownLatchDemo CycliBarrier(做加法如收集七龙珠后才能召唤神龙) Semaphore 信号主要用于两个目的,一个是用于多个共享资源的互斥作用,另外一个作用于并发线程数的控制。 SemaphoreDemo 当阻塞队列是空时,从队列中取出元素将会被阻塞 当阻塞队列是满时,从队列中添加元素将会被阻塞 **为什么用?有什么好处?** 在多线程领域所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动唤醒。 **为什么需要BlockingQueue?** 好处是我们不需要知道什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切都是BlockingQueue处理。 在concurrent包发布以前,在多线程环境下,我们程序员需要处理去控制这些细节,尤其还要兼顾效率和线程安全,而这些给我带来了难度。 BlockingQueue种类分析 ArrayBlockingQueue:由数组组成的有界数组 LinkedBlockingQueue:由链表组成的有界(默认值为Integer.MAX_VALUE)阻塞对类。 PriorityBlockingQueue:支持优先级排序的无界阻塞队列 DelayQueue:使用优先级队列实现的延迟无界阻塞队列。 SychronousQueue:不存储元素的阻塞队列,也即单个元素队列。 LinkedTransferQueue:由链表组成的无界队列 LinkedBlockingDeque:由链表组成的双向阻塞队列。 BlockingQueue的核心方法 **用在那里?** 1. 生产者消费者模式:传统版ProdConsumer_TraditionalDemo :队列阻塞版:ProdConsumer_BlockQueueDemo 2. 线程池 3. 消息中间件