一、synchronized
synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。可以保证同一时刻只有一个线程操作临界资源,可以保证临界资源的可见性。
1、synchronized的使用方式
修饰代码块,即同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
private Object object=new Object();
synchronized(object){
}
修饰普通方法,即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
private synchronized void add(){
}
修饰静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
private static synchronized void add(){
}
无论哪种实现方式,synchronized锁的都是某个对象,第一种方式锁的就是new Object,第二种方式锁的就是当前对象,第三中方式锁的是当前的Class对象
2、对象的结构
1、对象在堆中的的布局
- java对象的实例数据(不固定),例如他的属性,每个属性占几个字节
- 对象头(固定)
- 填充数据(JVM 在64位操作系统中,要求JVM中的对象是8byte的倍数,如果不满足就需要填充数据)
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
<scope>provided</scope>
</dependency>
private static Object object=new Object();
public static void main(String[] args) {
System.out.println( ClassLayout.parseInstance(object).toPrintable());
}
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
new Object 的布局:Object header=3*4=12 byte ; 4byte的填充数据 ;所以总共是16byte, 8byte的2倍。填充数据不是必然存在的,只有在对象头+实例数据不等于8byte的整数倍时才会存在。
以上是在64位操作系统中JVM开启指针压缩的情况,如果没有开启指针压缩呢?
- 开启(-XX:+UseCompressedOops) 可以压缩指针(JDK8在默认情况下开启的
- 关闭(-XX:-UseCompressedOops) 可以关闭压缩指针。
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 1c f7 16 (00000000 00011100 11110111 00010110) (385293312)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
由此可见 对于64位操作系统如果关闭指针压缩 Object header=4*4=16 byte ,刚好满足8byte的2倍,所以不需要填充数据
对象头的大小:
- 在32位系统下,存放Class Pointer的空间大小是4字节,MarkWord是4字节,对象头为8字节;
- 在64位系统下,存放Class Pointer的空间大小是8字节,MarkWord是8字节,对象头为16字节;
- 64位开启指针压缩的情况下,存放Class Pointer的空间大小是4字节,MarkWord是8字节,对象头为12字节;
3、什么是对象头
当前使用的虚拟机:HotSpot是JVM的一个实现是sun公司开发的,openJdk是HotSpot开源出来的源码
对象头的组成:object header=mark word+klass pointer
https://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
Mark Word的结构:
Bit-format of an object header (most significant first, big endian layout below):
32 bits:
--------
hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
size:32 ------------------------------------------>| (CMS free block)
PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
64 bits:
--------
unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
size:64 ----------------------------------------------------->| (CMS free block)
unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
在这里我们研究64位操作系统的Mark Word,Java对象处于5种不同状态时Mark Word的表现形式
unused:表示未使用,采用0做占位
biased_lock:占1bit,对象是否启用偏向锁标记。为1时表示对象启用偏向锁,为0时表示对象没有开启偏向锁。
lock:占2bit,锁状态标记位,该标记的值不同,整个Mark Word表示的含义不同。
age:占4bit,Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因,
hash:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向锁的时间戳。
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。
4、synchronized中锁的状态
java的线程采用的是操作系统的线程模型(是映射到操作系统原生线程之上),如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,而通过synchronized实现的同步锁(重量级锁)就是这么实现的,在Java 6之后Java官方对从JVM层面对synchronized较大优化,Java6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁;
1、 无锁:
对象刚创建时,还没有任何线程来竞争,对象的Mark Word 偏向锁标识位是biased_lock=0,锁状态lock=01,说明该对象处于无锁状态(无线程竞争它)。
2、偏向锁
在实际场景中,如果一个同步方法,只有一个线程来竞争锁,对于这种竞争不激烈的场景,无需使用操作系统的线程调度方式,采用偏向锁,可以大幅度提高性能;偏向锁的核心思想是,如果只有一个线程来竞争锁,那么锁就进入偏向模式。此时Mark Word 的结构也变为偏向锁结构,biased_lock=1, lock=01,并在Mark Word中记录自己偏爱的线程的ID ,此时HashCode被覆盖;当这个线程再次请求锁时,无需再做任何同步操作,只需要判断当前线程Id与锁中Mark Work中记录的线程ID是否相等即可;如果不相等则升级为轻量级锁。如如果在锁竞争比较激烈的场合,偏向锁就会失去作用,因为这样场合极有可能每次申请锁的线程都是不相同的,会升级为轻量级锁。
3、轻量级锁
轻量级锁:轻量级锁的主要目的是在竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景,通过CAS竞争锁,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
轻量级锁的加锁步骤:
- 在代码进入同步块时,如果同步对象锁状态为无锁状态(biased_lock=0, lock=01)或是偏向锁(biased_lock=1, lock=01),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
- 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向object mark word。如果更新成功,则执行步骤3,否则执行步骤4。
- 如果这个更新动作成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
- 如果这个更新操作失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
既然轻量级锁使用了CAS那他不然会存在,自旋过久占用CPU资源的情况;
自旋锁的缺陷:自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋的效果就很好。反之,如果所被占用的时间很长,自旋就是在白白浪费处理器时间。所以,自旋等待的时间必须要有限度,默认情况下是10次,也可以通过 -Xx:PreBloackSpin来更改。如果在自旋10次都没有获得锁,就应该挂起线程。
轻量级锁自旋的优化:
自适应自旋锁:自旋锁在Java1.6中改为默认开启,并引入了自适应的自旋锁;自适应意味着自旋的次数不在固定,某个线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。XX:+UseSpinning参数来开启自旋(JDK1.6之前默认关闭自旋)
锁消除:JVM检测到不可能存在共享数据竞争,JVM会对这些同步锁进行锁消除,也就是取消加锁操作。如下代码,如果多线程调用test()方法,由于操作的变量是一个私有变量,所有对于StringBuffer中的加锁操作,JVM认为是不必要的。
锁粗化
锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。以此来减少在锁操作上的开销。
在一个循环中多次加锁,解锁可以调整为:
4、重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来实现,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的。
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0; //锁的重入次数
_object = NULL;
_owner = NULL; //_owner指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
- owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
- _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
- _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中
- _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中
二、synchronized的特点
1、synchronized的可重入性
- 可重入特性:一个线程得到一个对象锁后再次请求该对象锁,是允许的;当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法
- 可重入原理:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁
- 可重入的作用:可以避免死锁;可以让我们更好的来组织代码
2、不可中断:对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。
3、非公平