synchronized只会用不知道原理?一文搞定

据说微信搜索《Java鱼仔》会变动强哦!java

本文收录于JavaStarter,里面有我完整的Java系列文章,学习或面试均可以看看哦git

(一)概述

在多线程的程序执行中,有可能会出现多个线程会同时访问一个共享而且可变资源的状况,这种时候因为线程的执行是不可控的,因此必须采用一些方式来控制该资源的访问,这种方式就是“加锁”。github

咱们把那些可能会被多个线程同时操做的资源称为临界资源,加锁的目的就是让这些临界资源在同一时刻只能有一个线程能够访问。面试

(二)CAS的介绍

CAS:compare and swap,比较且交换。使用CAS操做能够在没有锁的状况下完成多线程对一个值的更新。CAS的具体操做以下:安全

当要更新一个值时,先获取当前值E,计算更新后的结果值V(先不更新),当要去更新这个值时,比较此时这个值是否仍是等于E,若是相等,则将E更新为V,若是不相等,则从新进行上面的操做微信

以i++操做为例,在没有锁的状况下,这个操做是线程不安全的,假设i的初始值为0,CAS操做先获取原值E=0,计算更新后的值V=1,要更新以前先比较这个值是否仍是等于0,若是等于0则将E更新为1,若是不等于0则说明有线程已经更新了,从新获取E值=1,继续执行。多线程

ABA问题app

CAS操做可能会出现ABA问题,ABA问题即咱们要去比较的这个值E,通过多个线程的操做后从0变成1又变成了0。此时虽然E值和更新前相等,可是仍是已经被更新了。布局

ABA问题的解决办法性能

对E值增长一个版本号,每次要获取数据时将版本号也获取,每次更新完数据以后将版本号递增,这样就算值相等经过版本号也能知道是否通过修改。

java在不少地方都用到了CAS操做,好比Atomic的一些类:

AtomicInteger i=new AtomicInteger();

进入AtomicInteger方法中,能够看到有个叫Unsafe的类,进入这个类中,能够看到CAS的几个操做方法

在这里插入图片描述

(三)对象在内存中的存储布局

要想学会synchronized,首先要理解Java对象的内存布局,或者称为内存结构。

在这里插入图片描述

一个对象分为对象头、实例数据和对其填充。

其中对象头Header占12个字节:Mark Word占8个字节,类型指针class pointer占4个字节(默认通过了压缩,若是不开启压缩占8个字节)

实例对象按实际存储有不一样大小,对象为空时等于0。

Padding表示对齐,当此时内存所占字节不能被8整除时补上相应字节数。

以Object o=new Object()为例,咱们先导入一个jol依赖,经过jol能够看到具体的内存布局

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

运行如下代码:

public static void main(String[] args) {
    Object o=new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

观察结果,OFFSET表示偏移量的起始点,SIZE表示所占字节,前两行是Mark Word一共占8个字节,第三行是class pointer占4个字节,此时对象为空,实例对象等于0,最后padding补齐,一共16个字节。

在这里插入图片描述

(三)synchronized

synchronized能够保证在同一时刻,只有一个线程能够执行某个方法或某个代码块,synchronized把锁信息存放在对象头的MarkWord中。

synchronized做用在非静态方法上是对方法的加锁,synchronized做用在静态方法上是对当前的类加锁。

在早期的jdk版本中,synchronized是一个重量级锁,保证线程的安全可是效率很低。后来对synchronized进行了优化,有了一个锁升级的过程

无锁态(new)-->偏向锁-->轻量级锁(自旋锁)-->重量级锁

经过MarkWord中的8个字节也就是64位来记录锁信息。也有人将自旋锁称为无锁,由于自选操做并无给一个对象上锁,这里只要理解意思便可。

在这里插入图片描述

3.1 锁升级过程详解:

当给一个对象增长synchronized锁以后,至关于上了一个偏向锁

当有一个线程去请求时,就把这个对象MarkWord的ID改成当前线程指针ID(JavaThread),只容许这一个线程去请求对象。

当有其余线程也去请求时,就把锁升级为轻量级锁。每一个线程在本身的线程栈中生成LockRecord,用CAS自旋操做将请求对象MarkWordID改成本身的LockRecord,成功的线程请求到了该对象,未成功的对象继续自旋。

若是竞争加重,当有线程自旋超过必定次数时(在JDK1.6以后,这个自旋次数由JVM本身控制),就将轻量级锁升级为重量级锁,线程挂起,进入等待队列,等待操做系统的调度。

3.2 加锁的字节码实现

synchronized关键字被编译成字节码以后会被翻译成monitorenter和monitorexit两条指令,进入同步代码块时执行monitorenter,同步代码块执行完毕后执行monitorexit

(四)锁消除

在某些状况下,若是JVM认为不须要锁,会自动消除锁,好比下面这段代码:

public void add(String a,String b){
    StringBuffer sb=new StringBuffer();
    sb.append(a).append(b);
}

StringBuffer是线程安全的,可是在这个add方法中stringbuffer是不能共享的资源,所以加锁只会徒增性能消耗,JVM就会消除StringBuffer内部的锁。

(五)锁粗化

在某些状况下,JVM检测到一连串的操做都在对同一个对象不断加锁,就会将这个锁加到这一连串操做的外部,好比:

StringBuffer sb=new StringBuffer();
while(i<100){
    sb.append(str);
    i++;
}

上述操做StringBuffer每次添加数据都要加锁和解锁,连续100次,这时候JVM就会将锁加到更外层(while)部分。

(六)逃逸分析

首先问一个常常基础的虚拟机问题,实例对象存放在虚拟机的哪一个位置?按之前的回答,示例对象放在堆上,引用放在栈上,示例的元数据等存放在方法区或者元空间。

但这是有前提的,前提是示例对象没有线程逃逸行为。

JDK1.7开始默认开启了逃逸分析,所谓逃逸分析,就是指若是一个对象被编译器发现只能被一个线程访问,那么这个对象就不须要考虑同步。JVM就对这种对象进行优化,将堆分配转化为栈分配,归根结底就是虚拟机在编译过程当中对程序的一种优化行为。

开启逃逸分析:­ XX:+DoEscapeAnalysis
关闭逃逸分析: ­XX:­-DoEscapeAnalysis
相关文章
相关标签/搜索