浅谈Synchronized关键字

Java中锁的概念

自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取锁才会退出循环。

乐观锁:假设没有冲突,在修改数据时发现数据和之前获取的不一致,则读最新数据,修改后重复修改。

悲观锁:假设会有发生冲突,同步所有对数据的相关操作,从读数据就开始上锁。

独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能在家锁(单写)。

共享锁(读):给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁(多读)。

可重入锁:在同一个线程内,可以重复加锁,也需要对应的释放掉锁。

不可重入锁:同一个线程内,只能加一次锁。

Synchronized关键字就是一个由JVM提供的可重入的悲观锁

Synchronized加锁的范围

下面两个方法等价,锁住的都是当前实例对象

public synchronized void add() {
    i++;
}
public void add01(){
    synchronized(this){
        i++;
    }
}

下面两个方法等价,sync修饰static方法的话锁住的是当前类对象

//只要在同一个jvm里面,sync关键字锁住的就是同一个类对象,
//因为类对象是被加载成一个二进制文件放在JVM里,在被jvm加
//载到方法区,方法区是线程共享的,所以两个方法锁的是同一
//个方法区里的类对象。
public static synchronized void add_static() {
    i++;
}
public static void add_static01() {
     synchronized(类名.class){
        i++;
    }
}
  • 如果锁是在两个jvm里面(例如访问同一个数据库),锁也是无效的(相当于两个服务器去访问一个数据库)这时候需要用到分布式锁

synchronized关键字的作用

  1. 用于实例方法,静态方法时,隐式指定锁对象。
  2. 用于代码块时,显式指示锁对象。
  3. 锁的作用域:对象锁、类锁、分布式锁。
  4. 特征:可重入、独享、悲观锁。

JVM对sync关键字的优化

锁消除:必须是在单个线程里面,没有别的线程来抢锁,但他自己却在多次加锁解锁,达到一定次数,jvm就进行了锁消除优化,把锁去掉,减少加锁和释放锁操作。事例:Append()方法是加了syn关键字的,在这个单线程里多次调用append()方法会进行多次加锁和写锁如下:

//StringBuffer是线程不安全的,底层加了syn关键字
//jit优化,消除了锁
StringBuffer test = new StringBuffer();
test.append("a");
test.append("b");
test.append("c");
test.append("d");
test.append("b");

锁粗化:JVM对syn关键字进行了锁粗化:对于多个代码块都采用了syn关键字,jvm就把他们放在了一个syn里面,但是如果有某一个syn修饰的代码块里存在耗时操作,jvm就不会对代码进行锁粗化,因为syn会占用太久就没必要进行锁粗化。但是这种代码应该由开发者自己进行优化,不应该交给jvm进行优化。(可以在多线程场景下)

Synchronized底层结构

一个对象被syn修饰的时候,加锁和解锁的状态是存放在哪里?

下面给出解释,先看一段代码

class Teacher{
    String name = "Kody";
    int age = 40;
    boolean gender = true;

    Student stu;
}

New Teacher()存放在堆中,里面存放Teacher的字段,ref是Teacher()实例里面的对象,指向隔壁的Student()实例,padding是为了补齐这个实例,因为这个实例的长度必须为8的整数倍。
下面是Teacher()对象在堆中存放的图解:
在这里插入图片描述
其中存放锁相关的信息的地方是在对象头里的Mark Word里面,Mark Word是由32位/64位(位数根据操作系统的位数来确定)的二进制组成,存储对象自身的运算时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志位、线程持有的锁、偏向线程ID、偏向时间戳、对象分代年龄。


sync锁的五种状态:

  • Unlocked未锁定状态,Bitfields里面存放的是HashCode|Age|0,Tag里面存放的是01,其中Bitfields里面的HashCode存放的是对象自身的哈希值,0代表关闭偏向锁,如果关闭偏向锁的话Tag里面的01代表是无锁状态。
  • Biased/Biasable偏向锁,Bitfields里面存放的是ThreadID|Age|1,Tag里面存放的是01,其中Bitfields里面的ThreadID存放的是获取到当前锁的线程的ID(默认为0),1代表开启偏向锁,如果开启了偏向锁Tag里面的01代表的是偏向锁。
  • Light-weightLocked:轻量级锁,Bitfields里面存放的是LockRecordAddress,Tag里面存放的是00,其中Bitfields里面的LockRecordAddress代表的是指向锁记录的指针,Tag里面的00代表是轻量级锁。
  • MonitorAddress重量级锁,Bitfields里面存放的是MonitorAddress,Tag里面存放的是10,其中Bitfields里面的MonitorAddress代表的是指向重量级锁记录的指针(对应Monitor),Tag里面的10代表是重量级锁。
  • MarkedForGC:GC标记,Bitfields里面存放的是ForwardingAddress/etc.,Tag里面存放的是11,其中Bitfields里面的ForwardingAddress/etc.代表的是空,不需要存入信息,Tag里面的11代表GC标记。

sync如何进行加锁操作

  1. 在代码进入同步代码块的时候,如果同步对象锁属于未锁定状态,虚拟机会为当前线程创建一个锁记录(Lock Record)的空间,用于存储锁对象目前的MarkWord的拷贝,然后进行CAS操作(如下图),cas操作中old值是上一个成功获取锁记录的线程拷贝过去的,new值是新的线程cas操作成功后存放的记录。修改成功之后,栈帧里面有一个owner字段,指向Mark Word,用来记录哪个线程对markWord进行了加锁,以便以后解锁时可以找到;Lock Record Address是存放成功修改它的Lock Record的地址(哪个线程修改成功就存放该线程的地址)。
    在这里插入图片描述
  2. 对于别的线程在进行抢锁时可能会失败,对于失败的线程,就会开始自旋,当自旋达到一定次数的时候,锁就从轻量级锁升级成重量级锁。
  3. 如果有新的线程加入进来抢锁,这时候MarkWord已经被前面的线程修改并占用了,新进入的线程复制不到锁对象的MarkWord了(堆里面的对象是共享的,这个对象的对象头已经被前面的线程锁定了还没有释放掉),这时候直接升级成重量级锁(状态位从00(轻量级锁)修改成10(重量级锁))。
    在这里插入图片描述
  4. 锁升级成重量级锁之后,Lock Record Address 修改成了 Monitor address,Tag从00改变成了10,Java底层里每个对象都有一个Monitor对应,Monitor是由objectMonitor实现的,里面存放的主要是_entryList、_WaitSet、owner等信息。

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象);

  • _entryList:用来存放处于等待锁(blocked阻塞状态)的线程。
  • _WaitSet:用来存放waitting状态的线程(线程调用wait()方法之后就会进入到这个里面)。
  • Owner:当锁升级成重量级锁之后,owner指向的是获取锁的线程。
  1. 如果当前得到锁的线程执行完毕,会调用objectMonitor的monitorExit(),会把owner变成null(owner=null),同时count-1,然后count变成了0,只有count为0,owner为null的时候别的线程才可以来尝试获取锁;如果当前线程执行了wait()方法,也会释放掉锁,然后进入到waitSet队列里,会把owner变成null(owner=null),同时count-1,然后count变成了0,当执行了notify()/notifyAll()方法时,会唤醒waitSet队列里的线程,线程重新进行抢锁,如果抢锁失败的话就进入到entryList队列里面,进行排队等待,状态从waitting改成了blocked,monitor是非公平的,如果在一个线程出去的瞬间,又进来一个新的线程会和队列里出来的线程同时进行抢锁,可能新进来的线程会拿到锁,所以是sync默认是非公平的锁。

锁的升级过程

先通过下面的图解大致了解一下锁的流向
锁升级
图片解析:左边两个开启了偏向锁,偏向锁就是通过一个字段来区分是否开启偏向锁(1代表开启,0是关闭),当0位置存放了threadId之后就代表是被线程获取。偏向锁主要是用于某一个syn关键字修饰的代码块并没有多个线程去抢锁,所以就没必要来回的进行加锁和释放锁,当单个线程来执行syn修饰的代码块后,会存放当前线程的threadID,偏向锁不会释放锁,只是记住了这个threadId,下次这个线程再次访问的时候判断一下是否是当前线程,是的话继续执行,不是的话就要进行锁升级,如上图所表示。
第一种:偏向锁被占用就升级成轻量级锁,如果线程执行完成并释放锁的话就回到未锁定状态,并且关闭偏向锁(把1改成0),如果出现了多个线程自旋抢锁到达一定次数之后就要升级成重量级锁。
第二种:在偏向锁出现争抢要进行锁升级的一瞬间,偏向锁释放掉了锁,锁就未被占用,就回到未锁定状态,并且关闭偏向锁(把1改成0)。

锁只会升级不会降级,重量级锁执行完后会变成未锁定状态,再次拿到锁之后会直接变成重量级锁,不会再重复之前的操作。

sync总结

从上面的叙述我们可以总结一下sync底层的这几种锁:

  • 偏向锁:只有一个线程进入临界区。
  • 轻量级锁:多个线程交替进入临界区。
  • 重量级锁:多个线程同时进入临界区。
  • 偏向锁使用 CAS 操作 + 检查 Mark Word 中的 Thread ID 来获取锁,所以它是一种乐观锁
  • 轻量级锁使用 CAS 操作 + 锁自旋来获取锁,所以它也是一种乐观锁,操作多了就占用cpu资源了就升级成重量级了。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值