Sychronized锁和lock锁的实现原理以及两者的区别

sychronized锁的实现原理:

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  1. 普通同步方法,锁是当前实例对象
  2. 静态同步方法,锁是当前类的class对象
  3. 同步方法块,锁是括号里面的对象

同步代码块是使用monitorenter和monitorexit指令实现的,monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;

synchronized的作用:

  1. 确保线程互斥访问同步代码
  2. 保证共享变量的修改能够及时可见
  3. 有效解决指令重排序问题

 

Lock锁的实现原理:ReentrantLock

ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer(简称AQS),线程使用ReentrantLock获取锁分为两个阶段,第一个阶段是初次竞争(ReentrantLock默认使用非公平锁,当我们调用ReentrantLock的lock方法的时候,实际上它调用的是非公平锁的lock(),这个方法先用CAS操作,去尝试抢占该锁。如果成功,就把当前线程设置在这个锁上,表示抢占成功,如果失败,就调用LockSupport.park将当前线程阻塞,将其加入CLH队列中,等待抢占,第二个阶段是基于CHL队列的竞争。(然后进入CLH队列的抢占模式,当持有锁的那个线程调用unlock的时候,会将CLH队列的头结点的下一个节点线程唤醒,调用的是LockSupport.unpark()方法。)在初次竞争的时候是否考虑队列节点直接区分出了公平锁和非公平锁。在基于CLH队列的锁竞争中,依靠CAS操作来抢占锁,依靠LockSupport来做线程的挂起和唤醒,使用队列来保证并发执行变成了串行执行,从而消除了并发所带来的问题。总体来说,ReentrantLock是一个比较轻量级的锁,而且使用面向对象的思想去实现了锁的功能,比原来的synchronized关键字更加好理解

CLH队列:在AQS中CLH队列是维护一组线程的严格按照FIFO的队列CLH队列锁的优点在于空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail)。CLH的变种体被运用到了AQS中。

CAS:CAS是一种系统原语。简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。

Sychronized锁和lock锁的实现原理以及两者的区别_第1张图片

疑问:CAS执行过程具有一系列的操作,多线程并发时会不会造成数据不一致?不会。CAS是一种系统原语,由若干指令组成,原语的执行必须是连续的,在执行过程不过中断,故一定开始执行CAS操作,必然会执行完毕。

优点:无锁操作天生免疫死锁。

缺点:会产生ABA问题,即A被修改成B,然后又被修改成A,不能感知到修改

解决:1、AtomicStampedReference类。2、版本号。

AtomicStampedReference:AtomicStampedReference原子类是一个带有时间戳的对象引用,在每次修改后,AtomicStampedReference不仅会设置新值而且还会记录更改的时间。通过pair私有内部类存储数据和时间戳,并构造volatile修饰的私有实例,一旦volatile引用发生,变化对所有线程可见。compareAndSet方法中,只有期望对象的引用和版本号和目标对象的引用和版本号都一样时,才会新建一个pair对象,然后用新建的pair对象(pair.of(newReference,newStamp))和当前pair对象current做CAS操作。

CAS应用场景:常见的原子类都使用到了CAS。

 

公平锁与非公平锁:

非公平锁是当一个线程请求非公平锁时,如果在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁。

公平锁是如果有另一个线程持有锁或者有其他线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。

非公平锁比公平锁高效的原因是:在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。

 

两则之间的区别:

1、synchronized内置关键字,在JVM层面实现,发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

Lock锁是怎么去避免死锁的?

ReentrantLock的tryLock()方法可以采用非阻塞的方式去获取锁,使用tryLock()方法去设置超时时间,超时可以自动退出防止死锁。

2、Lock具有高级特性:时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票,可以知道有没有成功获取锁

3、在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。到了JDK1.6,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

 

sychronized的锁优化:

1)自旋锁与自适应锁:如果一个物理机器有多个处理器,能让两个或者多个以上的线程并发执行,我们就让 后面请求的那个线程 在不放弃 处理器的执行时间 的前提下去执行 一个忙循环 来等待 持有锁的线程 是否能很快 释放锁。

2)锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是检测到不可能存在共享数据竞争的锁进行消除。

比如局部变量,多线程操作局部变量不会引起线程安全问题,所以在局部变量上就没有必要加锁。

3)锁粗化:如果虚拟机检测到一系列连续的操作都对同一个对象反复加锁和解锁,将会把加锁同步范围扩展到整个操作序列的外部。

比如:A方法中有三个子方法 B、C、D。都加了锁,当一个线程需要访问B、C、D时  可以只获取A的锁,执行完B、C、D后释放锁即可。

4)轻量级锁:比如CAS。在无竞争的情况下,它避免了使用互斥量的开销。

5)偏向锁:在无竞争的情况下,它把整个同步都消除掉,连CAS都不做,当进入同步方法块中时,进入偏向模式,当有另一个线程尝试获取这个锁时,偏向模式宣告失败,锁撤销后进入轻量级锁状态。

 

volatile:

    volatile关键字实现加锁的原理:在当前的java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器中),而不是直接在主存中进行读写,这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中变量的拷贝,造成数据的不一致。

     Lock前缀指令会引起处理器缓存回写到内存。

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

    现在我们有了一个大概的印象就是:被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。

    注意:volatile具备可见性,但不具备原子性。

    volatile适用场景:查看详情

 

 

 

 

 

 

 

你可能感兴趣的:(Java)