文章目录
并发编程面试文章地址链接
内容 | 博客链接 |
---|---|
并发编程面试题之常见面试题 | https://blog.csdn.net/weixin_38251871/article/details/104658674 |
并发编程面试题之 volatile 关键字 |
https://blog.csdn.net/weixin_38251871/article/details/104667384 |
并发编程面试题之 CAS |
https://blog.csdn.net/weixin_38251871/article/details/104667406 |
并发编程面试题之锁 | https://blog.csdn.net/weixin_38251871/article/details/104667392 |
并发编程面试题之阻塞队列 | 待完成… |
并发编程面试题之 AQS |
待完成… |
并发编程面试题之线程池 | 待完成… |
并发编程面试题之 synchronized 和 ReentrantLock 的区别 |
https://blog.csdn.net/weixin_38251871/article/details/104667532 |
并发编程面试题之 CyclicBarrier、CountDownLatch、Semaphore |
待完成… |
并发编程面试题之 ConcurrentHashMap |
https://blog.csdn.net/weixin_38251871/article/details/104667433 |
并发编程面试题之 synchronized 实现原理 |
https://blog.csdn.net/weixin_38251871/article/details/104667415 |
Java 锁
乐观锁
乐观锁:
顾名思义就是很乐观;它是一种思想,认为读多写少,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间是否有人去更新这个数据。采取在写之前先读取出当前的版本号,然后进行加锁操作【和上一次的版本号进行比较,一样则更新,失败需要重复读-比较-写
的操作】Java
中的乐观锁是通过 CAS 操作实现的, CAS 是一种更新的原子操作,比较当前的值和传入的值是否一致,一致则更新,否则失败
乐观锁的实现方式
- 使用版本标识来确定读到的数据与提交时的数据是否是一致,提交后修改版本标识,当不一致的时候可以采取丢弃和再次尝试的策略
- 使用
CAS
操作, 详情查看 并发编程面试题之 CAS
悲观锁
悲观锁:
它是一种悲观思想,认为写多读少,每次去拿数据的时候都会认为别人会修改,所以在每次读写数据的时候都会上锁,这样别人想读写这个数据就会阻塞,直到拿到这个数据,传统的关系型数据库里用到了很多这种锁机制【例:行锁、表锁
】
公平锁
公平锁(Fair):
加锁钱检查是否有排队等待的线程,优先排队等待的线程, 先到先得
非公平锁
非公平锁(NonFair):
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾去等待
共享锁
共享锁:
允许多个线程同时获得锁,并发地访问共享资源,共享锁是一种乐观锁,它放宽了加锁策略,允许多个读操作的线程同时访问共享资源AQS
的内部类 Node 定义了两个常量SHARED
和EXCLUSIVE
,它们分别表示AQS
队列中等待线程的锁获取模式Java
中的并发包提供了ReadWriteLock(读写锁)
,它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但是两者不能同时进行
独占锁
独占锁:
每次只能有一个线程能持有锁,ReentrantLock
就是通过独占方式实现互斥锁,独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取到了锁,则其他的读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不多影响数据的一致性
读写锁
- Java 提供了
读写锁
用来提升并发程序性能的锁分离技术的成果, 在读的地方使用读锁, 写的地方使用写锁. 如果没有写的情况下, 读是无阻塞的, 这种读写分离的操作在一定程度上提高了程序的执行效率
自旋锁
- 如果持有锁的线程能在短时间内释放锁资源, 那么那些等待竞争锁的线程就不需要做
内核态
和用户态
之间的切换进入阻塞挂起状态, 它们只需要自旋等待持有锁的线程释放锁后即可立即获取锁, 这样就会避免用户线程和内核的切换消耗
自旋锁的优缺点
优点 :
尽可能地减少线程的阻塞, 对于锁竞争不激烈的情况下, 占用锁时间非常短的代码块来说性能可以大幅度的提升, 因为自旋的消耗会小于线程阻塞挂起再唤醒操作的消耗, 这些操作会导致线程发生两次上下文切换缺点 :
如果锁竞争激烈的情况下, 或者持有锁的线程需要长时间占用锁执行代码块, 在这种情况下就不太适合使用自旋锁, 因为自旋转在获取到锁之前一直都是占用着CPU
资源, 同时有大量的线程在竞争锁, 会导致获取锁的时间很长, 线程自旋的消耗会大于线程阻塞挂起再唤醒操作的消耗, 其他需要CPU
的线程又获取不到CPU
, 造成了CPU
资源的浪费.- 在
JDK 1.6
引入了适应性自旋锁, 意味着自旋的时间不再是固定的, 而是由前一次在同一锁上的自旋时间以及锁的拥有者的状态来决定
可重入锁(递归锁)
- 在同一线程, 在外层方法获取到锁之后,内层递归方法仍然有获取该锁的代码,但不受影响。在 Java 中,
ReentrantLock/synchronized
都是可重入锁
同步锁
synchronized
可以把任意一个非NULL
的对象当做锁,属于独占式的悲观锁,同时属于可重入锁
同步锁的作用范围
- 作用于方法的时候,锁住的是对象的实例
(this)
- 作用与静态方法的时候,因为 Class 的相关数据存储在元空间
(jdk1.8)
,metaspace
是全局共享的,因此静态方法就相当于一个全局锁,会锁住调用该方法的所有线程 - 作用于一个对象实例的时候,锁住的是所有以该对象为锁的代码块
偏向锁
锁的状态 :
无锁状态、偏向锁、轻量级锁、重量级锁
锁升级 :
随着锁的竞争, 锁可以从偏向锁升级到轻量级锁, 再升级到重量级锁(单向升级)
- 偏向锁的目的是在于某个线程获取到锁之后, 消除这个线程重入
CAS
的开销, 看起来这个线程得到了偏护, 引入偏向锁的目的是为了在没有多线程竞争的情况下尽量减少不必要的轻量级锁执行路径, 因为轻量级锁的获取及释放依赖多次CAS
原子指令, 而偏向锁只需要置换ThreadID
的时候依赖一次CAS
原子指令 偏向锁
的目的是为了在只有一个线程执行同步块的时候提高性能轻量级锁
的目的是为了在线程交替执行同步块的时候提高性能
轻量级锁
- 轻量级锁并不是代替重量级锁的, 它的本意是在没有多线程竞争的前提下, 减少重量级锁使用产生的性能消耗. 它所适应的场合是线程交替执行同步块的情况下, 如果存在同一时间访问同一个锁的情况, 就会导致轻量级锁升级为重量级锁
重量级锁
synchronized
是通过对象内部的一个叫做监视器锁monitor
来实现的, 但是监视器锁本质是依赖于底层操作系统的 Mutex Lock 来实现的, 而操作系统实现线程之间的切换就需要从用户态转为内核态, 状态之间的转换需要相对较长的时间, 这就是synchronized
效率低的原因, 这种依赖于操作系统Mutex Lock
实现的锁, 我们就称为重量级锁
分段锁
- 分段锁并非是一种实际的锁, 而是一种思想, 可以去看一下 ConcurrentHashMap
锁优化
- 减少锁持有的时间
- 减小锁的粒度【例:
ConcurrentHashMap
】 - 锁分离【例:
ReadWriteLock
】 - 锁粗化: 通常为了保证多线程之间的并发效率, 会要求每个线程持有锁的时间尽量短, 使用完公共资源后立即释放锁. 粗化的粒度需要自己衡量, 如果对同一个锁不断地发出请求, 获取和释放锁, 这样也会消耗系统资源, 反而影响性能
- 锁消除: 在
JIT
时, 如果发现不能被共享的对象, 就可以消除这些对象的锁操作