并发学习之synchronized关键字

synchronized概述

synchronized是java的一个关键字,是jvm的内置锁,由于synchronized的加锁过程是在jvm内部实现的,所以也叫隐式锁。

synchronized是保证多线程并发情况下同步互斥的方式之一,在jdk1.6版本之前,synchronized是直接依赖Monitor管程对象(监视器锁)实现同步,Monitor是一个重量级锁,底层依赖于底层操作系统Mutex lock互斥量实现,由于Java是用户线程,每次加锁解锁过程需要cpu经过用户态和操作系统的内核态之间的转化,所以性能较低。

并发大神Doug Lea基于此,独自使用Java语言开发出AQS框架,基于AQS框架开发出另一个同步互斥锁——ReentrantLock(保证可重入和公平性),在性能上,ReentrantLock要优于此时的synchronized。于是在jdk1.6版本,Oracle官方对synchronized锁进行了一系列优化升级,如:锁粗化锁消除新增偏向锁轻量级锁状态适应性自旋。在性能方面,已经没有明显差异,需要根据使用场景自行选择。

synchronized加锁方式

1、加在对象上,锁的是括号里的对象(Object
2、加在方法上,锁的是当前实例对象(this
3、加在静态方法上,锁的是该类(Class

synchronized底层原理

monitor监视器锁

synchronized底层都是基于monitor监视器锁实现。当一个Monitor被持有之后,它会处于锁定状态,同步就是基于进入和退出Monitor对象来实现方法同步和代码块同步。

每一个Java对象都可以成为Monitor锁(通常说的synchronized的对象锁),实际上,Monitor是由c++的ObjectMonitor对象实现的。ObjectMonitor中的重要属性:
1、count:记录加锁次数。
2、owner:指向持有ObjectMonitor对象的线程。
3、EntryList:处于等待锁block状态的线程,会被加入到该队列。
4、WaitSet: 存放wait状态的线程。

当多个线程同时访问同一个共享变量时:
1、首先进入EntryList队列等待锁。
2、当线程拿到对象锁monitor后,count+1,并且owner设置为当前持有锁的线程。
3、如果持有monitor的线程调用了wait()方法,就会释放掉monitor,owner置为null,count-1,该线程进入WaitSet等待被唤醒。
4、如果持有monitor的线程执行完毕,释放monitor,将count减为0,以便其他线程获取锁。

加锁过程

在字节码层面,如果synchronized锁的是对象,在编译成字节码时,会在同步块的起止位置分别加上monitorentermonitorexit 两条字节码指令,为了避免同步块中可能发生异常无法释放锁,monitorexit可能会加多条确保锁能成功释放。

上面已经知道,synchronized的加锁和解锁过程实际上是线程对monitor对象锁的持有与释放的过程。

monitorenter:线程执行monitorenter时尝试获取monitor对象锁,如果monitor的count数为0,那么线程进入 monitor,并将count+1,表示线程获取到monitor; 如果线程已经持有了monitor,只是重新进入,count+1;
monitorexit:执行monitorexit,count-1,直到减为0,表示线程释放锁,其他阻塞的线程可以进入获取锁。

如果锁的是方法,经过反编译会发现在字节码中添加了一个ACC_SYNCHRONIZED 访问标志符,而没有通过monitorenter和monitorexit指令实现。常量池中会多一个 ACC_SYNCHRONIZED 标示符,jvm就是通过判断ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,线程就会尝试获取monitor,如果获取成功,在方法执行完并释放monitor之前,其他线程不能进入该方法。

上面的方式无论是通过字节码指令还是使用访问标识符,本质上都是通过调用操作系统的互斥原语mutex来实现。被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

需要注意的是:wait()、notify()、notifyAll()等操作,都是基于Monitor的,所以这些操作要在同步代码块中执行。

锁的膨胀升级过程

jdk1.6之后,加入了偏向锁、轻量级锁。锁的升级过程是根据线程的竞争程度经无锁、偏向锁(-XX:-UseBiasedLocking关闭,默认开启)、轻量级锁到重量级锁,并且该过程是不可逆的。当锁竞争不激烈且代码执行时间不长时,线程会使用自旋的方式避免进入阻塞状态等待唤醒,减少底层内核切换造成的性能消耗。

从无锁状态到重量级锁的状态都是存储在对象锁的对象头的Mark Word区域中,每经过一次升级,对象头中的状态都会发生改变。
Mark Word

锁粗化

在同一个栈帧中,如果存在多个synchronized同步块的化,会导致线程频繁进出对象锁,影响性能。所以,在jdk1.6之后,jvm底层经过逃逸分析后进行了锁的优化,实现效果类似于将多个同步代码块中的代码在一个同步块中执行,减少了性能损耗。

    private Object object = new Object();

    public void method() {
        synchronized (object) {
            System.out.println("执行逻辑1");
        }

        synchronized (object) {
            System.out.println("执行逻辑2");
        }

        synchronized (object) {
            System.out.println("执行逻辑3");
        }

        /* 锁粗化后*/
        synchronized (object) {
            System.out.println("执行逻辑1");
            System.out.println("执行逻辑2");
            System.out.println("执行逻辑3");
        }
    }

注:在生产场景中尽量减少使用System.out.println();因为println()方法内部是加了对象锁,大量使用非常影响性能。

锁消除

考虑一种场景,如果对在方法体内部创建的局部对象加了锁,该对象是无法被其他线程引用到的,是被创建它的线程独享的,而锁对象是要全局共享,需要多线程竞争的。对于这样的对象加锁,是毫无意义的,
jvm会经过逃逸分析后,会对这样的加锁进行锁的消除:

    //锁的消除
    private void method1(){
        Object object1 = new Object();

        synchronized (object1){
			// ...
            System.out.println("锁的消除");
        }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值