Synchronized详解
1、同步器的意义
- 多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。
- 共享:资源可以由多个线程同时访问
- 可变:资源可以在其生命周期内被修改
- Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock
- 同步器的本质就是加锁,加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)
- 当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。
2、synchronized原理
- synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现。
- Monitor对象存在于每个Java对象的对象头Mark Word中,Synchronized锁便是通过这种方式获取锁的。
- 加锁的方式:
- 实例方法:锁是当前实例对象;
- 类方法:锁是当前类对象;
- 代码块:锁是括号里面的对象;
- 代码块
- synchronized 同步语句块的实现使用的是 monitor enter 和 monitor exit 指令,其中monitor enter 指令指向同步代码块的开始位置,monitor exit 指令则指明同步代码块的结束位置。当执?monitor enter 指令时,线程试图获取锁也就是获取 monitor的持有权。
- 当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执? monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外?个线程释放为?。
- 方法
- 当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置
了,执行线程将先持有monitor然后再执行方法,最后再方法完成时释放monitor。
3、对象的内存布局
- HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- HotSpot虚拟机的对象头包括两部分信息,第一部分是“Mark Word”,用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键。
4、锁升级
Synchronized的执行流程
- 检查Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态;
- 如果为可偏向状态,则检查线程ID是否指向当前线程,如果是则表示当前线程处于偏向锁状态,然
后执行同步代码; - 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID;
- 设置为当前线程,偏向标志位设置为1,锁标志位设置为01,然后执行同步码块;
- 如果竞争失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁;
- 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁;
- 如果替换失败,表示其他线程竞争锁,当前线程尝试自旋获取锁;
- 如果自旋成功,则依然处于轻量级锁状态;
- 如果自旋失败,则轻量级锁膨胀为重量级锁(monitor),后面等待锁的线程也要进入阻塞状态;
锁升级的过程(重点)
无锁状态 —> 偏向锁 —> 轻量级锁(自旋锁) —> 重量级锁
- 当想保证线程安全而且经常只有一个线程使用资源的话就会从无锁状态升级为偏向锁。
- 当偏向锁出现竞争现象,就会升级为轻量级锁。
- 轻量级锁出现竞争时会自旋等待,如果竞争过大或者自旋等待时间过长,就会从轻量级锁升级为重
量级锁。
偏向锁
- 主要用来优化同一线程多次申请同一个锁的竞争。
- 偏向锁的作用:当一个线程再次访问这个同步代码或方法时,该线程只需去对象头的 Mark Word 中去判断一下是否有偏向锁指向它的 ID,无需再进入 Monitor 去竞争对象了。
轻量级锁
- 判断当前对象是否处于无锁状态,若是,将在当前线程的栈帧中建立一个名为锁记录(Lock Record),用于存储锁对象目前的Mark Word的拷贝用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00;
- 否则, 判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当
前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,需要膨胀为 重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;
自旋锁的实现
- 当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
重量级锁 monitor对象(ObjectWaiter、WaitSet、EntryList、Owner)
- WaitSet 和 EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),owner指向持有ObjectMonitor对象的线程
- 当一个线程尝试获得锁时,如果锁没有被占用,线程获取到对象的monitor后,把monitor中的owner 变量设置为当前线程,同时monitor中的计数器count加1;
- 如果该锁已经被占用,将该线程封装成一个ObjectWaiter对象插入 EntryList 队列尾部;
- 线程占有锁时调用了wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
|