synchronized
Synchronized
原子性
所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
注意:synchronized和volatile特性的最大区别在于原子性,volatile不具备原子性
Synchronized使用
Synchroniezd可以在静态方法,成员方法,成员对象属性上使用,归根结底它上锁的资源只有两类:一个是对象,一个是类.
同步原理
Synchronized在软件层面依赖JVM实现锁,而Juc.Lock在硬件层面依赖特殊的CPU指令.
下面看一个Synchronized例子:
1 |
|
查看反编译字节码:
每个对象都有一个监视器锁,当monitor被占用时就会处于锁定状态,获得锁相当于获得了对象的监视器,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后姜进入数设置为1,该线程即为monitor的所有者
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
- 如果其他线程已经占用了monitor
锁的四种状态
在 synchronized 最初的实现方式是 “阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,这种方式就是 synchronized实现同步最初的方式,这也是当初开发者诟病的地方,这也是在JDK6以前 synchronized效率低下的原因,JDK6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
所以目前锁状态一种有四种,从级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁,锁状态只能升级,不能降级
java对象头
我们以 Hotspot 虚拟机为例,Hopspot 对象头主要包括两部分数据:Mark Word(标记字段) 和 Klass Pointer(类型指针)
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
在上面中我们知道了,synchronized 用的锁是存在Java对象头里的,那么具体是存在对象头哪里呢?答案是:存在锁对象的对象头的Mark Word中,那么MarkWord在对象头中到底长什么样,它到底存储了什么呢?
无锁:对象头开辟 25bit 的空间用来存储对象的 hashcode ,4bit 用于存放对象分代年龄,1bit 用来存放是否偏向锁的标识位,2bit 用来存放锁标识位为01
偏向锁: 在偏向锁中划分更细,还是开辟 25bit 的空间,其中23bit 用来存放线程ID,2bit 用来存放 Epoch,4bit 存放对象分代年龄,1bit 存放是否偏向锁标识, 0表示无锁,1表示偏向锁,锁的标识位还是01
轻量级锁:在轻量级锁中直接开辟 30bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为00
重量级锁: 在重量级锁中和轻量级锁一样,30bit 的空间用来存放指向重量级锁的指针,2bit 存放锁的标识位,为11
GC标记: 开辟30bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。–
Monitor
Monitor可以理解为一个同步工具或者一种同步机制,通常被描述为一个对象,每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁.
Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表,每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程唯一标识,表示该锁被这个线程占用.
偏向锁的加锁过程:
- 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态
- 如果为可偏向状态,则判断线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁,如果竞争成功,则将MArk Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4.
- 如果CAS获取偏向锁失败,则表示有竞争,当到达全局安全点时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码
- 执行同步代码
轻量锁的加锁过程
轻量锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时锁就会升级为轻量锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能.
轻量锁的获取主要分为两种情况:
- 当关闭偏向锁功能时
- 由于多个线程竞争导致偏向锁升级为轻量级锁
一旦有第二个线程加入锁竞争,偏向锁就会升级为轻量级锁,竞争是指多个线程轮流获取锁,运气不好,遇到阻塞的情况.
在轻量级锁状态下继续竞争,没有抢到锁的线程将自旋,即不停循环判断锁是否能够被成功获取,获取锁
的操作就是通过CAS修改对象头里的标志位,先比较当前锁是否未”释放”,如果是将其修改设置为锁定,然后线程将当前锁的持有者信息修改为自己.
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等.
重量级锁
忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!