Java中synchronized关键字的使用和原理

synchronized关键字的使用

synchronized 关键字是 Java 中一个独占式的悲观锁,可以用来修饰方法块和方法,根据锁定对象的类型进行分类,可以分为对象锁类锁

对象锁

修饰同步代码块:锁定对象为 this 或者实例对象;

public class Sync{
    private int a = 0;
    public void add(){  
        // 锁定对象为 this
        synchronized(this){
            System.out.println("a values " + ++a);
        }
    }
    public void del(){
        Sync s = new Sync(); 
        // 锁定对象为实例对象 
        synchronized(s){
            System.out.println("a values " + ++a);
        }
    }
}

修饰方法:同步非静态方法;

public class Sync{
    private int a = 0;
    // 同步非静态方法
    public synchronized void add(){  
        
    }
}

类锁

修饰同步代码块:锁定对象为当前类;

public class Sync{
    private int a = 0;
    public void add(){  
        // 锁定对象为当前类
        synchronized(Sync.class){
            System.out.println("a values " + ++a);
        }
    }
}

修饰方法:同步静态方法;

public class Sync{
    private int a = 0;
    // 同步静态方法,锁定当前类
    public synchronized static void add(){

    }
}

了解了用法还不够,还要知道背后的实现原理,接下来我们分别从 synchronized 同步语句块和 synchronized 同步方法两方面来分析实现原理。

synchronized关键字的原理

synchronized 同步语句块

将同步代码块反编译结果如下:

可以看出,synchronized 同步语句块的实现,使用的是 monitorenter monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

每个 Java 对象的对象头中都存储了 monitor 对象,synchronized 就是通过对该对象的锁定和释放来实现加解锁的。而又因为每个对象头中都有对应的 monitor 对象,所以 Java 中任意对象都可以作为锁。

当 monitor 对象被占用时就会处于锁定状态,线程执行 monitorenter 指令时会尝试获取 monitor 的所有权,过程如下:

  1. 如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为 1,该线程即为monitor 的所有者;
  2. 如果线程已经占有该 monitor,只是重新进入,则进入 monitor 的进入数加 1;
  3. 如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权。

从以上过程可以看出,synchronized 关键字和 ReentrantLock 一样,都是可重入锁。

也就是说,当一个线程已经获取一个锁时,它可以再获取无数次,从代码的角度上将就是有无数个相同的 synchronized 语句块嵌套在一起。

在进入时,monitor 的进入数加一;退出时就减一,直到为 0 的时候才可以被其他线程竞争获取。

synchronized 同步方法

将同步方法反编译结果如下:

 我们发现,同步方法里面没有了 monitorenter 和 monitorexit 指令,但是常量池中多了ACC_SYNCHRONIZED 标识符,JVM 就是根据该标识符实现方法的同步的。

当方法调用时,会检查方法的 ACC_SYNCHRONIZED 访问标识是否被设置,如果设置了,执行线程将先获取 monitor 对象,获取成功之后才能执行方法体,执行完后会释放 monitor 对象。

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

这种方式与语句块没什么本质区别,都是通过竞争 monitor 对象的方式来实现的,只不过这种方式是一种隐式的实现。

通过上面的描述可以发现,synchronized 的关键实现主要是依靠 monitor 对象来完成的,在 Java 虚拟机 (HotSpot) 中,monitor 对象是由 ObjectMonitor 实现的,其数据结构如下:

ObjectMonitor() {
    _count        = 0; //记录个数
    _owner        = NULL; // 运行的线程
    //两个队列
    _WaitSet      = NULL; //调用 wait 方法会被加入到_WaitSet
   _EntryList    = NULL ; //锁竞争失败,会被加入到该列表
}

可以看到,ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,每个等待锁的线程都会被封装成 ObjectWaiter 对象被加入队列中。

当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的 monitor 锁后,会把 ObjectMonitor 中的 _owner 变量设置为当前线程,同时会给计数器 _count 加 1。

若调用 wait() 方法,线程就会释放当前持有的 monitor 对象,同时把 _owner 变量恢复为 null,计数器自减 1,之后该线程就会进入 _WaitSet 集合中等待被唤醒。

除了调用 wait() 方法之外,当前线程执行完毕后也将释放 monitor 锁并复位变量的值,以便其他线程可以重新获取 monitor 锁。

JDK 1.6 中对 synchronized 关键字的优化

在JDK 1.6中,为了减少获得和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,锁的状态变成了四种,如下图所示:

偏向锁

偏向锁是 Java 为了提高程序的性能而设计的一个比较优雅的加锁方式。

它的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时对象头中的 Mark Word 会变为偏向锁结构,也就是使用 CAS 操作把获取到这个锁的线程的 ID 记录在对象的 Mark Word之中的偏向线程 ID 中,当这个线程再次请求锁时,无需再重新获取锁。

对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,会膨胀为轻量级锁。

轻量级锁

引入轻量级锁的目的:在多线程交替执行同步代码块时(未发生竞争),避免使用互斥量(重量锁)带来的性能消耗。但多个线程同时进入临界区时(发生竞争),则会使得轻量级锁膨胀为重量级锁。

当面对线程竞争时,轻量级锁主要采用自旋锁机制来进行解决,当自旋超过一定次数,轻量级锁就会升级为重量级锁。

重量级锁

轻量级锁膨胀之后,就会升级为重量级锁。

重量级锁是依赖对象内部的 monitor 锁来实现的,而 monitor 又依赖了操作系统的 MutexLock(互斥锁),所以重量级锁也被称为互斥锁。

我们本篇博客分析的 synchronized 主要是针对 JDK 1.6 之前的实现进行探讨的,对应到 1.6 之后的 synchronized ,就是重量级锁的底层实现。

本文参考资料如下,非常感谢:

Synchronized的实现原理(汇总) - 月染霜华 - 博客园

synchronized 原理分析_慕课手记

面试官:请详细说下synchronized的实现原理_笔经面经_牛客网

Guess you like

Origin blog.csdn.net/j1231230/article/details/120546206