Synchronized
一、使用
修饰类的:实例方法、静态方法、代码块;
实例方法:锁对象为当前实例对象:
public synchronized void sayHello(){
System.out.println("Hello World");
}
静态方法:锁对象为当前类Class对象:
public static synchronized void sayHello(){
System.out.println("Hello World");
}
代码块:
//锁对象
synchronized(this){}
//锁对象
synchronized(""){}
//锁class
synchronized(xxx.class){}
二、原理
2.1、Java对象头(Object Header)
普通对象:
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
数组对象:
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
其中Mark Word(32位,JVM为32位):
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Unlocked |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
- identity_hashcode:自身hashcode
- age:分代年龄,只有4位,最大值为15,所以-XX:MaxTenuringThreshold最大值为15;
- thread:偏向锁的线程ID
- lock:锁状态标记位,用尽可能少的二进制位表示尽可能多的信息。该标记的值不同,整个mark word表示的含义不同。
- biased_lock:是否可偏向,1可,0不可。
- epoch:本质是一个时间戳 , 代表了偏向锁的有效性。
- state:
- Unlocked:无锁(禁用偏向机制)。biased_lock:0;lock:01
- Biased:已有偏向锁/可偏向。biased_lock:1;lock:01
- Lightweight Locked:轻量级锁。lock:00;
- Heavyweight Locked:重量级锁。lock:10;
- Marked for GC:GC。lock:11;
2.2、Monitor 监视器
- count:该线程获取锁的次数,一共获取了多少次锁;
- recursions:锁的重入次数;
- WaitSet:线程调wait()时进入;
- EntryList:阻塞等待队列,竞争锁失败的线程会进入;
- Owner:指向获取锁的线程。
2.3、CAS
JNI(Java Native Interface):Java本机编程接口,JDK的一部分,JNI提供了若干的API,实现了Java和其他语言(主要是C&C++)通信(Java调用C/C++,C/C++调用Java);
- CAS (Compare And Swap) 指令是一个CPU层级的原子性操作指令。 在 Intel 处理器中, 其汇编指令为 cmpxchg。Java通过JNI调用;
- CAS操作包含三个操作数:内存位置(V)、预期原值(A)、新值(B);
如果 V 位置的值与 A 相匹配,那么改为 B 。否则不处理。无论哪种情况,都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。); - 类似乐观锁;
- ABA问题:A在比较时,其实是A->B->A,代表其他线程已经处理过(CAS获取锁时代表已占有,并中间还有过一次),此时CAS必须失败,但结果确会成功。解决:使用版本号;
- 循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
- pause指令:
- 可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多资源,延迟时间取决具体实现版本,有的是0;
- 可以避免在退出循环时因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU执行效率;
- 只能保证一个共享变量的原子操作,多个时:
1. 用锁,
2. i=2,j=a,合并为ij=2a,然后用CAS来操作ij;
3. Java1.5开始JDK提供AtomicReference:引用对象之间的原子性;
2.4、加锁流程
整体说明:
- 几乎无竞争:偏向锁;
- 轻度竞争:偏向锁 -> 轻量级锁;
- 在重度竞争:升级为重量级锁(不会下降);
如图:
- JVM 提供了关闭偏向锁的机制, JVM 启动命令参数:-XX:-UseBiasedLocking
无锁 -> 偏向锁
偏向锁获取方式:MarkWord 标记线程ID, 以表示哪一个线程获得了偏向锁。
- 首先读取 MarkWord, 判断状态:
-
可偏向状态(threadID为空;biased_lock:1;lock:01),尝试用 CAS 操作, 将自己的线程 ID 写入MarkWord;
- 如果 CAS 成功, 则获得偏向锁,如图;
- 一个线程在执行完同步代码块以后, 并不会尝试将 MarkWord 中的 thread ID 赋回原值 。这样做的好处是: 该线程再次加锁,而没有被其他线程获取过锁,依旧停留在可偏向的状态下, 即可在不修改对象头的情况下, 直接认为偏向成功;
- 如果 CAS 失败, 说明, 另外一个线程 B 抢先获取了偏向锁。 说明竞争比较激烈, 需要撤销 B 获得的偏向锁,将 B 持有的锁升级为轻量级锁。 该操作需要等待全局安全点;
- 如果 CAS 成功, 则获得偏向锁,如图;
-
已偏向状态(threadID有值;biased_lock:1;lock:01), 则检测 MarkWord 中存储的 thread ID 是否等于当前 thread ID 。
- 如果相等, 则证明本线程已经获取到偏向锁, 可以直接继续执行同步代码块;
- 如果不等, 则证明该对象目前偏向于其他线程, 需要撤销偏向锁;
- 偏向锁的 撤销(revoke) 是一个很特殊的操作, 需要等待全局安全点(Safe Point), 此时间点所有的工作线程都停止了字节码的执行。
偏向锁 -> 轻量级锁
超过一个线程竞争某一个对象时, 会发生偏向锁的撤销操作,偏向锁撤销后, 对象可能处于两种状态:不可偏向的无锁状态(biased_lock:0;lock:01)(之所以不允许偏向, 是因为已经检测到了多于一个线程的竞争, 升级到了轻量级锁的机制)、不可偏向的已锁(lock:00) ( 轻量级锁) ;
- 首先根据标志位判断出对象状态处于不可偏向的无锁状态如图:
不可偏向的无锁状态情况步骤如下:
- 当前线程栈帧中建立锁记录(Lock Record)空间,用于存储锁对象Mark Word的拷贝(Displaced Mark Word)。
- 拷贝Mark Word -> 锁记录,如图:
- CAS将Mark Word -> 锁记录的指针,并将锁记录的owner -> Mark Word。成功执行4,先自旋任失败再5;
- 程已拥有轻量级锁,并且Mark Word的锁标志位设置为“00”,如图:
- 将膨胀为重量级锁;
重量级加锁:
轻量级锁在向重量级锁膨胀的过程中, 一个操作系统的互斥量(mutex)和条件变量( condition variable )会和这个被锁的对象关联起来。
具体而言, 在锁膨胀时, 被锁对象的 markword 会被通过 CAS 操作尝试更新为一个数据结构的指针, 这个数据结构中进一步包含了指向操作系统互斥量(mutex) 和 条件变量(condition variable) 的指针;
- 如果t0,t1,t2在进行获取锁时,如果t0获取锁资源成功(成功的标志:CAS成功把monitor的_owner字段设置为当前线程(非公平));
- 那么t1和t2是不会立马挂起的,而是先通过CAS自旋的方式再次尝试获取锁;
- 如果失败则入队列(EntryList队列);
- 虽然自旋锁方式省去了阻塞线程的时间和空间(队列的维护等)开销,但是长时间自旋也是很低效的。所以自旋的次数一般控制在一个范围内,例如10,50等,在超出这个范围后,线程就进入排队队列;
- 被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响;
进入Synchronized同步块后,会通过对象的Mark Word获取monitor,获取了monitor才能调wait(),notify(),notifyAll() ;
三、内存可见性
synchronized能解决内存可见性问题,被synchronized加锁后,他会做出以下操作:
- 获取同步锁;
- 清空内存;
- 从主内存中拷贝新的对象副本到工作线程中;
- 继续执行代码,刷新主内存的数据;
- 释放同步锁;
四、总结
- 可保证原子性;
- 可保证内存可见性;获取锁,从主内存获取最新;释放锁,把最新写入主内存;简单读取最新变量可用volatile;
- 注意死锁:A和B线程分别持有A和B锁,分别等待B和A锁;应按顺序申请;
- 可重入性:重入和退出时有计数器增减;
volatile关键字
一、相关概念:
- 程序运行时的临时数据存放在主存(物理内存)中,但从主存写入/读取数据的速度比CPU执行速度慢很多,为了效率,CPU里面加入高速缓存;
- 多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的);
- 修改过程:主存读取数据 -> 复制到高速缓存 -> CPU计算 -> 写入高速缓存 -> 刷新到主存;
- 引发内存可见性问题:多个线程共享一个变量,A线程修改(没有立马刷新到主存),B线程不能马上看到(读取的自己线程的高速缓存),甚至永远也看不到;
- MESI协议保证了缓存一致性。核心的思想:写入共享变量时,会发出信号通知其他CPU将该变量的缓存行置为无效状态,当其他CPU读这个变量时,发现是无效就重新读取。
二、volatile修饰的变量
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 内存可见性:
1. 修改的值强制写入主存;
2. 其他线程缓存无效;
3. 其他线程读取时重新从主存获取; - 禁止指令重排;
- volatile不能保证原子性;
public class TestVolatile {
public static void main(String[] args) {
// 创建td线程,并启动
ThreadDemo td = new ThreadDemo();
Thread thread = new Thread(td);
thread.start();
// main线程负责检查flag是否被变为了true
while (true){
// 此处如果没有volatile修饰将读取缓存值,对象可能是缓存对象引用地址
if (td.isFlag()){
// 如果完成了工作,那么就全部停止
System.out.println("Thread td finished its work.");
break;
}
}
}
/**
* 此线程将自己类的属性进行修改,若属性不加volatile关键字,其他线程通过get方法获取的是旧的值
*/
static class ThreadDemo implements Runnable{
// 内置一个标志变量,使用volatile修饰,保证其他线程通过get方法获取新值
private volatile boolean flag = false;
// 工作任务是将自己线程内部的标志变量变为true
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("Changed flag from false to " + isFlag() + ".");
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
}