关于Synchronize锁
Synchronize锁原理
synchronized 在代码块上是通过 monitorenter 和 monitorexit指令实现,在静态方法和方法上加锁是在方法的flags中加入 ACC_SYNCHRONIZED。
偏向锁概念
- 就是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。
- 偏向锁只适用于只有一个线程反复进入同步代码块的场景,可以提高带有同步但无竞争的程序性能。
Synchronize偏向锁原理
- 线程第一次执行同步代码块时,先将对象头中的偏向锁的标记改为1。
- 使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,退出同步代码块。
- 这个线程第二次执行同步代码块时,只需要判断Mard Word中的线程ID和要获取锁的线程的ID是否相同,一样,就直接进来,效率很高。
Synchronize锁的撤销/升级
- 在安全点暂停拥有锁的线程,判断锁对象是否处于被锁定状态。
- 修改锁对象头标志位,撤销锁或者升级锁并唤醒线程。
Synchronize轻量级锁原理
线程并不立即堵塞,而是自旋尝试获取锁。适用于那些同步代码块执行的很快,竞争并不激烈的场景
Synchronize重量级锁
依赖操作系统的MutexLock(互斥锁)来实现的,未获取到锁的线程会立即堵塞,堵塞的线程虽然不会消耗CPU,但是唤醒和堵塞需要操作系统来帮忙,需要从用户态转系统态,这可能需要消耗比执行代码更多的时间。
Synchronize锁升级发生的时机
- 第一次执行同步代码块时(无竞争),获取偏向锁
- 两个线程来竞争锁时,升级为轻量级锁
- 自旋超过指定次数依旧未获取到锁或者又来一个线程来竞争锁时,升级为重级锁。
关于Lock锁
公平性锁和非公平性锁
所谓公平锁,线程将按照他们发出请求的顺序来获取锁,不允许插队;但在非公平锁上,则允许插队:当一个线程发生获取锁的请求的时刻,如果这个锁是可用的,那这个线程将跳过所在队列里等待线程并获得锁。
ReentrantLock加锁过程
- cas尝试加锁
- 加锁失败加入包装成node加入队尾
- 自旋加锁/设置信号后暂停线程
- 异常发生或中断时的出队操作
- 线程中断处理
ReentrantLock的tryLock()和tryLock(long timeout, TimeUnit unit)
- tryLock()无论是公平模式还是非公平模式,一律使用非公平方式获取锁
- tryLock(long timeout, TimeUnit unit)会根据模式,使用对应的方式获取锁
ReentrantReadWriteLock如何维护状态
- ReentrantReadWriteLock内部维护的读写状态是由32位码表示,高16位为读状态,表示持有读锁的线程数,低16位为写状态,表示写锁的重入次数 ,状态的改变通过AQS实现,保证同步
锁饥饿问题
由于非公平模式入队前都会尝试获取锁,在读多写少情况下,会有持续不断的共享读锁的获取被响应,于是写锁线程一直被堵塞
Condition
- 必须先获取到锁。
- await 加入条件队列,并堵塞当前线程。作用同Object.wait()
- signal 唤醒条件队列的队首线程。作用同Object.notify()
为什么await、signal要先获取锁
- 代码层面,await执行时会强制执行释放锁的操作,如果没有锁自然就会导致异常
- await、signal操作的目的是基于某种条件, 协调多个线程间的运行状态,由于涉及到多个线程间基于共享变量的相互通信, 必然需要引入某种同步机制, 以确保wait(), notify() 操作在线程层面的原子性
StampedLock乐观读锁的使用
- 尝试获得一个乐观读锁tryOptimisticRead()
- 业务读逻辑
- 验证获取乐观读锁后是否有写锁发生validate(stamp)
- 有则自旋/转悲观锁,无则返回结果
StampedLock同ReentrantReadWriteLock的区别
- 乐观读,StampedLock在读操作时不会阻塞写操作
- 乐观读,StampedLock避免了写饥饿情况的发生
- 不可重入,StampedLock写锁重复获取会导致死锁
StampedLock锁标示
- state 二进制数前7位为读锁,后面都为写锁标记
- 写锁的获取和释放不同于ReentrantReadWriteLock的+1、-1, StampedLock只会加1000 0000(128),该目的主要是为了乐观锁以及解决ABA问题
关于CAS和volatile
什么是CAS
意为比较并交换,至少需要三个参数,分别为内存地址A,旧的预期值E,需要修改的新值U。步骤为:
- 获取地址A上的值V。
- 比较 V 与 E 是否相等。(比较)
- 如果比较相等,将 U 写入 A。(交换)
- 返回操作是否成功。
CAS的原理
- java 的 cas 利用的的是 unsafe 这个类提供的 cas 操作。
- unsafe 的cas 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg
- Atomic::cmpxchg 的实现使用了汇编的 cas 操作,并使用 cpu 硬件提供的 lock信号保证其原子性
CAS的ABA问题
ABA 的问题,就是一个值从A变成了B又变成了A,使用CAS操作不能发现这个值发生变化了,处理方式是可以使用携带类似时间戳的版本AtomicStampedReference
CPU如何实现原子操作
缓存锁定机制,也就说当某个处理器对缓存中的共享变量进行了操作,其他处理器会有个嗅探机制,将其他处理器的该共享变量的缓存失效,待其他线程读取时会重新从主内存中读取最新的数据。
AtomicStampedReference如何解决ABA问题
- 携带版本号,即每次compareAndSet操作的时候都会根据版本号和对象引用生成Pair对象。
- 将A->B->A 情况优化成了A1->B->A2,因此比较失败,交换就不会进行
volatile的原理
- volatile修饰的共享变量进行写操作的时候会多出内存屏障指令
- 内存屏障指令会引起处理器缓存写回内存;
- 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
- 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。
内存屏障
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的前面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
解释 - LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
举例说明volatile的原理
Thread-A写了变量i
- Thread-A发出LOCK#指令
- 发出的LOCK#指令锁总线(或锁缓存行),同时让Thread-B高速缓存中的缓存行内容失效
- Thread-A向主存回写最新修改的i
Thread-B读取变量i
1.Thread-B发现对应地址的缓存行被锁了,等待锁的释放,缓存一致性协议会保证它读取到最新的值
关于volatile的happens-before
volatile变量的写happens-before 于任意后续对volatile变量的读
volatile的指令重排序
- 通过内存屏障实现的
- volatile写是在前面和后面分别插入内存屏障(前写后读),而volatile读操作是在后面插入两个内存屏障
双重判断锁实现的单例模型为甚么要用volatile修饰
Instance instance = new Instance()
粗略来说有3个步骤
- 分配内存空间
- 初始化对象
- instance指向分配的内存空间
其中2和3可能发生重排序,导致获取到的单例对象未初始化。
volatile和synchronized比较
原子性 | 有序性 | 可见性 | |
---|---|---|---|
synchronized | 是 | 是 | 是 |
volatile | 否 | 是 | 是 |
关于AQS
AQS关键方法说明(搭配理解)
getState():返回同步状态的当前值;
setState(int newState):设置当前同步状态;
compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性;
tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态
tryRelease(int arg):独占式释放同步状态;
tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败;
tryReleaseShared(int arg):共享式释放同步状态;
isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占;
acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;
acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;
tryAcquireNanos(int arg,long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;
acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断;
tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制;
release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;
releaseShared(int arg):共享式释放同步状态;
共享锁和独占锁获取锁时的区别
- 共享锁可以被多个线程拥有,所以当一个节点获取到共享锁后会继续唤醒后继节点去获取锁。
- 独占锁只能被一个线程拥有,所以当一个节点获取到独占锁后没有必要再唤醒它的后继节点了
关于LockSupport
LockSupport有什么用
- park和unpark方法可以替代wait和notify的功能,而且不会造成死锁现象
- blocker入参能够再dump线程的时候看到阻塞对象的信息
LockSupport同wait/notify的区别
- wait和notify都是Object中的方法,在调用前必须先获得锁对象,LockSupport不用
- notify无法唤醒指定的线程,LockSupport可以
LockSupport的park方法和unpark方法执行顺序颠倒为什么不会有问题
- 主要依赖于UnSafe类的park和unpark底层的实现,它实际上维护着一个_counter的许可证。
- unpark时将许可证设置为允许,并唤醒线程(如果有的话)
- park时如果许可证为允许,那就不再堵塞线程。
关于Semaphore
Semaphore的原理分析
- 底层通过AQS的功效锁实现,state即为令牌数
- acquire获取令牌,获取不到时堵塞线程,并加入同步队列
- release释放令牌,并依次唤醒同步队列中的堵塞线程去获取令牌,直到无令牌可用
关于CyclicBarrier
CyclicBarrier的原理分析
- 底层通过ReentrantLock加锁。
- 实例化时需要确定栅栏的数量及达标时的任务
- 当调用await的线程数未达到指定数时,堵塞当前线程
- 当调用await的线程数达到指定数时,就执行指定任务,后将堵塞线程唤醒并将CyclicBarrier对象复原以便再次利用
关于CountDownLatch
CountDownLatch的原理分析
- 底层通过AQS的共享锁实现,state为计数器的值
- 实例化时需要确定计数器的值
- 调用await时,将当前线程堵塞,并加入同步队列
- 调用countDown时,就将计数器的值减1,当计数器的值为0时,依次唤醒同步队列的线程