synchronized关键字(锁升级)

概述

synchronized是Java的一个关键字,用来保证多线程下临界区资源的共享安全性

synchronized可以加在方法上(静态方法和普通方法)、代码块上

使用语法: 

synchronized (对象) {
    // 操作临界资源
}

public synchronized void test() {
    // 操作临界资源
}

为什么需要加synchronized?

由于在多线程环境下,对于共享资源(比如共享变量)的修改,可能会导致结果与预期不一致

产生原因:以下有两个线程,分别对static变量进行多次+1和-1操作,最终结果预期为0,但由于单条java指令被编译成字节码后可能对应多条指令,因此会造成数据出错

示例:

static int sum = 0;
static Object object = new Object();
Thread t1 = new Thread(() -> {
    for (int i = 0; i < 500; i++) {
        sum++;
    }
});


Thread t2 = new Thread(() -> {
    for (int i = 0; i < 500; i++) {
        sum--;
    }
});

t1.start();
t2.start();

t1.join(); // 等待t1线程执行完成
t2.join(); // 等待t2线程执行完成

log.debug("{}", sum); // 最后结果不确定

i++对应字节码为:

getstatic    i    // 取指令
iconst_1
iadd              // 加1操作
putstatic    i    // 存指令

i--对应字节码为:

getstatic    i
iconst_1
isub
putstatic    i

假如第一个线程取完指令并进行+1操作后,还没来得及存指令;此时发生了线程上下文切换,第二个线程也取指令并完成了-1操作。

此时无论是第一个线程先将1写回变量,还是第二个线程将-1写回变量,结果都不等于0

因此,需要有一种方法保证两个线程不能同时操作临界资源,synchronized就可以解决这样的问题。

Monitor监视器(管程)

首先,synchronized无论是加在方法上,还是加在代码块上,本质上都跟Java中的一个类相对应。

假如存在一个Main类,synchronized加在不同位置时对应的类:

  • synchronized void test():加在普通方法上,对应类实例,相当于Main.this
  • synchronized static void test():加在静态方法上,对应类对象Main.class
  • synchronized (obj):加在任意其他对象上,对应给对象的实例,即obj

因此,Java中的每一个类都需要能够让线程互斥地访问临界资源,当一个线程正在使用临界资源时,其他线程需要处于阻塞状态。

由于需要记录正在使用临界资源的线程、处于阻塞状态的线程、处于等待状态的线程(由于运行条件不满足主动等待的线程),每个类都需要关联一种程序结构,这种程序结构就叫做:管程

从上述得出:

管程需要存储3种不同状态的线程:

  • Owner:正在执行的线程
  • EntryList:未获取到锁而处于阻塞状态的线程
  • WaitSet:处于等待状态的线程

类是如何与管程建立关联的?

每个Java中的类都会包含头部header,header中包含了32bit的MarkWord字段

Java中的类就是通过MarkWord字段与管程相关联的

ptr_to_heavyweight_monitor:指向管程(Monitor)的指针

此时每一个Java类都可以对多个线程进行管理了。

Monitor工作流程

  • 初始时,Monitor的Owner为null
  • 当有线程thread2进入时,会将Monitor中的Owner设置为thread2(Monitor中只能有一个Owner)
  • 如果此时有其他线程thread3,thread4进入synchronized方法,就会进入Monitor的EntryList中,进入阻塞状态
  • Owner中的线程执行完,会唤起EntryList中等待的线程来竞争锁(非公平)
  • WaitSet中的thread0,thread1是之前获取过锁,但条件不满足进入WAITING状态的线程

锁升级

synchronized锁的4种状态:

  • 无锁(Normal):MarkWord后三bit为 001
  • 偏向锁(Biased):MarkWord后三bit为 101
  • 轻量级锁(Lightweight Locked):MarkWord后两bit为 00
  • 重量级锁(Heavyweight Locked):MarkWord后两bit为 10

具体处于哪种状态下,是由锁住的对象的MarkWord决定的

轻量级锁

假如默认情况下,synchronized锁对象为无锁状态,此时多个线程运行method1方法(但时间是错开的)

static final Object obj = new Object();
public static void method1() {
    synchronized(obj) {
        // 同步块
        method2();
    }
}

public static void method2() {
    synchronized(obj) {
        // 同步块
    }
}

此时线程Thread-0尝试获取锁(假如当前Object对象是无锁状态)

左侧部分代表线程运行时内存栈帧,右侧为加锁的对象

为了标记是谁对当前对象加了锁,Object类会将锁记录(Lock Record)地址写入MarkWord中,并将MarkWord中的值写入Lock Record中,便于后续退出synchronized代码块后恢复初始状态,这种比较并互换值的操作就叫做CAS(compare and swap)

如果CAS成功,那么此时Object对象的MarkWord就更换了,其他线程看到MarkWord的后两bit为00时,就知道已经有人占用了锁,需要进入阻塞状态

偏向锁

但假如此时线程0内部有锁重入的现象:

static final Object obj = new Object();
public static void method1() {
    synchronized(obj) {
        // 同步块
        method2();
    }
}

public static void method2() {
    synchronized(obj) {
        // 同步块
    }
}

虽然是同一个线程获取锁,但由于不同方法对应不同栈帧(即Lock Record不同),会导致CAS失败

  • 如果当前是其他线程持有了Object锁,表明有竞争,则会进入锁膨胀的过程,升级为重量级锁
  • 如果是当前线程持有了Object锁,则会添加一条Lock Record作为锁重入的计数

产生问题的原因为:Object对象MarkWord存储的是Lock Record地址,CAS机制是通过判断Lock Record是否相同来判断是否CAS成功

优化措施:MarkWord存储线程ID(Thread ID),这样即使是不同方法下,由于Thread ID相同,也能判断对象锁的持有者是否是自己。在Java6中这种锁被引入,并称为偏向锁

重量级锁

如果同时有多个线程竞争同一把锁的情况,此时MarkWord已经无法记录多个线程的竞争情况,需要进行锁膨胀,将轻量级锁升级为重量级锁

重量级锁结构:

  • MarkWord中不再存储线程信息,而是存储指向Monitor的指针
  • Monitor存储线程信息:获取到锁的线程存储在Owner中,阻塞的线程存储在EntryList中,等待的线程存储在WaitSet中

每当Owner中的线程执行完退出同步块,就会唤起EntryList中阻塞的线程,将锁分配给该线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值