关于Lock、synchronized、volatile原理及区别

1.volatile

大多数人都知道volatile一个是保证多线程并发时的内存的可见性,还有一个就是禁止指令重排序,那么什么是内存的可见性呢?JMM模型规范了所有的变量(这里指分配对象之类的共享变量),必须通过主内存与线程工作内存通信。

但是这里会存在一个问题,如果多线程并发的情况下,有两个线程同时对a++操作了,在没有正确同步的情况下,那么有可能就会出现a=2情况,产生这种情况是因为JMM规范线程读取变量的时候都必须经过主内存,然后存储到本地的副本中,如果线程1读取a=1到自己的线程副本中,进行a++操作,但是此时还没有把数据写回主内存,线程2也可开始了从主内存读取a=1的值,那么这种情况下,两个线程的累加并不是我们预期的a=3值。那么这里我们是不是直接用volatile修饰这个变量就会让a++操作并发安全了呢?这里其实还是埋了个坑,我还是需要介绍一下指令重排序的概念,我们java的字节码文件经过JVM加载执行最后都会编译成机器语言如汇编,我们方法的代码执行会变成一条条指令,但是硬件层在某些时候为了提高CPU效率会对一些没有数据依赖的指令进行重排序,就比如a=1,b=2.如果根据程序次序规则的happens-before关系,a=1的指令会先于b=2的指令执行,但是CPU有可能将先将b=2执行了,而且这里JMM规范对这种情况并没有要求,JMM规范里在程序处理结果跟顺序执行一致的情况下,并没有要求A happens-before B,就一定要A先执行,它只要求A执行的结果对B是可见的,也就是不影响最后程序的结果它都是允许的。

如上图是一个DCL单例创建,如果不适用volatile修饰,我们获取初始化一半的对象,这里因为new SingletonDemo()创建对象时不是一个原子操作,当JVM遇到new指令时,假设步骤为1.在主内开辟对象空间,2.执行对象初始化操作,3.将对象地址赋值给栈内存的变量instance.因为我们使用的是synchronized,代码进入临界区后,JMM规范允许临界区的代码重排序,所以这里有可能将步骤3执行在步骤2的前面。这样就有可能在并发的情况下,其他线程读取到了未初始化完毕的对象。那么如果用volatile修饰instance变量禁止指令重排序后,我们就可以保证单例的并发安全了。

这里补充一点,被volatile修饰的变量,底层编译成汇编的时候,会多一个Lock前缀指令,该指令主要作用是,将写缓冲区刷回内存,还有就是如果支持缓存锁定,就锁定该内存的地址或者锁总线,让内存中的变量变成某个cpu独享状态,然后通过MESI缓存一致性来保证线程间的内存可见性,还有它禁止了内存屏障内的指令重排序。其实这些就是JMM为了保证votatile的语义的硬件实现。

 

2.synchronized

JVM实现的锁,当我们对共享资源进行修改时,为了保证并发安全性,通俗的说,就是我们对共享资源上一把锁,只有拿到锁的线程才能进入,然后线程出来之后,还要上交这把锁。关于JVM如何实现的呢?

synchronized修饰成员方法时,锁的对象就是this,修饰类方法时,锁的对象就是this.getClass(),修饰代码块时,锁的对象就是()里的对象,然后我们所有的对象其实都是有一个ObjectMonitor对象关联的,当我们线程要获取锁时,就是获取这个对象,那么当多个线程争取monitor对象时,只会有一个线程成功,其他竞争失败的对象会进入一个entry-list队列里阻塞等待抢锁成功的线程释放锁,如果持有锁的线程调用wait()方法,那么它将进入一个wait set的队里并且释放锁,等待其他线程对它的notify.这里其实JVM还有实现,释放锁后对entry-list队列里的阻塞线程进行唤醒操作。因为java线程是需要操作系统转入内核进行唤醒和上下文切换的,所以才说synchronized是重量级锁

但是从JDK1.6之后,synchronized进行了多种优化操作,其中就是偏向锁,轻量级锁。偏向锁就是通过设置对象的markwold里的锁标志位,以及记录当前获取锁的线程ID,通过CAS操作替换markwold,如果替换成功则获取锁。轻量级锁其实实现就是自旋,当一个线程获取锁时,会判断前面线程是否通过自旋获取过锁,如果前面的线程通过自旋成功获取锁,那么这里也会通过自旋等待来获取锁,这里的缺点就是自旋空转浪费CPU处理器时间,如果应用程序中都是存在并发而且执行锁操作时间较长的情况下的,关闭自旋操作可能效率更高。

 

3.Lock

Lock锁时JDK层面实现的,通过AQS同步器实现的。底层原理是通过硬件提供的原子操作compareAndswap指令实现的一种lock free(无锁化),它有reentrantLock(重入锁),ReentrantReadWriteLock(读写锁)。其实都是通过基础的同步器AQS实现的。

AQS采用了模板设计,封装了等待线程入队列以及出队列的操作,读写锁对信号量设计的更巧妙。采用了高16位代表读锁获取的次数,低16位代表写锁获取的次数,来进行一个读写分离。

 

4.它们的区别

volatile是轻量级的锁,它能保证变量的内存可见性以及禁止重排序,Lock是JDK层面通过循环cas操作的lock free的锁,这里需要注意应用场景,如果资源一定会存在并发的情况下,而且处理也比较耗时的话,不建议使用Lock接口,使用JVM原生的synchronized效率应该更好些,synchronized是JVM底层实现的锁,虽然做过一系列优化,尽管也会使用cas以及自旋做一些优化锁,但是一旦膨胀为重量级锁之后,是无法降级的,也就是必须通过系统的阻塞与唤醒线程,是比较消耗资源的行为了。所以我们在锁选型的时候,应该侧重各自的优缺点以及应用场景去实现。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值