系列文章目录
线程安全(一)java对象头分析以及锁状态
线程安全(二)java中的CAS机制
线程安全(三)实现方法sychronized与ReentrantLock(阻塞同步)
线程安全(四)Java内存模型与volatile关键字
线程安全(五)线程状态和线程创建
线程安全(六)线程池
线程安全(七)ThreadLocal和java的四种引用
线程安全(八)Semaphore
线程安全(九)CyclicBarrier
线程安全(十)AQS(AbstractQueuedSynchronizer)
0.准备
- jdk1.8环境
- 引入OpenJDK的jar包
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
1.新建对象在内存中的情况
代码示例:
public static void main(String[] args) {
Object o = new Object();
System.out.println("new Object:" + ClassLayout.parseInstance(o).toPrintable());
A a = new A();
System.out.println("new A:" + ClassLayout.parseInstance(a).toPrintable());
a.setFlag(true);
a.setI(1);
a.setStr("ABC");
System.out.println("赋值 A:" + ClassLayout.parseInstance(a).toPrintable());
}
static class A{
private boolean flag;
private int i;
private String str;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
运行结果:
概念
对象在内存中分为三个部分
- 对象头(Header)
- 实例数据(Instace Data)(可为空)
- 对齐填充(Padding)(非必须)
分析
- 新建Objec对象时,在内存里占用16个字节,其中Header占12个(markword占8个+classpointer占4个),没有实例数据,补充对齐4个。
- 新建对象A时,中Header占12个(markword占8个+classpointer占4个),实例数据中 boolean占一个字节,会补齐三个,int占4个,String占8个,无需补充对齐。
结论
- 新建Object对象,会在内存占用16个字节,其中Header占12个(markword占8个+classpointer占4个),没有实例数据,补充对齐4个。
- 如果对象头+实例数据的字节数能被8整除,则不需要补充对齐。
附:
-XX:+UseCompressedClassPointers(64位虚拟机ClassPointer是8个字节(64位),默认此压缩类指针指令是开启的,占4个字节(32位))
-XX:+UseCompressedOops (默认压缩类普通对象指针是开启的,占4个字节(32位))
2.MarkWord与锁升级
第一个字节中8bit
附:《深入理解java虚拟机第二版》图
2.1.偏向锁和轻量级锁
代码示例
public static void main(String[] args) {
Object lightObject = new Object();
try {
Thread.sleep(6000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object biasedLockObject = new Object();
System.out.println("---------------------------------------加锁前---------------------------------------");
System.out.println("偏向锁:" + ClassLayout.parseInstance(biasedLockObject).toPrintable() + "\n轻量级锁:" + ClassLayout.parseInstance(lightObject).toPrintable());
System.out.println("---------------------------------------加锁后---------------------------------------");
synchronized (biasedLockObject) {
System.out.println("偏向锁:" + ClassLayout.parseInstance(biasedLockObject).toPrintable());
}
synchronized (lightObject) {
System.out.println("轻量级锁:" + ClassLayout.parseInstance(lightObject).toPrintable());
}
System.out.println("---------------------------------------释放锁---------------------------------------");
System.out.println("偏向锁:" + ClassLayout.parseInstance(biasedLockObject).toPrintable() + "\n轻量级锁:" + ClassLayout.parseInstance(lightObject).toPrintable());
}
运行结果
拿出对象Header第一个字节进行分析
概念
- 默认情况,偏向锁有时延,直接建立对象为无锁-》轻量级锁-》无锁
- 增加延迟,偏向锁-》偏向锁-》偏向锁,同一个的对象每次运行时,加锁前对象头一样,加锁后和释放后是不一样的,因为线程把对象头里线程Id改为了自己线程的Id
- 轻量级锁执行过程:在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前的MarkWord的拷贝(Displaced Mark Word),然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功,该线程就拥有了该对象的锁,失败,则首先会检查对象的Mark Word是否已经指向当线程的栈帧,有就代表已拥有,直接进入同步,否则,被其他线程占用
- 轻量级锁解锁过程:通过CAS过程将对相当前的MarkWord与线程中的Displaced MarkWord替换回来,如果替换成功,则同步成功,失败,则说明有其他线程尝试获取过锁,就要在释放锁的同时,唤醒等待线程
分析
- JVM默认,偏向锁有时延,sleep一段时间后,就会开启偏向锁,也可以设定参数-XX:BiasedLockingStartupDelay=0
- 上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程 偏向锁不可重偏向 批量偏向 批量撤销
- 轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,偏向级锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。这个锁会偏向于第一个获得锁的线程,如果在接下来执行的过程中,该锁都没有被其它线程获取,则持有偏向锁的线程将永远不需要在进行同步。
- 当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式,同时使用CAS操作把获取到这个锁的线程ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步模块时,虚拟机都可以不再进行任何同步操作(例如 Locking UnLocking以及对Mark Word的Update等)。当有另一个线程尝试获取这个锁的时候,偏向模式就宣告结束。
根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标记为“01”)或轻量级锁定(标记为“00”)的状态,后续的同步操作就如轻量级锁那样执行。
2.2.偏向锁膨胀为重量级锁
代码示例
static Object object = null;
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().setName("线程0");
Thread.sleep(6000);
object = new Object();
System.out.print("初始偏向锁->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
sysn("线程1");
Thread.sleep(1000);
System.out.print("线程1占用,未有其他线程尝试获取锁->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
sysn("线程2");
Thread.sleep(1000);
System.out.print("线程1占用,线程2尝试获取锁后->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
Thread.sleep(10000);
System.out.println("释放锁->" + "线程名称:" + Thread.currentThread().getName() + ClassLayout.parseInstance(object).toPrintable());
}
private static void sysn(String threadName) {
new Thread(() -> {
Thread.currentThread().setName(threadName);
synchronized (object) {
System.out.print(threadName + "占用中->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
运行结果
分析
- 初始化对象可偏向状态,线程1占用时,变为偏向锁,其他线程也尝试占用时,膨胀为重量级锁,没有线程占用之后,对象状态还是膨胀状态
2.3.轻量级锁锁膨胀为重量级锁
代码示例
static Object object = null;
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().setName("线程0");
object = new Object();
System.out.print("无锁->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
sysn("线程1");
Thread.sleep(1000);
System.out.print("线程1占用,未有其他线程尝试获取锁->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
sysn("线程2");
Thread.sleep(1000);
System.out.print("线程1占用,线程2尝试获取锁后->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
Thread.sleep(10000);
System.out.println("释放锁->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
}
private static void sysn(String threadName) {
new Thread(() -> {
Thread.currentThread().setName(threadName);
synchronized (object) {
System.out.print(threadName + "占用中->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
运行结果
分析
- 初始化对象为无锁状态,线程1占用时,变为轻量级锁,其他线程也尝试占用时,膨胀为重量级锁,没有线程占用之后,对象状态还是膨胀状态
下图为《深入理解java虚拟机第二版》的图
借用一张网图,侵删
3.锁优化
3.1自旋锁与自适应自旋
自旋锁避免线程切换的开销,党要占用处理器的时间,锁被占用时间短,自旋等待好,占用时间长,白白消耗处理器资源,自选默认10次
3.2锁消除
代码上要求同步,但是被检测到不可能存在共享数据竞争 String.concatString()
3.3锁粗化
如果虚拟机检测到一系列操作都是对同一个对象反复加锁和解锁,将会把加锁的范围扩展到这个操作序列外部例如String的多个append()方法