从Synchronized说起各种锁

2018-04-08  本文已影响0人  jjjjxd

一、synchronized

   synchronized原理:

     进入和退出 Monitor 对象(每个对象都拥有一个监视器)来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现。当一个monitor被持有后,它将处于锁定状态。线程执行到monitornter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象锁。

 synchronized优化:

    JDK6 中为了提升锁的性能,引入了“偏向锁”和“轻量级锁”的概念,所以在 Java 中锁一共有 4 种状态:无锁、偏向锁、轻量级锁、重量级锁。它会随着竞争情况逐渐升级,锁可以升级但不能降级。下面我们介绍一下这几种锁的特点:

java中的几种锁

    偏向锁:当一个线程访问同步块并获取所时,在对象头和栈帧中的锁记录里存储线程ID,以后进入和退出同步块时不需要CAS操作进行加锁和解锁,只需要在对象头中是否存储着当前线程的偏向锁,如果测试成功说明当前线程已经获取锁。如果测试失败则再测试一下对象头中偏向锁标识是否为1(表示为偏向锁),如果没有的话使用CAS竞争锁,如果设置了,则使用CAS将对象头偏向锁指向当前线程。 

   轻量级锁:其本质是自旋锁,也就是轮询去获取锁,通过竞争激烈程度适当调整自旋次数,以及决定是否升级为重量级锁(自适应)【自旋会消耗CPU】:

         加锁

         当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record)区域,同时将锁对象的对象头中 Mark Word 拷贝到锁记录中,再尝试使用 CAS 将 Mark Word 更新为指向锁记录的指针。如果更新成功,当前线程就获得了锁。如果更新失败 JVM 会先检查锁对象的 Mark Word 是否指向当前线程的锁记录。如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁。

          解锁

        轻量锁的解锁过程也是利用 CAS 来实现的,会尝试锁记录替换回锁对象的 Mark Word 。如果替换成功则说明整个同步操作完成,失败则说明有其他线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为重量锁)

        轻量锁能提升性能的原因是:

认为大多数锁在整个同步周期都不存在竞争,所以使用 CAS 比使用互斥开销更少。但如果锁竞争激烈,轻量锁就不但有互斥的开销,还有 CAS 的开销,甚至比重量锁更慢。

  重量级锁:竞争不用自旋,不需消耗cpu,竞争不到的线程阻塞

二、ReentrantLock

重入锁 (即支持同一个线程重复获取锁)【也是悲观锁】,并支持获取锁时公平和非公平选择(公平即按时间先对锁获取请求的先被满足,公平往往没有非公平的吞吐量高,但是能减少“饥饿”的概率)

ReentrantLock基于AQS实现

三、volatile

原理:使用Lock前缀,Lock前缀在处理器下会引起:

①锁缓存,当缓存发生变化时引起处理器缓存写回到内存【使用缓存一致性确保修改的原子性,缓存一致性机制阻止同时修改由两个以上的处理器缓存的内存区域数据】,

②一个处理器的缓存回写到内存会导致其他处理器的缓存无效

使用场景:状态标志,双重检查等

四、CAS

Compare and Swap,是乐观锁的一种实现方式。

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

CAS三大问题:

①ABA问题:加版本号或时间戳【AtomicStampedReference,维护一个对象的引用和一个状态戳,每次修改引用对象引用时,是一个整数值,每一次修改对象值的同时,也要修改状态戳,从而区分相同对象值的不同状态。当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功】

②自旋时间过长:使用pause指令延迟流水线执行指令,使CPU不会消耗过多的执行资源

③只能保证一个共享变量原子操作,可以通过多个变量合并成一个变量或者使用AtomicReference保证对象引用的原子性

五、AQS

1、AQS是JAVA构建锁和其他同步框架的基础框架,使用一个 volatile int  表示同步状态,通过内置的FIFO队列完成资源获取线程的排队情况

2、通过继承AbstractQueuedSynchronizer 通过如下的3个方法来访问和修改同步状态:

getState():获取当前同步状态

setState():设置当前同步状态

compareAndSetState(int expect,int update):使用CAS设置当前状态,保证状态的原子性

3、同步器可重写的方法:

tryAcquire(int arg):独占获取同步状态,然后通过CAS设置同步状态

tryRelease(int arg):独占释放同步锁状态,等待获取同步状态的线程有机会获取同步状态

独占锁获取伪代码【摘自并发变成网】

tryAcquireShared(int arg):共享获取同步状态,返回大于0的值表示获取成功。反之获取失败

tryReleaseShared(int arg):共享释放同步状态

isHeldExclusively():判断当前同步器是否在独占模式下被线程占用

4、同步器的模板方法

void acquire(int arg):独占方式获取同步状态,如果线程获取同步状态成功则成功返回。否则将进入同步队列等待。该方法调用重写的tryAcquire()

void acquireInterruptibly(int arg):与auquire相同,但是该方法响应终端,即在同步队列中如果线程被中断,则方法会抛出中断异常。

tryAcquireNanos(int arg, long nanos): acquire增加超时限制

acquireShared(int arg):共享获取同步状态

acquireSharedInterruptibly(int arg):该方法会响应中断

tryAcquireSharedNanos(int arg,long nanos):acquireShared增加超时限制

release(int arg) 同步释放锁,并将同步队列中的第一个节点线程唤醒

releaseShared(int arg)共享释放同步状态

5. 队列同步器实现原理

  同步器内部维护一个同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程及等待信息构造成一个节点并加入到同步队列尾部,同时阻塞当前线程。当同步状态释放时,会把首节点的线程唤醒。使其再次尝试同步状态。加入队列使用CAS【不断循环知道加入成功】,即将并行转为串行。

节点加入同步队列

首节点线程释放同步状态,唤醒后继节点。而后继节点获取同步状态,将自己设置为首节点(不需要CAS只有一个获取同步状态成功线程设置首节点,将首节点设置为原首节点后继节点并断开原首节点的next引用即可)

同步器acquire方法:使用tryAcquire方法获取同步状态,如果获取失败执行acquireQueued方法。首先执行addWaiter方法

acquire方法

addWaiter() :使用CAS将当前节点加入到队尾。首先尝试快速添加,尝试失败后使用enq将请求串行cas添加

addWaiter方法 end方法

acquireQueued:节点加入到同步队列中,就进入一个自旋的过程。当获取到同步状态则从自旋过程中退出。否则则一直自旋(并阻塞当前节点线程)。方法中,只有在当前节点前驱节点为头结点时才尝试获取同步状态。原因①头结点是获取同步状态成功的节点,头结点释放同步状态后,会唤醒其后继节点,后继节点被唤醒时检查自己的前驱节点是否是头结点。②维护同步队列FIFO原则

acquireQueued


release:释放同步状态,唤醒后继节点

 

ReentrantLock公平锁实现:

与非公平锁相比 多了一个在当前同步状态为0时首先检验hasQueuedPredecessors()方法,即加入同步队列的当前节点是否有前驱节点判读,如果有则表示有线程比当前线程更早的请求锁,因此需要等待前驱线程获取并释放锁才获取锁。

tryAcquire-fair


hasQueuedPredecessors


相比非公平锁,公平锁保证锁按照FIFO原则,代价是大量线程切换。非公平锁可能造成饥饿现象,但是极少的减少线程切换,保证更大的吞吐量

LockSupport

利用LockSupport的静态方法park可以阻塞线程,unpark(thread)可以唤醒被阻塞的thread线程

六、其他

synchronized和lock对比:

    synchronized:方便

   ReentrantLock:锁中断。需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程。 锁公平

七、ThreadLocal


上一篇下一篇

猜你喜欢

热点阅读