一,Synchorized语法演示
1,类方法演示;静态类同步是对类对象加锁,基于同一类对象的操作都会在阻塞队列等待执行(通过自定义类加载器可以实现类的不同加载)
package com.gupao.concurrent;
/**
* 类同步方法
* @author pj_zhang
* @create 2019-09-25 21:55
**/
public class StaticMethodSynchorized {
public static void main(String[] args) {
// 线程一执行
new Thread(() -> {
System.out.println("线程_1尝试竞争锁");
doSomething();
}, "线程_1").start();
// 线程二执行
new Thread(() -> {
System.out.println("线程_2尝试竞争锁");
doSomething();
}, "线程_2").start();
}
public synchronized static void doSomething() {
System.out.println(Thread.currentThread().getName() + "获取到当前锁");
// 自旋,不释放锁对象
for (;;) {}
}
}
2,实例方法演示;实例类同步是对类实例加锁,对同一类实例对象的加锁操作,需要等待执行;如果此处构建两个实例,并由两个实例分别在线程_1和线程_2中调用同步方法,则不会存在锁竞争;
package com.gupao.concurrent;
/**
* 实例同步方法
* @author pj_zhang
* @create 2019-09-25 21:55
**/
public class EntityMethodSynchorized {
// 此处构建同一对象进行同步方法处理
// 如果对象不同,则对象携带锁标志位不同,
// 不同的实例执行实例方法,会根据当前实例锁状态进行加锁处理
// 则线程1和线程2会全部执行
public static EntityMethodSynchorized synchorized = new EntityMethodSynchorized();
public static void main(String[] args) {
// 线程一执行
new Thread(() -> {
System.out.println("线程_1尝试竞争锁");
synchorized.doSomething();
}, "线程_1").start();
// 线程二执行
new Thread(() -> {
System.out.println("线程_2尝试竞争锁");
synchorized.doSomething();
}, "线程_2").start();
}
public synchronized void doSomething() {
System.out.println(Thread.currentThread().getName() + "获取到当前锁");
// 自旋,不释放锁对象
for (;;) {}
}
}
3,同步代码块演示(同样分实例锁和类锁);同步代码块的锁对象可以分为类锁和类对象锁,分别可以对应第一步和第二步演示的解释;
package com.gupao.concurrent;
/**
* 同步代码块处理
* @author pj_zhang
* @create 2019-09-25 22:07
**/
public class CodeBlockSynchronized {
// 此处构建同一对象进行同步方法处理
// 如果对象不同,则对象携带锁标志位不同,
// 不同的实例执行实例方法,会根据当前实例锁状态进行加锁处理
// 则线程1和线程2会全部执行
public static Object object = new Object();
public static void main(String[] args) {
// 线程一执行
new Thread(() -> {
System.out.println("线程_1尝试竞争锁");
doSomething();
}, "线程_1").start();
// 线程二执行
new Thread(() -> {
System.out.println("线程_2尝试竞争锁");
doSomething();
}, "线程_2").start();
}
public static void doSomething() {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "获取到当前锁");
for (;;) {}
}
}
}
二,Synchorized锁状态存储
1,javap编译查看锁状态
1.1 同步方法:从下图可以看到,jvm对同步方法添加了一个ACC_SYNCHRONIZED的flag。jvm底层会对该字节指令进行解析,添加monitor监视器,其他线程在执行到该方法时,会首先获取类对象(类方法)或者实例对象(实例方法)的monitor监视器。如果获取到,则加锁成功,如果获取不到,说明存在线程正在执行,进入同步队列进行线程等待;正在执行的线程执行完成后,会释放monitor监视器,并唤醒在同步队列中的线程进行线程等待
1.2 同步代码块:从同步代码块中可以很清晰看到两个字节指令,monitorenter(进入监视器)和monitorenter(退出监视器),监视器退出后,会唤醒同步队列中等待的线程,进行锁抢占;相对于同步方法,同步代码块在加锁和释放锁中更可控
2,对象的内存布局
2.1 从上图可以看出,Synchorized的加锁处理其实是对对象的monitor进行操作,则可以从锁的内存布局进行入手查看锁实现
2.2 对象在内存中分三个区域存储,分别是对象头(header),实例数据(Instance Data)已经对齐填充(Padding);其中对象头分为instanceOopDesc(instanceOop.hpp)和ArrayOopDesc(ArrayOop.hpp)描述,instanceOopDesc又继承自oopDesc(oop.hpp)。对普通实例对象的定义,oopDesc包含两个成员,分别是_mark和_metadata。_mark也就是Mark Word(markOop.hpp)记录了对象和锁有关的消息。Mark Word在虚拟机在32位和64位虚拟机的内存布局如下,其中定义了分代年龄(4位),是否偏向锁(1位)及锁标志位(2位)
3,锁在对象中的存储:Mark Word里面存储的数据会随着锁标志位的变化而变化,Mark Word大致表现为下面五种变化;线程执行会从无锁状态获取偏向锁,在存在线程竞争时,从偏向锁升级为轻量级锁,轻量级锁在指定的自旋次数后,如果还没有获取到锁,则膨胀为重量级锁;膨胀为重量级锁后,未抢占到锁的线程状态为BLOKED,进入同步队列,等待持有锁的线程执行完成后唤醒
三,Synchorized锁升级
1,锁升级的意义:加锁能够实现数据的安全性,但是同时带来性能的下降。对每一次加锁进行线程阻塞和调度额外增加CPU的压力,因此JVM提出了锁升级的概念,用来减少频繁获取锁和释放锁所带来的性能开销。锁存在四种状态:无锁,偏向锁,轻量级锁,重量级锁,锁的状态根据竞争的激烈程度从低到高不断升级。
2,无锁
* 对象初始化状态,还没有被加锁
3,偏向锁,
3.1 偏向锁的获取流程
a,首先获取锁对象的Mark Word,判断是否处于可偏向状态(biased_lock为1,且threadId为空)
b,如果是可偏向状态,则通过CAS操作,把当前线程ID写入到Mark Word
* 如果CAS成功,表示已经获取的锁对象的偏向锁,可以继续执行同步代码
* 如果CAS失败,说明已经有其他线程获取了偏向锁,说明存在线程竞争;这时候需要撤销Mark Word中偏向锁的线程ID,并把锁状态由偏向锁变更为轻量级锁(这个操作需要等到全局安全点执行)
c,如果是已偏向状态,则检查Mark Word中存储的线程ID是否是当前线程
* 如果是,不需要再次获得锁,直接执行同步代码
* 如果不是,则继续进行锁升级
3.2,偏向锁撤销:并不是把锁对象恢复到无锁可偏向状态(偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现CAS失败也就是存在锁竞争的过程中,直接把偏向锁升级到轻量级锁的状态;
3.3,偏向锁获取及撤销流程图
4,轻量级锁
4.1,轻量级锁的加锁流程:锁升级为轻量级锁后,锁对象的Mark Word会进行相应的变化
a,线程会在自己的栈帧中创建锁记录:LockRecord
b,将锁对象的Mark Word复制到线程刚刚创建的LockRecord中
c,将LockRecord中的Owner指针指向锁对象
d,将锁对象的Mark Word替换为指向锁记录的指针
4.2,轻量级锁解锁流程:轻量级锁的释放逻辑就是加锁逻辑的逆向逻辑
a,通过CAS操作把栈帧中的LockRecord替换回到锁对象的Mark Word中,如果替换成功说明没有竞争
b,如果替换失败,表示当前锁存在竞争,轻量级锁会膨胀为重量级锁
4.3,轻量级锁执行原理
a,轻量级锁的执行原理就是自旋,当存在线程来竞争锁时,该线程会通过执行一段无意义的循环不断尝试获取锁,而不是直接进行线程阻塞,等到正在执行的线程释放锁后,该线程会直接获取到锁进行代码执行
b,因为线程在自旋是,会存在CPU消耗,所以自选锁适用于那些同步代码快执行很快的场景
c,自选锁在JVM中默认重试次数是10,可通过 preBlockSpin 参数设置,JDK1.6后,引入了自适应自旋锁,即JVM会根据前一次的自旋时间动态判断当前的自旋次数,更合理的进行线程自旋规划
4.4,轻量级锁的加锁和解锁流程图
5,重量级锁:当线程膨胀为重量级锁后,则意味着线程只能被挂起阻塞等待被唤醒
5.1,重量级锁的加锁流程
a,重量级锁加锁,就是就是获取锁对象的 Monitor 监视器,如果获取成功,则执行
b,Monitor监视器获取失败,线程进入同步队列,线程转态为BLOCKED
c,当当前获取锁的线程释放Monitor监视器后,会唤醒在同步队列中等待的线程,重新进行锁竞争
5.2,重量级锁的释放流程
a,重量级锁的释放,其实就是同步代码执行完成后,对Monitor监视器的释放
5.3,重量级锁加锁和释放锁流程图
四,基于Synchorized的wait()/notify()流程分析
1,对wait()和notify()的理解,可以在前一步重量级锁竞争同步队列的基础上添加一层阻塞队列进行理解(参考AQS的Condition类)
2,多线程在无wait()情况下进行并发访问时,会有一道线程竞争到锁进行执行,其他线程在同步队列中等候,当线程释放锁时,会唤醒在同步队列中的线程进行锁争抢,并重复该流程
3,此时存在线程,在线程执行过程时进入线程等待(wait()),线程等待需要超时唤醒或者手动唤醒。在线程等待期间,线程不参与锁争抢,此时该线程存放在同步队列是不合适的,需要存在的阻塞队列,等线程被唤醒或者自动唤醒后,再从阻塞队列迁移到同步队列参与锁争抢
4,流程图如下