java多线程02-----------------synchronized底层实现及JVM对synchronized的优化

  提到java多线程,我们首先想到的就是synchronized关键字,它在我们多线程学习的前期帮助我们解决了绝大多数的问题,但是大多数包括我在内的很多开发人员并不了解它的实现机制,最近看了一些关于synchronized的博文和书籍,在此总结一下。

  我们都知道,当一个线程执行synchronized代码块或方法前,都必须获得锁,那么究竟这个锁在什么地方?

  想要了解这个问题,我们首相得了解一下对象头。

  在jvm中,对象在内存中可以分为三块区域:对象头、实例数据和对齐填充数据。其中java对象头就是实现synchronized的基础。它主要又两部分组成:

  1) Mark Word:主要存储对象的HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。

  2)Class Pointer类型指针:是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

  java对象头与锁相关的信息如下所示:

  在这里与synchronized相关的就是对象头的Mark Word部分中的重量级锁(标志位为10),java语言为每一个对象设置了一个与之关联的monitor对象,对象头中的重量级锁的指针指向的就是这个monitor对象。在使用synchronzed之后,线程在执行同步代码块之前要获得该monitor对象,当同步代码块执行完之后,释放monitor对象。

(一)synchronized代码块分析

  如下面一段synchronized代码。

public class SynchronizedDemo {
private static int i = 0; public void appender() {
synchronized(SynchronizedDemo.class) {
i++;
}
}
}

  经过javap –v SynchronizedDemo之后反编译后的查看字节码文件如下:

  monitorenter和monitorexit两个命令就是在使用synchronized关键字后独有的命令,分别的含义为对对象的监视器monitor进行获取和释放。当执行monitorenter指令时,当前线程试图获得同步对象的monitor的持有权,当monitor的计数器为0时,可成功获得monitor,并将计时器的值设置为1,这样以来,该线程了就获得了同步对象的monitor,如果该线程已经获得了monitor,他可以重人这个monitor,重入时这个monitor的计数器也会加1,monitorenter和monitorexit指令成对执行。当其他线程发现monitor的计数器不为0时,即其他线程拥有该对象的monitor的持有权时,会被阻塞,直到该线程执行完所有的monitorexit命令,将计数器重新置为0时,其他线程才有机会持有monitor。上图中之所以出现一个monitor却出现两个monitorexit的原因是java语言为保证monitorenter和monitorexit成对出现而在编译时为自动生成一个异常处理器,就是为了保证即使在异常发生时也可以执行monitorexit命令,释放该线程对monitor的持有权。

(二) synchronized方法分析

  如下一段代码:

  对于同步方法,无需通过字节码命令来实现。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

(三)synchronized与happens-before的关系

  Java语言中的happens-before关系中有一条内部多规则(监视器规则),它支出,内部锁的释放happens-before后续每一个对该锁的申请,即锁释放之前的执行结果对后序锁申请操作是可见的。

  Synchronized的happens-before关系如下图所示:

  图中每一个箭头连接的两个节点就代表他们之间的happens-before关系,黑色箭头是根据happens-before中的程序顺序规则推到出的,蓝色为内部锁(监视器)规则推导出的,红色蓝色是根据传递性规则推导出的。此时就可以得出,操作2 happens-before操作5,根据happens-before的含义,就可知,操作2的执行结果对操作5可见,操作2的执行顺序先于操作5.

(四)锁获取和所释放的内存含义

  获得锁:该线程从主内存中将共享变量的值加载入本地内存(刷新处理器缓存),保证线程读到的共享变量的值为最新值。

  释放锁:对共享变量的更新刷新到主内存(冲刷处理器缓存)。

(五)synchronized与等待唤醒机制

  wait()、notify()和notifyAll()方法必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。

  需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。

(六)JVM对synchronized的优化

  需要注意的是,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。从jdk1.6之后,java官方从jvm层对synchronized做了大量优化,引入了轻量级锁、偏向锁、自旋锁等概念,大大提高了synchronized的效率。

  锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

  1)锁消除(Lock Elision):

锁消除是JIT编译器对内部锁的具体实现所做的一种优化,在动态编译同步代码块时,JIT编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的所对象是否只能够被一个线程访问而没有被发布到其他线程。如果同步代码块所使用的锁对象通过这种分析证实只能被一个线程访问,那么JIT编译器在编译这个同步代码块的时候并不生成synchronized所表示的锁申请与释放所对应的机器码(monitorenter、monitorexit),这种编译器优化被称为锁消除。

Java标准库中有些类虽然是线程安全的,但是在实际使用中,我们往往不在多个线程间共享这些类的实例,而这些类实现线程安全到时候往往借助于内部锁。这些类就是锁消除优化的常见目标。

在锁消除的作用下,利用ThreadLocal将一个线程安全的对象作为一个线程特有对象来使用,不仅可以避免锁的征用,而且还可以彻底消除这些对象内部所使用的锁的开销。

  2)锁粗化(Lock Merging):

锁粗化是JIT编译器对内部锁具体实现所做的一种优化,对于相邻的同步代码块,如果这些同步块使用的是同一个锁实例,那么JIT编译器会将这些同步块合并成一个大的同步块,从而避免了一个线程反复申请释放同一个锁导致的开销。然而锁粗化会导致一个线程持有一个锁的时间变长,从而使同步在该锁上的线程申请锁等待的时间变长。

  3)偏向锁(Baised Locking):

  偏向锁是java虚拟机对锁实现的一种优化,这种优化基于这样的观测结果:大多数锁并没有被争用,并且这些锁在其整个生命周期内至多只会被一个线程持有。然而java虚拟机在实现monitorenter和monitorexit字节码时需要借助于一个CAS操作,这个操作的代价相对来说比较昂贵。因此,java虚拟机为每个对象维护一个偏好(Bias),即一个对象对应的内部锁第一次被一个线程获得时,那么这个线程就会被记录为该对象的偏好线程(会在对象头的锁记录中存储锁偏向的线程ID),这个线程后续无论是申请锁还是释放锁,都无须借助于原先的原子操作,只需要检测一下对象头的Mark Word中是否存储了指向当前线程的偏向锁,如果测试成功,表示当前线程获得了该锁。

一个锁没有被争用,并不代表仅仅只有一个线程访问该锁,当一个对象的偏好线程以外的其他线程申请该对象的内部锁时,java虚拟机会收回该对象对原偏好线程的偏好,重新设置对象的偏好线程。这个偏好收回和重新分配过程的代价也是比较昂贵的。因此,偏向锁只适用于存在相当大一部分锁并没有被争用的系统当中。

  4)自旋锁:

  线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。

  所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。

  自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。

  自旋锁在JDK 1.4.2中引入,JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

  5)轻量级锁

  如果说偏向锁是只允许一个线程获得锁,那么轻量级锁就是允许多个线程获得锁,但是只允许他们顺序拿锁,不允许出现竞争,也就是拿锁失败的情况,轻量级锁的步骤如下:

  1)线程1在执行同步代码块之前,JVM会先在当前线程的栈帧中创建一个空间用来存储锁记录,然后再把对象头中的Mark Word复制到该锁记录中,官方称之为Displaced Mark  Word。然后线程尝试使用CAS将对象头中的Mark Word 替换为指向锁记录的指针。如果成功,则获得锁,进入步骤3)。如果失败执行步骤2)

  2)线程自旋,自旋成功则获得锁,进入步骤3)。自旋失败,则膨胀成为重量级锁,并把锁标志位变为10,线程阻塞进入步骤3)

  3)锁的持有线程执行同步代码,执行完CAS替换Mark Word成功释放锁,如果CAS成功则流程结束,CAS失败执行步骤4)

  4)CAS执行失败说明期间有线程尝试获得锁并自旋失败,轻量级锁升级为了重量级锁,此时释放锁之后,还要唤醒等待的线程

java多线程02-----------------synchronized底层实现及JVM对synchronized的优化的更多相关文章

  1. java多线程(五)-访问共享资源以及加锁机制(synchronized,lock,voliate)

    对于单线程的顺序编程而言,每次只做一件事情,其享有的资源不会产生什么冲突,但是对于多线程编程,这就是一个重要问题了,比如打印机的打印工作,如果两个线程都同时进行打印工作,那这就会产生混乱了.再比如说, ...

  2. java 多线程系列基础篇(四)之 synchronized关键字

    1. synchronized原理 在java中,每一个对象有且仅有一个同步锁.这也意味着,同步锁是依赖于对象而存在.当我们调用某对象的synchronized方法时,就获取了该对象的同步锁.例如,s ...

  3. Java多线程02(线程安全、线程同步、等待唤醒机制)

    Java多线程2(线程安全.线程同步.等待唤醒机制.单例设计模式) 1.线程安全 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量 ...

  4. Java多线程初学者指南(12):使用Synchronized块同步变量

    我们可以通过synchronized块来同步特定的静态或非静态方法.要想实现这种需求必须为这些特性的方法定义一个类变量,然后将这些方法的代码用synchronized块括起来,并将这个类变量作为参数传 ...

  5. Java多线程初学者指南(10):使用Synchronized关键字同步类方法

    要想解决“脏数据”的问题,最简单的方法就是使用synchronized关键字来使run方法同步,代码如下: public synchronized void run() { ... } 从上面的代码可 ...

  6. Java多线程 | 02 | 线程同步机制

    同步机制简介 ​ 线程同步机制是一套用于协调线程之间的数据访问的机制.该机制可以保障线程安全.Java平台提供的线程同步机制包括: 锁,volatile关键字,final关键字,static关键字,以 ...

  7. java 多线程:Thread 并发线程: 方法同步synchronized关键字,与static的结合

    1.方法内的变量是安全的 方法内定义的变量,每个变量对应单独的内存变量地址,多个线程之间相互不影响.多个线程之间的变量根本没有一毛钱关系 public class ThreadFuncVarSafe ...

  8. java 多线程操作List,已经做了同步synchronized,还会有ConcurrentModificationException,知道为什么吗?

    如题,最近项目里有个模块我做了异步处理方面的事情,在code过程中发现一个颠覆我对synchronized这个关键字和用法的地方,请问各位java开发者们是否对此有一个合理的解释,不多说,我直接贴出问 ...

  9. Java 多线程 02

    多线程·线程间通信 和 GUI 单例设计模式 * A:单例设计模式 * 保证类在内存中只有一个对象 * B:如何保证 * a:控制类的创建,不让其他类来创建泵类的对象,私有化构造方法 * b:在本类中 ...

随机推荐

  1. touch-action css属性 滚动和缩放手势

    CSS 属性 touch-action 用于指定某个给定的区域是否允许用户操作,以及如何响应用户操作(比如浏览器自带的划动,缩放等) 默认情况下,平移(滚动) 和 缩放手势由浏览器专门处理.该属性用于 ...

  2. vue循环出来列表里面的列表点击click事件只对当前列表有效;

    <div id="app"> <div class=‘b’ v-for='item in items' @click="toggle(item)&quo ...

  3. http常用状态吗以及分别代表什么意思?

    http常用状态码: 状态码 定义 说明 1xx 信息 街道请求继续处理 2xx 成功 成功的收到.理解.接受 3xx 重定向 浏览器需要执行某些特殊处理一完成请求 4xx 客户端错误 请求的语法有问 ...

  4. JS函数节流和防抖

    看JS高级程序设计时,了解到一个概念--函数节流,是为了防止在高频率触发某些事件导致浏览器崩溃.最近又了解到另一个概念,防抖,感觉和函数节流很像,也查看了很多篇博文,算是理解了. 区别: 函数节流:频 ...

  5. Java 局部变量未初始化会报错,局部变量没有初始值,成员变量有初始值

    Java 成员变量有初始值,而局部变量没有初始值. 如下所示,代码不能编译通过 public static void main(String[] args){     String s;     Sy ...

  6. linux文本界面../和./的区别

    linux文本界面../和./的区别 ../代表的是上一个目录 ./代表的当前目录

  7. http过程

    当在浏览器里输入URL地址时,http的通讯过程: 1) 连接 DNS解析:URL——>DNS服务器(找到返回其ip,否则继续将DNS解析请求传给上级DNS服务器) Socket连接:通过IP和 ...

  8. Linux下配置MySQL主从复制

    一.环境准备 本次准备两台Linux主机,操作系统都为CentOS6.8, 都安装了相同版本的MySQL.(MySQL5.7). 主从服务器的防火墙都开启了3306端口. 相关信息如下: [主服务器] ...

  9. JavaScript括号中什么什么不加引号什么时候加引号?

    *****我的QQ号:1539832180.欢迎一起讨论学习.***** 1.如果是你定义的变量,不能加引号. 因为在大多数语言里面,单引号(或双引号)里面的内容表示的都是字符串. 2.如果是你定义的 ...

  10. luogu2568 GCD

    先筛法求出 \([1,n]\) 间的素数,然后枚举每个素数.可以发现,对于每个素数 \(x\),它的贡献是 \([1,\lfloor n/x \rfloor]\) 间的有序互质对数. 我们钦定 \(( ...