Java中的锁

(1)、悲观锁和乐观锁

1)、概念

        悲观锁和乐观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。

        悲观锁:对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候总有别的线程来修改数据,所以每次在读写数据的时候都会上锁,这样的话别的线程想要读写这个数据就会block直到拿到锁。在Java中,synchronized关键字和Lock的实现类都是悲观锁,AQS框架下的锁则是先是尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如ReentrantLock。

        乐观锁:每次在去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下再次期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写操作。java中的乐观锁基本都是通过CAS操作实现的,CAS是一种原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

2)、图示

 (2)、synchronized同步锁

        synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized翻译为中文的意思是同步,也称之为“同步锁”。

        synchronized的作用的是保证在同一时刻,被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。

2.1)、synchronized的使用方式

1.修饰实例方法:作用于当前实例加锁
public synchronized void method(){
// 代码
}
2.修饰静态方法:作用于当前类对象加锁
public static synchronized void method(){
    // 代码
}
3.修饰代码块:指定加锁对象,对给定对象加锁
synchronized(this){
//代码
}

2.2)、synchronized的底层实现

        synchronized的底层实现是完全依赖JVM虚拟机的,所以在谈synchronized的底层实现,就不得不谈数据在JVM内存的存储,Java对象头以及Monitor对象监视器。

Java对象头

        对象头的组成:由对象头(MarkWord和ClassPointer)、实例属性(instance data)、对齐填充组成。

MarkWord(标记字段):用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等等。

64位虚拟机对象头的实现:https://img-blog.csdnimg.cn/1000a12417f2485a93c961fbc6605252.png

ClassPinter(对象指针):用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。

数组长度:如果对象是一个Java数组,那么在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定对象的大小,但是从数组的元数据中无法确定数组的大小。

实例数据:实例数据部分是对象真正存储的有效信息,也既是我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的都需要记录下来。

 2.3)、synchronized锁升级的过程

        无锁:无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

        偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁的时候,会在对象头和栈针中记录存储锁偏向的线程id,以后该线程在进入同步块的时候先判断对象头的MarkWord里是否存储着指向当前线程的偏向锁,如果存在就直接获取锁。

        轻量级锁:当其他线程尝试竞争偏向锁时,锁升级为轻量级锁。线程在执行同步块之前,jvm会先在当前线程的栈针中创建用于存储锁记录的空间,并将对象头中的MarkWord替换为指向锁记录的指针,如果成功,当前线程获得锁,如果失败,标识其他线程竞争锁,当前线程便尝试使用自旋锁来获取锁。

        重量级锁:锁在原地循坏等待的时候,实惠消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循坏反而会消耗CPU资源。默认情况下锁自旋的次数是10次,可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。10次后如果还没获取锁,则升级为重量级锁。

2.4)、synchronized的核心组件

        Wait Set:那些调用wait方法被阻塞的线程被放置在这里;

        Contention List:竞争队列,所有请求所的线程首先被放在这个竞争队列中;

        Entry List:Contention List中那些有资格称为候选资源的线程被移动到Entry List中;

        OnDeck:任意时刻,最多只有一个线程正在竞争资源,该线程被称为OnDeck;

        Owner:当前已经获取到所有资源的线程被称为Owner;

        Owner:当前释放锁的线程;

2.5)、synchronized的实现

        (1)JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是在并发情况下,Contention List会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到Entry List中作为候选竞争线程;

        (2)Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryLIst中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程);

        (3)Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁,这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”;

        (4)OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的任然停留在EntryList中,如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中;

        (5)处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的;

        (6)Synchronized是非公平锁,Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源;

        (7)每个对象都有个monitor对象,加载就是在竞争monitor对象,代码块加锁是在前后分别加上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标记位来判断的;

       (8)Synchronized是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。

        (9)java1.6,Synchronized进行了很多的优化,有适应自旋、锁粗化、轻量级和偏向锁等,效率有了本质上的提高,在之后退出的java1.7和java1.8中,均对该关键字的实现机制做了优化,引入了偏向锁和轻量级锁,都是在对象头中有标记位,不需要经过操作系统加锁。

ref:13张图,深入理解Synchronized

(3)ReentrantLock锁

        ReentrantLock是一个可重入的锁,支持非公平和公平两种模式。底层是通过AQS来实现的。

3.1)、ReentrantLock的结构组成

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值