JUC+并发总结

本文总结了JUC(Java并发工具包)中的面试重点,包括CAS操作的原理、ABA问题及其解决方案、AtomicInteger的实现、CountDownLatch和CyclicBarrier的使用、ConcurrentHashMap的特点以及并发集合的选择。还探讨了并发编程中的原子性、可见性和有序性概念,以及volatile和synchronized的作用。此外,详细解释了锁的优化策略,如自旋锁、自适应自旋和锁消除。文章还比较了Lock与synchronized的区别,并介绍了读写锁的概念。
摘要由CSDN通过智能技术生成

JUC 面试题总结

类的实例是如何加载到容器中去的(实例化) (陌陌)

静态成员先初始化,只初始化一次,非静态成员被实例化多次就初始化多次。所以静态成员在内存中只有一份,而非静态成员有多份。

进程是操作系统分配资源基本单位
程序,exe,双击就会通过总线加载在内存,就必须把指令加载至CPU,数据加载至内存
线程是cpu调度执行基本单位
多个线程共享一个进程的资源,从
Java线程
纤程,原本在cpu进程调度时会浪费大量资源,用于用户态和内核态,减少了用户态和内核态的切换次数。
并发:时间片的划分,一个一个执行,一个cpu执行多个程序。
并行:多个CPU执行多个程序,可以一起执行

什么是 CAS

CAS表示Compare And Swap,比较并交换,需要三个操作数,分别是内存位置V,旧的预期值A和准备设置的新值B。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。继续比较直到主内存和工作内存中的值一致为止;但是不管是否更新都会返回V的旧值,这些处理过程都是原子操作,执行期间不会被其它线程打断。cas操作会消耗cpu资源,cas操作类似于自旋的操作,也有人将cas称为自旋锁,cas是一种无锁的操作,在无锁情况下,保证线程安全。

CAS有什么问题?

CAS从语义上面来讲存在一个逻辑漏洞,如果V初次读取时是A,并在准备赋值时也是A,这依旧不能说明它没有被其它线程更改过,因为这段时间内假设它的值先改为B又改为A,那么CAS操作就会误以为它从来没有被改变过。
从两个角度理解,从基础数据类型理解,影响不大,从引用数据类型,就会导致对象引用数据指向发生改变,
这个漏洞称为ABA问题,juc包提供一个AtomicStampedReference,原子更新带有版本号的引用类型,通过控制变量值的版本来解决ABA问题,大部分情况下,ABA不会影响程序并发的正确性。

CAS底层原理

自旋锁+unsafe类

atomicInteger.getAndincrement()方法的源代码:

 public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

在这里插入图片描述
1:unsafe
是CAS的核心类,由于java方法无法调用底层系统,需要通过本地(native)方法来访问,unsafe相当于一个后门,基于该类可以直接操作特定内存的数据,位于sun.misc包中,其内部方法操作可以像c的指针一样直接操作内存,因为java中的CAS操作的执行依赖于unsafe类的方法
注意:unsafe类中所有的方法都是native修饰的,也就是说unsafe类中的方法都是直接调用操作系统底层资源执行相应的任务
2:变量valueOffset,表示变量值在内存中的偏移地址,因为unsafe就是根据内存偏移地址来获取数据的。
3、变量value是用volatile修饰保证了多线程之间的内存可见性

CAS是一条CPU并发原语,他的功能是判断内存某个位置的值是否是预期值,如果是就更改为新的值,这个过程是原子的;
由于CAS柿子红系统原语,原语属于操作系统用语的范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行的过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

这里循环体现了自旋锁
var1:atomicInteger对象本身
var2:该对象值的引用地址
var4:是用var1和var2找出主内存中真实中真实的值
用该对象当前的值与var5比较;
如果相同,更新var4+var5并且返回true
如果不同,继续取值然后再比较,直到更新完成;
底层代码的用例:
在这里插入图片描述

有哪些原子类?(暂时不用记)

处于java.util.concurrent.atomic包,依据作用分为四种:原子更新基本类型类,原子更新数组类,原子更新引用类以及原子更新字段类,atomic包里的类基本都是Unsafe实现的包装类。

AtomicInteger 原子更新整形**、 AtomicLong** 原子更新长整型、AtomicBoolean 原子更新布尔类型。

AtomicIntegerArray,原子更新整形数组里的元素、AtomicLongArray 原子更新长整型数组里的元素、 AtomicReferenceArray 原子更新引用类型数组里的元素。

AtomicReference 原子更新引用类型、AtomicMarkableReference 原子更新带有标记位的引用类型,可以绑定一个 boolean 标记、 AtomicStampedReference 原子更新带有版本号的引用类型,关联一个整数值作为版本号,解决 ABA 问题。

AtomicIntegerFieldUpdater 原子更新整形字段的更新器、 AtomicLongFieldUpdater 原子更新长整形字段的更新器AtomicReferenceFieldUpdater 原子更新引用类型字段的更新器。

AtomicInteger实现原子更新的原理

cas原理
getAndIncrement以原子方式将当前的值加1,首先要在for死循环里面取得AtomicInteger里存储的数值,第二步对AtomicInteger当前的值加1,第三步调用compareAndSet方法进行原子更新,先检查当前数值是否等于expect,如果等于则说明当前值没有被其它线程修改,则将值更新为next,否则会更新失败返回false,程序会进入for循环重新进行compareAndSet操作。
atomic包中只提供了三种基本类型的原子更新,AtomicInteger 原子更新整形、 AtomicLong 原子更新长整型、AtomicBoolean 原子更新布尔类型。
atomic 包里的类基本都是使用 Unsafe 实现的,Unsafe 只提供三种 CAS 方法compareAndSwapInt,compareAndSwapLong 和 compareAndSwapObject,例如原子更新 Boolean 是先转成整形再使用 compareAndSwapInt 。

CountDownLatch是什么

实现原理:AQS+volatile state 是用volatile,AQS中使用了大量的CAS操作。
递减门栓,作用:可以把线程阻塞住,当线程调用这个方法,awit 方法当计数器没有变为0就一直阻塞,直到计数器变为0。构造方法接收一个 int 参数作为计数器,如果要等待 n 个点就传入 n。每次调用 countDown 方法时计数器减 1。

CyclicBarrier 是什么

循环屏障是基于同步到达某个点的信号量触发机制,作用是让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障才会解除。构造方法中的参数表示拦截线程数量,每个线程调用 await 方法告诉 CyclicBarrier 自己已到达屏障,然后被阻塞。还支持在构造方法中传入一个 Runnable 任务,当线程到达屏障时会优先执行该任务。适用于多线程计算数据,最后合并计算结果的应用场景

CountDownLacth 的计数器只能用一次,而 CyclicBarrier 的计数器可使用 reset 方法重置,所以 CyclicBarrier 能处理更为复杂的业务场景,例如计算错误时可用重置计数器重新计算。

ConcurrentHashMap(暂时不背)

ConcurrentHashMap 用于解决 HashMap 的线程不安全和 HashTable 的并发效率低,HashTable 之所以效率低是因为所有线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器的部分数据,那么多线程访问容器不同数据段的数据时,线程间就不会存在锁竞争,从而有效提高并发效率,这就是 ConcurrentHashMap 的锁分段技术。首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。

主要对 JDK7 做了三点改造① 取消分段锁机制,进一步降低冲突概率。② 引入红黑树结构,同一个哈希槽上的元素个数超过一定阈值后,单向链表改为红黑树结构。③ 使用了更加优化的方式统计集合内的元素数量。具体优化表现在:在 put、resize 和 size 方法中设计元素总数的更新和计算都避免了锁,使用 CAS 代替。

get 同样不需要同步,put 操作时如果没有出现哈希冲突,就使用 CAS 添加元素,否则使用 synchronized 加锁添加元素。

当某个槽内的元素个数达到 7 且 table 容量不小于 64 时,链表转为红黑树。当某个槽内的元素减少到 6 时,由红黑树重新转为链表。在转化过程中,使用同步块锁住当前槽的首元素,防止其他线程对当前槽进行增删改操作,转化完成后利用 CAS 替换原有链表。由于 TreeNode 节点也存储了 next 引用,因此红黑树转为链表很简单,只需从 first 元素开始遍历所有节点,并把节点从 TreeNode 转为 Node 类型即可,当构造好新链表后同样用 CAS 替换红黑树。

ArrayList 的线程安全集合是什么

可以使用 CopyOnWriteArrayList 代替 ArrayList,它实现了读写分离。写操作复制一个新的集合,在新集合内添加或删除元素,修改完成后再将原集合的引用指向新集合。这样做的好处是可以高并发地进行读写操作而不需要加锁,因为当前集合不会添加任何元素。使用时注意尽量设置容量初始值,并且可以使用批量添加或删除,避免多次扩容,比如只增加一个元素却复制整个集合。

并发面试题总结

JMM:
可见性:原子性;可见性;有序性

JMM 的作用是什么

Java memory model (Java内存模型)
Java 线程的通信由 JMM 控制,JMM 的主要目的是定义程序中各种变量的访问规则。变量包括实例字段、静态字段,但不包括局部变量与方法参数,因为它们是线程私有的,不存在多线程竞争。

JMM 遵循一个基本原则只要不改变程序执行结果,编译器和处理器怎么优化都行。(原则:as-if-serial ,happens-before,两个都差不对)例如编译器分析某个锁只会单线程访问就消除锁,某个 volatile 变量只会单线程访问就把它当作普通变量。

JMM 规定所有变量都存储在主内存,每条线程有自己的工作内存,工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。不同线程间无法直接访问对方工作内存中的变量,线程通信必须经过主内存。

关于主内存与工作内存的交互,即变量如何从主内存拷贝到工作内存、从工作内存同步回主内存,JMM 定义了 8 种原子操作
在这里插入图片描述

as-if-serial 是什么

不管怎么重排序,单线程程序的执行结果不能改变,编译器和处理器必须遵循 as-if-serial 语义。

为了遵循 as-if-serial,编译器和处理器不会对存在数据依赖关系的操作重排序,因为这种重排序会改变执行结果。但是如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

as-if-serial 把单线程程序保护起来,给程序员一种幻觉:单线程程序是按程序的顺序执行的,底层是程序员不能看见的,所以程序猿不知道是否发生重排序。

happens-before 是什么?

和 as-if-serial 说的其实是同一件事,然后着重讲解as-if-serial

as-if-serial 和 happens-before 有什么区别

as-if-serial 保证单线程程序的执行结果不变,happens-before 保证正确同步的多线程程序的执行结果不变。

这两种语义的目的都是为了在不改变程序执行结果的前提下尽可能提高程序执行并行度。

什么是指令重排序

在寄存器看来,内存的执行速度非常非常慢
指令的重排序就是指令的执行顺序和程序猿编写的数据不一致。
目的是为了提高效率。
为了提高性能,编译器和处理器通常会对指令进行重排序,重排序指从源代码到指令序列的重排序,分为三种:
编译器优化的重排序,编译器在不改变单线程程序语义的前提下可以重排语句的执行顺序。
指令级并行的重排序,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
内存系统的重排序。

原子性、可见性、有序性分别是什么

原子性
原子性就是说一个操作不可以被中途cpu暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行. 如果一个操作是原子性的, 那么在多线程环境下, 就不会出现变量被修改等奇怪的问题。

可见性

可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。JMM 通过在变量修改后将值同步回主内存,在变量读取前从主内存刷新的方式实现可见性,无论普通变量还是 volatile 变量都是如此,区别是 volatile 保证新值能立即同步到主内存以及每次使用前立即从主内存刷新。

除了 volatile 外,synchronized 和 final 也可以保证可见性。同步块可见性由"对一个变量执行 unlock 前必须先把此变量同步回主内存,即先执行 store 和 write"这条规则获得。final 的可见性指:当一个变量被final修饰,这个变量就不可以修改,就没有可见性问题。

有序性

有序性可以总结为:在本线程内观察所有操作是有序的,在一个线程内观察另一个线程,所有操作都是无序的。前半句指 as-if-serial 语义,后半句指指令重排序和工作内存与主内存延迟现象。

Java 提供 volatile 和 synchronized 保证有序性,volatile 本身就包含禁止指令重排序的语义,而 synchronized 保证一个变量在同一时刻只允许一条线程对其进行 lock 操作,确保持有同一个锁的两个同步块只能串行进入。

谈一谈 volatile

java虚拟机提供的轻量级的同步机制

保证可见性:
不保证原子性;变量在更改的过程中,会出现重置,解决原子性:用atomicInteger解决。
禁止指令重排序

保证变量对所有线程可见

当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的。volatile 变量在各个线程的工作内存中不存在一致性问题,但 Java 的运算操作符并非原子操作(num++),导致 volatile 变量运算在并发下仍不安全。

禁止指令重排序优化
内存屏障:又称为内存栅栏,是一个cpu指令,作用两个:
1:保证特定操作的执行顺序;
2:保证某些变量的的内存可见性(利用该特性可以保证volatile的内存可见性)

插入内存屏障,禁止在内存屏障前后的指令执行重排序
对volatile 进行写操作时,会在写操作后面加入一条store屏障指令,把工作内存中的变量值重新刷回主内存;
对volatile 进行读操作时,会在读操作后面加入一条load屏障指令,从主内存中读取共享变量;
使用 volatile 变量进行写操作,汇编指令带有 load 前缀,相当于一个内存屏障,后面的指令不能重排到内存屏障之前。

使用 lock 前缀引发两件事:① 将当前处理器缓存行的数据写回系统内存。②使其他处理器的缓存无效。相当于对缓存变量做了一次 store 和 write 操作,让 volatile 变量的修改对其他处理器立即可见。

单例模式volatile分析:

DCL(double check lock 双端检锁机制)DCL不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排序
instance = new DoubleCheckSingleton();//这里由于out-of-order 无序操作

那么问题就来了:必然会做这么些事情

1,给DoubleCheckSingleton分配内存

2,初始化DoubleCheckSingleton实例

3,将instance对象指向分配的内存空间(instance为null了)

private volatile static DoubleCheckSingleton instance = null 每次都从主内存读取instance

final 可以保证可见性吗?

final 可以保证可见性,当一个变量被final修饰,这个变量就不可以修改,就没有可见性问题。

谈一谈 synchronized

锁性质:悲观锁
特点:可以保证原子性,可见性,但是不可以防止指令重排序,但是可以保证有序性(同一时间内只有一个线程在使用资源,又因为as-if-serial原则保证,单进程在执行的时候,其结果和串行执行是一样的,于是保证了有序性)
sync和volatile保证有序性不一样。

被 synchronized 修饰的同步块对一条线程来说是可重入的,并且同步块在持有锁的线程释放锁前会阻塞其他线程进入。从执行成本的角度看,持有锁是一个重量级的操作。Java 线程是映射到操作系统的内核线程上的,如果要阻塞或唤醒一条线程,需要操作系统帮忙完成,不可避免用户态到核心态的转换。

不公平的原因

公平锁优点:不会出现线程饿死(活跃态中饥饿)
非公平锁优点:效率高
两者缺点就是对方的优点
非公平锁会出现新线程抢先占有阻塞队列中线程的执行机会,

锁优化有哪些策略?

锁一共有 4 个状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,状态会随竞争情况升级。锁可以升级但不能降级,这种只能升级不能降级的锁策略是为了提高锁获得和释放的效率。

自旋锁是什么

一个for循环,循环十圈

什么是自适应自旋

JDK6 对自旋锁进行了优化,自旋时间不再固定,而是由前一次的自旋时间及锁拥有者的状态决定。

如果在同一个锁上,自旋刚刚成功获得过锁且持有锁的线程正在运行,虚拟机会认为这次自旋也很可能成功,进而允许自旋持续更久。如果自旋很少成功,以后获取锁时将可能直接省略掉自旋,避免浪费处理器资源。

有了自适应自旋,随着程序运行时间的增长,虚拟机对程序锁的状况预测就会越来越精准。

锁消除是什么?

锁消除指即时编译器对检测到不可能存在共享数据竞争的锁进行消除。

锁细化是什么

让锁的粒度尽可能小,

锁粗化是什么

原则需要将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中进行同步,这是为了使等待锁的线程尽快拿到锁。

但如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之外的,即使没有线程竞争也会导致不必要的性能消耗。因此如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把同步的范围扩展到整个操作序列的外部。

偏向锁是什么、

偏向锁是为了在没有竞争的情况下减少锁开销,锁会偏向于第一个获得它的线程,如果在执行过程中锁一直没有被其他线程获取,则持有偏向锁的线程将不需要进行同步。

当锁对象第一次被线程获取时,虚拟机会将对象头中的偏向模式设为 1,同时使用 CAS 把获取到锁的线程 ID 记录在对象的 Mark Word 中。如果 CAS 成功,持有偏向锁的线程以后每次进入锁相关的同步块都不再进行任何同步操作。

一旦有其他线程尝试获取锁,偏向模式立即结束,根据锁对象是否处于锁定状态决定是否撤销偏向,后续同步按照轻量级锁那样执行。

轻量级锁是什么?(自旋锁)

轻量级锁是为了在没有竞争的前提下减少重量级锁使用操作系统互斥量产生的性能消耗。

当多个线程争用被标记为偏向锁的对象时,偏向锁失效的时候,锁会升级为轻量级锁,争抢的线程自旋10圈,如果回来获取得到锁资源,就还是自旋锁,反之就会升级为重量级锁,阻塞住线程

偏向锁、轻量级锁和重量级锁的区别?

偏向锁的优点是加解锁不需要额外消耗,和执行非同步方法比仅存在纳秒级差距,缺点是如果存在锁竞争会带来额外锁撤销的消耗,适用只有一个线程访问同步代码块的场景。

轻量级锁的优点是竞争线程不阻塞,程序响应速度快,缺点是如果线程始终得不到锁会自旋消耗 CPU,适用追求响应时间、同步代码块执行快的场景。

重量级锁的优点是线程竞争不使用自旋不消耗CPU,缺点是线程会阻塞,响应时间慢,适应追求吞吐量、同步代码块执行慢的场景。

Lock 和 synchronized 有什么区别

重入锁 ReentrantLock 是 Lock 最常见的实现,与 synchronized 一样可重入,不过它增加了一些高级功能:

等待可中断: 持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待而处理其他事情。

公平锁: 公平锁指多个线程在等待同一个锁时,必须按照申请锁的顺序来依次获得锁,而非公平锁不保证这一点,在锁被释放时,任何线程都有机会获得锁。synchronized 是非公平的,ReentrantLock 在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会急剧下降,影响吞吐量。

一般优先考虑使用 synchronized:① synchronized 是语法层面的同步,足够简单。② Lock 必须确保在 finally 中释放锁,否则一旦抛出异常有可能永远不会释放锁。使用 synchronized 可以由 JVM 来确保即使出现异常锁也能正常释放。③ 尽管 JDK5 时 ReentrantLock 的性能优于 synchronized,但在 JDK6 进行锁优化后二者的性能基本持平。从长远来看 JVM 更容易针对synchronized 优化,因为 JVM 可以在线程和对象的元数据中记录 synchronized 中锁的相关信息,而使用 Lock 的话 JVM 很难得知具体哪些锁对象是由特定线程持有的。

ReentrantLock 的可重入是怎么实现的

在ReentrantLock中有一个volatile修饰的state变量,记录了重入次数。
以非公平锁为例,通过 nonfairTryAcquire 方法获取锁,该方法增加了再次获取同步状态的处理逻辑:判断当前线程是否为获取锁的线程来决定获取是否成功,如果是获取锁的线程再次请求则将同步状态值增加并返回 true,表示获取同步状态成功。

成功获取锁的线程再次获取锁将增加同步状态值,释放同步状态时将减少同步状态值。如果锁被获取了 n 次,那么前 n-1 次 tryRelease 方法必须都返回 fasle,只有同步状态完全释放才能返回 true,该方法将同步状态是否为 0 作为最终释放条件,释放时将占有线程设置为null 并返回 true。

对于非公平锁只要 CAS 设置同步状态成功则表示当前线程获取了锁,而公平锁则不同。公平锁使用 tryAcquire 方法,该方法与nonfairTryAcquire 的唯一区别就是判断条件中多了对同步队列中当前节点是否有前驱节点的判断,如果该方法返回 true 表示有线程比当前线程更早请求锁,因此需要等待前驱线程获取并释放锁后才能获取锁。

什么是读写锁?

ReentrantLock 是排他锁,同一时刻只允许一个线程访问,读写锁在同一时刻允许多个读线程访问,在写线程访问时,所有的读写线程均阻塞。读写锁维护了一个读锁和一个写锁,通过分离读写锁使并发性相比排他锁有了很大提升。

读写锁依赖 AQS 来实现同步功能,读写状态就是其同步器的同步状态。读写锁的自定义同步器需要在同步状态,即一个 int 变量上维护多个读线程和一个写线程的状态。读写锁将变量切分成了两个部分,高 16 位表示读,低 16 位表示写。

写锁是可重入排他锁,如果当前线程已经获得了写锁则增加写状态,如果当前线程在获取写锁时,读锁已经被获取或者该线程不是已经获得写锁的线程则进入等待。写锁的释放与 ReentrantLock 的释放类似,每次释放减少写状态,当写状态为 0 时表示写锁已被释放。

读锁是可重入共享锁,能够被多个线程同时获取,在没有其他写线程访问时,读锁总会被成功获取。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取则进入等待。读锁每次释放会减少读状态,减少的值是(1<<16),读锁的释放是线程安全的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值