这篇文章给大家介绍怎么剖析volatile、synchronized实现原理,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。
在java并发编程中volatile和synchronized都扮演着重要的角色。两者都起到相同的作用:保证共享变量的线程可见性。与synchronized相比volatile可以看做是轻量级的synchronized,没有线程的上下文切换和调试,性能比synchronized要好很多。但需要注意的是volatile变量在复合操作的时候并不能保证线程安全,相反sychronized能。下面从底层看一下volatile、synchronized到底是怎么实现的。
在介绍volatile前,我们先来简单介绍一下java的内存模型
public int i = 1
假设对象有个属性字段i,初始值为1,对象位于堆上。我们通常把堆看作是主内存,此时分别两个两个不同的线程访问字段i。现代操作系统,每个线程都分配有单独的处理器缓存,用这些处理器缓存去缓存一些数据,就可以不用再次访问主内存去获取相应的数据,这样就可以提高效率。看下图
这样做可以提高效率,但同时也带来的一个问题:修改数据时,各线程的数据不一致。
这样做可以提高效率,但同时也带来的一个问题:修改数据时,各线程的数据不一致。
所以这时候我们需要做到当线程1修改一个共享变量时,其它访问该共享变量的线程能够感知到变化。而这个功能volatile可以做到。我们来看下volatile到底是怎样工作的。
volatile 只能用于修饰变量。代码:
volatile public int i = 1;
当volatile变量i被赋值2时,这时线程1会做两件事:
更新主内存。
向CPU总线发送一个修改信号。
这时监听CPU总线的处理器会收到这个修改信号后,如果发现修改的数据自己缓存了,就把自己缓存的数据失效掉。这样其它线程访问到这段缓存时知道缓存数据失效了,需要从主内存中获取。这样所有线程中的共享变量i就达到了一致性。
所以volatile也可以看作线程间通信的一种廉价方式。
在多线程并发编程中synchronized一直扮演着元老级角色,很多人都会称呼它为重量级锁。但是随着Java SE 1.6对synchronized进行各种优化之后,有引起情况下它就并不那重了。下面来详细介绍这方面的内容。
synchronized实现同步的基础是:Java中的每个对象都可作为锁。所以synchronized锁的都对象,只不过不同形式下锁的对象不一样。
对于普通同步方法,锁的是当前实例对象。
对于静态同步方法,锁的是当前类的Class对象。
对于同步方法块,锁是Synchronized括号里配置的对象。
当一个线程试图访问同步代时,它必须先获得锁,才能执行代码逻辑,退出的时候又必须释放锁。那锁到底是怎么实现的呢?
在JVM规范中规定了synchronized是通过Monitor对象来实现方法和代码块的同步,但两者实现细节有点一不样。代码块同步是使用monitorenter和monitorexit指令,方法同步是使用另外一种方法实现,细节JVM规范并没有详细说明。但是,方法的同步同样可以使用这两指令来实现。
monitorenter指令是编译后插入到同步代码块的开始位置,而monitorexit指令是插入到方法结束处和异常处。JVM保证了每个monitorenter都有对应的monitorexit。任何一个对象都有一个monitor与之关联,当且一个monitor被持有后,对象将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象对应monitor的所有权,即尝试获得对象的锁。
那我们一直口口声声说的锁,它到底在哪里呢?远近天边,近在眼前。上面提到对象可以作为锁,其实锁就在对象里面,准确来说是对象头的Mark World结构中。关于对象头的结构具体可参考:林林:聊聊java对象内存布局
这里简单描述一下:
对象头分为两部分:Mark Word 与 Class Pointer(类型指针)。
Mark Word存储了对象的hashCode、GC信息、锁信息三部分,Class Pointer存储了指向类对象信息的指针。在32位JVM上对象头占用的大小是8字节,64位JVM则是16字节,两种类型的Mark Word 和 Class Pointer各占一半空间大小。
下面的图是32位JVM上面的对象头展示。前面25bit是hashCode, 4bit是GC信息,后面两位分别是偏向锁标志与锁状态标志。Mark Word在不同的级别锁时,存储内容会发生变化。
上面提到Java SE 1.6对synchronized进行各种优化,这优化指的是什么呢。指的是synchronized不一定就是重量级锁,它根据锁的重量级分成了三种,由低到高:偏向锁、轻量级锁、重量级锁。上面图就展示了每种锁在Mark Word的存储内容。下面来介绍每种锁的应用场景和升级过程。
偏向锁:经过研究发现,在多线程竞争不激烈的环境或业务中,一个锁总是由同一个线程多次获得。这样的话就没有必要用生成重量级锁和重复加锁了,因为这样代价会很高。所以这时可以通过引入偏向锁来解决这代价过高的问题。具体做法是当一个线程尝试去获取锁时,在对象头和栈帧的锁记录里存储指向当前线程的偏向锁(CAS 操作),同时设置偏向锁标志位为1(CAS 操作)。之后线程再对同一对象加锁,只需要简单测试一下对象头里面是否存储着指向当前线程的偏向锁就可以了,不需要真正执行加锁操作。这时其它线程来尝试获取锁时,CAS操作是获取不了锁的,这时只能等待原先的线程把锁撤销了,才能竞争锁。偏向锁的撤销需要等到全局安全点才能撤销,这就意味着其它线程可能要等很长时间。所以竞争激烈的情况偏向锁就不是很适用。这时应该升级为轻量级锁。
轻量级锁:线程在执行同步块之前,JVM会先在当前线程的的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark World复制到线程栈帧的锁记录空间里面。然后呢,用CAS操作把对象头的Mark World替换为指向锁记录空间的指针。成功就表示获得锁了,失败就暂时自旋一下,等待其它线程解锁。如果自旋到了时间发现还不能获得锁,这时只有两种情况:(1)竞争超激烈 (2)同步代码执行时间太长。这时候如果还自旋是很不划算的,因为不但不能快速获取锁,还会白白浪费了CPU。这种情景轻量级锁就不合适了还不如升级为重量级锁。
重量级锁:所谓的重量级锁,其实就是最原始和最开始java实现的阻塞锁。在JVM中又叫对象监视器。这时锁对象的对象头字段指向的是一个互斥量,所有线程竞争重量级锁,竞争失败的线程进入阻塞状态(操作系统层面),并且在锁对象的一个等待池中等待被唤醒,被唤醒后的线程再次去竞争锁资源。
所以偏向锁、轻量级锁、重量级锁是适用于不同的竞争环境。
关于怎么剖析volatile、synchronized实现原理就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。