Java中的锁

3 篇文章 0 订阅
1 篇文章 0 订阅

一. Java的锁机制

1.1 锁的类型

乐观锁:读多写少的情况,读数据不加锁,写数据加锁。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁:悲观的意思是写多, 每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。
java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
Java线程:java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换。
synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。

1.2 Java对象

对象结构:在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:
对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机的对象头包括:
  • markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。
  • 对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.
  • 如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

一个Java对象在内存中的布局可以连续分成两部分:instanceOop(继承自oop.hpp)和实例数据
执行new A()的时候,JVM 做了什么工作。首先,如果这个类没有被加载过,JVM就会进行类的加载,并在JVM内部创建一个instanceKlass对象表示这个类的运行时元数据(相当于Java层的Class对象)。初始化对象的时候(执行invokespecial A::),JVM就会创建一个instanceOopDesc对象表示这个对象的实例,然后进行Mark Word的填充,将元数据指针指向Klass对象,并填充实例变量。
元数据—— instanceKlass 对象会存在元空间(方法区),而对象实例—— instanceOopDesc 会存在Java堆。Java虚拟机栈中会存有这个对象实例的引用。

二. Java中的锁

2.1 自旋锁

如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
自旋锁需要消耗cpu,所以需要设定一个自旋等待的最大时间。
自旋锁主要是减少线程的阻塞,适用于锁的竞争不激烈,且占用锁时间非常短的代码块。

2.1 重量级锁synchronized


它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
Owner:当前已经获取到所资源的线程被称为Owner;
!Owner:当前释放锁的线程。
Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

2.3 Java偏向锁(Biased Locking)

偏向锁是Java6引入的一项多线程优化。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

三. CAS

CAS:现代的处理器都包含对并发的支持,其中最通用的方法就是比较并交换(compare and swap),简称CAS。
CAS是非阻塞的同步机制,锁解决的是阻塞的同步机制,但是锁需要借助操作系统,线程的挂起和恢复都有很大的开销。而CAS在不释放cpu的情况下进行同步,这是CAS的功能。
  • CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。

3.1 ABA问题

ABA问题因为CAS需要操作值得时候,会检查值有没有变化,如果没有变化,像上面提到这回进行跟新操作,假设有这么一种情况原来是A变成的B,又变成了A,那么CAS 进行检查的时候就会发现他们没有变化,但实际上是变化了的,这便会存在问题,本来不应该跟新的,结果却跟新了。ABA问题的解决思路是加版本号,即在变量前追加版本号,每次跟新的时候版本号加一,如A—>B----→A  就会变成1A---->2A---→3A.这样就很好的解决了ABA 问题

3.2 Atom原子类

JVM是支持CAS的,体现在我们常用的Atom原子类,拿AtomicInteger分析一下源码

对AtomicInteger进行+1操作,循环里,会将当前值和+1后的目标值传入compareAndSet,直到成功才跳出方法。compareAndSet是不是很熟悉呢,接着来看看它的代码。

compareAndSet调用了unsafe.compareAndSwapInt,这是一个native方法,原理就是调用硬件支持的CAS方法。看懂这个应该就能明白Atom类的原理,其他方法的实现是类似的。 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值