深入分析Synchronized原理

一 、基本使用

Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:

1、原子性:确保线程互斥的访问同步代码;

2、可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;

3、有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;

从语法上讲,Synchronized可以把任何一个非null对象作为"锁",在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)

注意,synchronized 内置锁 是一种 对象锁(锁的是对象而非引用变量),作用粒度是对象 ,可以用来实现对 临界资源的同步互斥访问 ,是 可重入 的。其可重入最大的作用是避免死锁,如:

子类同步方法调用了父类同步方法,如没有可重入的特性,则会发生死锁;当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。

Synchronized总共有三种用法:

1、当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this);

public synchronized void increase() {
    i++;
}

2、当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;

public static synchronized void increase() {
    i++;
}

3、当synchronized作用在某一个对象实例时,即修饰代码块,监视器锁(monitor)便是synchronized后面括号()括起来的对象实例;

在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方法对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了。

成员锁:锁的对象是变量

public Object synMethod(Object a) {
    synchronized(a) {
        // 操作
    }
}

实例对象锁:this 代表当前实例

synchronized(this) {
    for (int j = 0; j < 100; j++) {
		i++;
    }
}

类锁:当前类的 class 对象锁

synchronized(AccountingSync.class) {
    for (int j = 0; j < 100; j++) {
        i++;
    }
}

二 、同步原理

数据同步需要依赖锁,那锁的同步又依赖谁?

synchronized给出的答案是在软件层面依赖JVM,而j.u.c.Lock给出的答案是在硬件层面依赖特殊的CPU指令。

1、测试类

/**
 * 归根结底它上锁的资源只有两类:一个是对象,一个是类。
 */
public class SynchronizedDemo {

    public static int i = 0;

    SynchronizedDemo test = new SynchronizedDemo();

    /**
     * 对实例方法加锁,必须获得该类的实例对象的锁才能进入同步块
     */
    public synchronized void test1() {
        i++;
    }

    /**
     * 对静态方法加锁,必须获得该类Class的锁才能进入同步块
     */
    public static synchronized void test2() {
        System.out.println("Hello static test2");
    }

    /**
     * 对代码块加锁
     */
    public void test3(Object a) {
        /**
         * 必须获得类锁
         */
        synchronized (SynchronizedDemo.class) {
            System.out.println("Hello test3-1");
        }

        /**
         * 必须获得对象锁
         */
        synchronized (test) {
            System.out.println("Hello test3-2");
        }

        /**
         * 必须获得对象锁
         */
        synchronized (a) {
            System.out.println("Hello test3-3");
        }
    }

}

2、查看字节码文件

// 编译生成字节码文件
javac SynchronizedDemo.java 
// 反编译查看字节码文件
javap -verbose SynchronizedDemo.class

3、通过反编译的结果分析同步块的实现

当synchronized作用在实例方法test1时:

当synchronized作用在静态方法test2时:

flags里面多了一个ACC_SYNCHRONIZED标志,这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,直到该锁被释放。

在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

当synchronized修饰代码块时:

由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。

monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

a、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;

b、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;

c、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;

4、总结

通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

三 、同步概念

1、Java对象头

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:

实例数据:存放类的属性数据信息,包括父类的属性信息;

对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;

对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

Synchronized用的锁就是存在Java对象头里的,那么什么是Java对象头呢?

Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。其中 Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。 Java对象头具体结构描述如下:

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值