本文向您介绍如何分析可变和同步实现的原理。内容非常详细。感兴趣的朋友可以参考一下,希望对你有所帮助。
00-1010 volatile和synchronized在java并发编程中起着重要的作用。两者扮演着相同的角色:确保共享变量的线程可见性。与synchronized相比,volatile可以视为轻量级的synchronized。没有上下文切换和线程调试,性能比同步好得多。但是需要注意的是,在复合操作中,volatile变量不能保证线程安全,而sychronized变量可以。让我们从底层来看看易变和同步是如何实现的。
前言
在介绍volatile之前,我们先简单介绍一下java的内存模型。
Inti=1假设对象有一个初始值为1的属性字段I,并且对象在堆上。我们通常认为堆是主内存,此时,两个不同的线程分别访问字段I。在现代操作系统中,每个线程都被分配了一个单独的处理器缓存。如果用这些处理器缓存来缓存一些数据,就可以在不再次访问主存的情况下得到相应的数据,可以提高效率。看下面的图片
这可以提高效率,但也带来了一个问题:修改数据时,各线程的数据不一致.
这样可以提高效率,但也带来了一个问题:修改数据时,每个线程的数据不一致。
所以此时,我们需要确保当线程1修改一个共享变量时,访问该共享变量的其他线程能够感知到这个变化。而这个函数volatile可以做到。让我们看看易变性是如何工作的。
Volatile只能用来修饰变量。代码:
易失性公共int I=1;
当可变变量I被赋值为2时,线程1将做两件事:
更新主内存。
向CPU总线发送修改信号。
此时,在接收到这个修改信号后,监控CPU总线的处理器如果发现修改后的数据是自己缓存的,就会使缓存的数据无效。这样,当其他线程访问这个缓存时,就知道缓存数据是无效的,需要从主内存中获取。这样,所有线程中的共享变量I是一致的。
所以volatile也可以看作是一种廉价的线程间通信方式。
00-1010 synchronized一直在多线程并发编程中扮演着老手的角色,很多人会称之为重量级锁。然而,随着Java SE 1.6中同步的各种优化,在某些情况下并没有那么重。下面我们来详细介绍一下这方面。
Synchronized是基于这样一个事实,即Java中的每个对象都可以用作锁。因此,同步锁都是对象,但是以不同形式锁定的对象是不同的。
对于通用同步方法,当前实例对象被锁定。
对于静态同步方法,当前类的Class对象被锁定。
对于同步方法块,锁是在同步括号中配置的对象。
当线程试图访问同步生成时,它必须在执行代码逻辑之前获得锁,并且在退出时必须释放锁。锁是怎么实现的?
JVM规范中规定synchronized是通过Monitor对象实现方法和代码块的同步,但两者的实现细节略有不同。代码同步通过使用monitorenter和monitorexit指令实现,方法同步通过另一种方法实现,这种方法在JVM规范中没有规定。然而,方法的同步也可以通过使用这两条指令来实现。
monitorenter指令在编译后插入同步代码块的开始位置,而monitorexit指令插入方法和异常的末尾。JVM确保每个monitorenter都有一个相应的monitorexit。每个对象都有一个与之关联的监视器。当手持监视器时,对象将被锁定。线程执行monitorenter指令时,会尝试获取对象对应的监视器的所有权,即尝试获取对象的锁。
我们一直说的锁,在哪里?远近,就在眼前。上面提到的对象可以用作锁,但实际上锁在对象中,准确地说,它在对象头的Mark World结构中。对象头的结构请参考:琳琳:说说java对象的内存布局。
这里简单描述一下:
标题分为两部分:标记字和类指针。
Mark Word存储对象的hashCode、GC信息和锁信息,Class Pointer存储指向类对象信息的指针。在32位JVM上,对象头的大小是8字节,而64位JVM的大小是16字节,两种类型的标记字和类指针各占一半的空间大小。
下图显示了32位JVM上方的对象头。前25位为hashCode,4位为GC信息,后两位分别为偏置锁定标志和锁定状态标志。标记单词
在不同的级别锁时,存储内容会发生变化。
上面提到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实现原理就分享到这里了,希望
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/93990.html