美文网首页面试精选
多线程积累:synchronized 关键字

多线程积累:synchronized 关键字

作者: skipper_shou | 来源:发表于2021-03-01 23:29 被阅读0次

    前言

    关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能)。

    synchronized的三种应用方式

    synchronized关键字最主要有以下3种应用方式,下面分别介绍

    • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
    • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
    • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    当你将synchronized加到非静态方法的实例方法时,在同一个实例对象的该方法调用,会对方法的调用进行加锁,但是,如果new了两个实例对象,则不会加锁,导致没有达到预期结果。
    此时,需要在静态方法中加入synchronized关键字,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
    当然,为了缩小同步的代码块,可以用synchronized关键字修饰代码块,减少加锁的范围,提升性能。

    synchronized原理浅析

    1.synchronized代码块底层原理

    从javap反编译后得到字节码可以得到:

    monitorenter  //进入同步方法
    //..........省略其他  
    monitorexit   //退出同步方法
    goto          //return
    //省略其他.......
    monitorexit //退出同步方法
    

    从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。
    当执行到monitorenter时,线程会尝试获取对象锁所对应的monitor的持有权,获取成功,并将计数器设置为1,取锁成功,直到正在执行线程执行完毕,即执行monitorexit指令,计数器的值会被设置为0,并释放锁。当然当前线程可对该monitor进行重入,这里就涉及重入锁概念,但是无论如何,方法中调用过每条monitorenter都会对应一条monitorexit,都会进行一一配对。
    从以上的字节码可以看到有两个monitorexit,因为为了保证在方法异常完成时,monitorenter和monitorexit都会正常配对执行,编译器会自动产生一个异常处理器,处理所有的异常,并执行monitorexit指令。

    2.synchronized修饰方法底层原理

    从javap反编译后得到字节码可以得到:

    //..........省略其他  
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    //省略其他.......
    

    从字节码可知,当前无monitorenter 和 monitorexit 指令,只有ACC_SYNCHRONIZED标识,表示该方法是一个同步方法,具体锁方面的原理跟代码块的相似。
    在理解原理之后,也要理解锁的状态、锁的竞争等概念。

    锁状态

    锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。

    1.偏向锁

    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
    在大多数情况下,锁总是由同一个线程多次获得,不存在多线程竞争,所以出现了偏向锁,从而达到提高性能的目的。
    当偏向锁遇到其他线程尝试竞争偏向锁时,才会释放锁,会暂停拥有偏向锁的线程,判断是否处于被锁定状态,然后恢复到无锁状态或轻量级锁状态。

    2.轻量级锁

    当一个线程持有偏向锁是,呗另外的线程访问,偏向锁会升级为轻量级锁,其他线程通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
    由此可知,当只有一个等待线程,则该线程通过自旋进行等待,但是当自旋超过一定的次数,或者一个线程持有锁,一个线程自旋,此时再来第三个线程,轻量级锁活升级为重量级锁

    3.重量级锁

    重量级锁,效率低下,因为monitor是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

    synchronized注意点

    1.synchronized可重入性

    由synchronized底层原理可知,当前线程已经持有锁对象,再次申请锁时,属于重入锁。

     for(int j=0;j<1000000;j++){
                //this,当前实例对象锁
                synchronized(this){
                    i++;
                    increase();//synchronized的可重入性
                }
            }
    
    

    如上图伪代码可知,synchronized允许重入。

    2.synchronized线程中断

    对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。

    3.synchronized等待唤醒机制

    notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象

    synchronized (obj) {
           obj.wait();
           obj.notify();
           obj.notifyAll();         
     }
    

    4.wait和sleep区别

    wait方法调用之后,会释放持有的monitor,直到有线程调用notify/notifyAll后才能继续运行。
    sleep方法只是让线程休眠,并不释放锁。

    5.执行过程

    线程进入synchronized代码块前后,执行过程如下:

    • a.线程获得互斥锁
    • b.清空工作内存
    • c.从主内存拷贝共享变量最新的值到工作内存成为副本
    • d.执行代码
    • e.将修改后的副本的值刷新回主内存中
    • f.线程释放锁

    相关文章

      网友评论

        本文标题:多线程积累:synchronized 关键字

        本文链接:https://www.haomeiwen.com/subject/kstofltx.html