Java多线程

synchronized原理

2018-07-15  本文已影响85人  一路花开_8fab

synchronized代码块是由一对monitorenter/monitorexit指令实现的,monitor对象是同步的基本实现单元。

    // synchronized(object1)代码块
    public void printA() {
        synchronized (object1) {
            try{
                Thread.sleep(3000);
            }catch (Exception e) {
                System.out.println(e.getMessage());
            }

        }
    }

如上面的synchronized代码块,利用javap工具查看生成的class文件信息如下图。当线程执行到monitorenter指令时,将尝试获取线程对应monitor的所有权,即尝试获取对象的锁。

monitorenter/monitorexit指令对

java对象头

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

java对象头内容

Hotspot虚拟机的对象头包含的内容如上图。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但如果对象是数组类型,则使用三个机器码。

32位虚拟机Mark Word

64位虚拟机无锁和偏向锁状态下Mark Word如下图所示:

64位虚拟机Mark Word

1. 偏向锁

偏向锁的适应场景是:大部分对象生命周期中最多会被一个线程锁定。使用偏向锁时,JVM会使用CAS操作,在对象头的Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及整整的互斥锁,从而降低无竞争开销。偏向锁在Java 6和Java 7中是默认启用的,但是它在应用程序启动几秒钟之后才激活。

-XX:-BiasedLockingStartupDelay=0

下图展示了偏向锁的获得和撤销流程

偏向锁的获得和撤销流程

1.1 获取锁

(1)检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
(2)若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
(3)如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行步骤(4);
(4)通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
(5)执行同步代码块

1.2 释放锁

偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM就需要撤销偏斜锁,并切换到轻量级锁实现。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:
(1)暂停拥有偏向锁的线程,判断锁对象是否还处于被锁定状态;
(2)撤销偏向锁,恢复到无锁状态(01)或者轻量级锁的状态;
偏向锁状态下没有阻塞发生,并且通过减少不必要的CAS操作(与轻量级锁相比)来降低获取锁的成本。但是,由于撤销偏向锁是很重的行为,因此,当实践中需要大量使用并发操作时,往往并不需要偏斜锁。
可以使用JVM参数来关闭偏向锁,当关闭偏向锁后,将默认获取轻量级锁。

-XX:-UseBiasedLocking=false

2.轻量级锁

引入轻量级锁的主要目的是在多数没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗

轻量级锁及膨胀流程图

2.1 获取锁

(1)判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3);
(2)JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3);
(3)判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;

2.2 释放锁

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:
(1)取出在获取轻量级锁保存在Displaced Mark Word中的数据;
(2)用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行步骤(3);
(3)如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。
轻量级锁情况下没有阻塞发生,但是使用了大量的CAS操作

3. 重量级锁

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

三种锁优缺点对比

三种锁优缺点对比

补充:自旋锁

轻量级锁情况下,如果某个线程竞争锁失败,会尝试使用自旋来获取锁。线程不会立马阻塞,而是通过空转的方式(for(;;;))等待锁的释放。
适用场景:自旋锁可以减少线程的阻塞,这对于锁竞争不激烈,且占用锁时间非常短的代码块来说,有较大的性能提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗。
如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗,造成cpu的浪费。

上一篇下一篇

猜你喜欢

热点阅读