java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java的synchronized关键字

Java并发编程中的synchronized关键字详细解读

作者:程光CS

这篇文章主要介绍了Java并发编程中的synchronized关键字详细解读,在Java早期版本中,synchronized 属于 重量级锁,效率低下,这是因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,Java 的线程是映射到操作系统的原生线程之上的,需要的朋友可以参考下

前言

在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。这是因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。在 Java 6 之后, synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized 锁的效率提升了很多。

一、synchronized的使用方法

synchronized 块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用。

synchronized关键字可以用来修饰实例方法、静态方法、代码块,表示对其进行加锁,当线程进入 synchronized 代码块前只有获取到相应的锁才能访问,否则自动进入自旋或阻塞状态(BLOCKED)等待锁被其他线程释放后竞争锁。

1、修饰实例方法 (锁当前对象实例)

给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。

synchronized void method() {
    //业务代码
}

2、修饰静态方法 (锁类对象)

给当前类加锁,进入同步代码前要获得 当前类class对象的锁。这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例。

synchronized static void method() {
    //业务代码
}

3、修饰代码块 (锁指定对象/类对象)

synchronized(this) {
    //业务代码
}

二、synchronized的特性

1. 可重入锁

持有锁的线程可直接进入此锁关联的任意其他代码。

2. 非公平锁

不是按照先来后到的原则来分配锁。

3. 不可中断锁

synchronized在锁竞争时是不可中断的,获取不到锁的线程会一直处于阻塞状态。而ReentrantLock获取锁失败可以被interrupt()进行中断操作。

三、synchronized相关问题

1. volatile和synchronized的区别是什么?

2. 占有锁的线程在什么情况下会释放锁?

四、synchronized的底层原理

对于synchronized同步代码块,编译后在代码块前后分别有一个monitorenter 和 monitorexit 指令,在JVM中当线程执行到monitorenter指令时尝试获取指定对象的锁,执行到monitorexit 指令则释放锁。

在这里插入图片描述

对于synchronized同步方法,编译后方法中有一个ACC_SYNCHRONIZED标识,在JVM中当线程执行到有此标识的方法时会隐式调用monitorenter和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit。最后都能达到加锁的效果。

在这里插入图片描述

五、synchronized的锁升级过程

早期synchronized实现的同步锁为重量级锁。但是重量级锁会造成线程阻塞排队,阻塞和唤醒线程会使CPU在用户态和核心态之间频繁切换,所以代价高、效率低。因此 Java6 对 synchronized 锁进行了优化,增加了轻量级锁和偏向锁。为了提高效率,不会一开始就使用重量级锁,JVM在内部会根据需要,按如下步骤进行锁的升级:

在这里插入图片描述

1. 无锁状态

初期锁对象刚创建时,还没有任何线程来竞争,对象的markword是上图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。

2. 偏向锁

当有一个线程来竞争锁时,先用偏向锁,会在对象头的markword中记录线程threadID,并且线程退出synchronized块后偏向锁不会主动释放,因此之后此线程需要再次获取锁的时候,通过比较当前线程的 threadID 和 对象头中的threadID 发现一致,就不需要再做任何检查和切换直接进入,这种竞争不激烈的情况下,效率非常高。如上图第二种情形。

JDK 15中的偏向锁 偏向锁在单线程反复获取锁的场景下性能很高,但细想便知生产环境中高并发的场景下很难有这种场景。 而且对于偏向锁来说,在多线程竞争时的撤销操作十分复杂且带来了额外的性能消耗(需要等到safe point,并STW)。 JDK 15 之前,偏向锁默认是 开启的,从 JDK 15 开始,默认就是关闭的了,需要显式打开(-XX:+UseBiasedLocking)。

3. 轻量级锁

当需要获取对象的hashcode值时就会禁用偏向锁升级为轻量级锁,将hashcode值写入markword;或者当有第二个线程开始竞争这个锁对象,通过对比markword中记录线程threadID发现不一致,那么首先需要查看Java 对象头中记录的线程 1 是否存活(偏向锁不会主动释放锁),如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程 2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程 1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程 1,撤销偏向锁,升级为 轻量级锁,如果线程 1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。如上图第三种情形。

在这里插入图片描述

4. 重量级锁(monitor)

当轻量级锁等待获取锁的线程自旋到达一定次数,或者竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁。synchronized的重量级锁是基于在监视器(monitor)实现的,JVM中每个对象都可以关联一个ObjectMonitor监视器对象(C++实现),升级为重量级锁后对象的Mark Word再次发生变化,会指向对象关联的监视器对象(如上图第四种情形)。

ObjectMonitor的主要数据结构如下:

 ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;      //线程重入次数
    _object       = NULL;   //指向对应的java对象
    _owner        = NULL;   //持有锁的线程
    _WaitSet      = NULL;   //等待队列:处于wait状态的线程会被加入到这个队列中
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;  //要竞争锁的线程会先被加入到这个队列中
    FreeNext      = NULL ;
    _EntryList    = NULL ;  //处于blocked阻塞状态的线程,会被加入到这个队列中
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

锁竞争机制:

在这里插入图片描述

六、synchronized的其它优化

1. 锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

public void test() {
    for (int i = 0; i < 100; i++) {
        synchronized (object) {
            i++;
        }
    }
}

我们看以上方法,在for循环中,synchronized保证每个i++操作都是原子性的。但是以上的方法有个问题,就是在每次循环都会加锁,开销大,效率低。

虚拟机即时编译器(JIT)在运行时,会自动根据synchronized的影响范围进行锁粗化优化。

优化后代码:

public void test() {
    synchronized (object) {
        for (int i = 0; i < 100; i++) {
            i++;
        }
    }
}

2. 锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java 虚拟机在 JIT 编译时通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,我们知道StringBuffer 是线程安全的,里面包含锁的存在,但是如果我们在函数内部使用 StringBuffer局部变量,那么代码会在 JIT 后会自动将锁消除。

到此这篇关于Java并发编程中的synchronized关键字详细解读的文章就介绍到这了,更多相关Java的synchronized关键字内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文