上一篇中,涉及到了锁升级的过程,也对其锁的升级有了一个大概的了解:单线程持有,在jvm延迟偏向的时间内是轻量级锁,之后为偏向锁,出现多个线程交替执行,对同一资源加锁会升级为轻量级锁,多个线程竞争拿不到锁会升级为重量级锁。在上一篇的基础上再进一步的了解锁升级的过程。
上一篇中,我们发现jvm的优化过程中,存在延迟偏向,我们通过让线程睡眠过了延迟时间之后,锁从一开始的轻量级锁变为偏向锁(也可以设置参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0,将延迟时间设置为0,),来查看单线程对锁的作用。
还有一种情况:偏向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) {
TestA a = new TestA();
Thread t1 = new Thread(){
@Override
public void run() {
synchronized (a) {
System.out.println("第一个线程。。。。");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread(){
@Override
public void run() {
synchronized (a) {
System.out.println("第二个线程。。。。");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
t2.start();
}
看结果前,不妨猜测一下,线程t2是不是轻量级锁?
第一个线程。。。。
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 b8 ab 1f (00000101 10111000 10101011 00011111) (531347461)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
第二个线程。。。。
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 b8 ab 1f (00000101 10111000 10101011 00011111) (531347461)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
结果是两个都是偏向锁,首先他不存在资源竞争,交互获取,不看结果真不知道是偏向锁,而且他说表示的线程ID(531347461 这里他不是确切的线程ID,可以把它做是)也是一样的,这里出现一个线程复用的问题,第一个线程被销毁后,第二个线程会拿到同样的ID,然后同步操作时会判断线程ID是否是一样,不一样就会升级为轻量级锁。(非官方,非权威,个人理解)
尝试在第二个线程前面增加一个线程:
new Thread(()->{
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
结果很明显的从偏向锁变为轻量级锁了。
第一个线程。。。。
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 78 c9 1f (00000101 01111000 11001001 00011111) (533297157)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
第二个线程。。。。
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 60 f6 7c 20 (01100000 11110110 01111100 00100000) (545060448)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
批量重偏向:
当一个线程实例化多个对象并执行同步操作,后来另一个线程也对这些对象进行同步操作,进行了多次撤销偏向锁后,jvm会认为接下来的这些对象都需要批量重偏向,那么接下来的对象都是偏向锁;
因为线程在同一个线程里执行相同的操作,并去对同一个对象进行操作,致使产生这样的结果。
这里可以这样设置:
当然我们可以通过-XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值
这里也存在上面偏向锁的问题,在第二个线程前增加一个线程去除线程复用,出现的结果才会是正常结果,那如果不加呢,出现的结果是概率性的,说不定某次你运行的时候,结果全部是偏向第一个线程,你会认为这就是重偏向,其实不对,你再多运行几次,就会发现,结果有很大的不同,jvm默认的批量重偏向阈值为20-40,那么正确结果是前19个,是轻量级锁,从第20个开始到40个结束,会是偏向锁。
public static void main(String[] args) {
List<TestA> list = new ArrayList<>();
Thread t = new Thread(()->{
for (int i = 0; i < 50; i++) {
TestA a = new TestA();
synchronized (a) {
list.add(a);
if (i == 49) {
System.out.println("第一个线程操作------");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
}
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 增加一个线程 ,去占用第一个线程,避免线程复用
new Thread(()->{
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("第二个线程操作------------------");
Thread t2 = new Thread(()->{
for (int i = 0; i < list.size(); i++) {
synchronized (list.get(i)) {
if(i == 18 || i == 19){
System.out.println("第二个线程 计数:"+(i+1)+"-------------");
System.out.println(ClassLayout.parseInstance(list.get(i)).toPrintable());
}
}
}
});
t2.start();
}
结果太长,就贴每个操作的第一次:
第一个线程操作------
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 a8 25 20 (00000101 10101000 00100101 00100000) (539338757)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
第二个线程操作------------------
第二个线程 计数:19-------------
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 18 f4 cb 20 (00011000 11110100 11001011 00100000) (550237208)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
第二个线程 计数:20-------------
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 01 3b 20 (00000101 00000001 00111011 00100000) (540737797)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
批量撤销:
根据上面批量重偏向得到的结果来看,线程A对多个对象实例化同步,线程B对这些对象同步操作,但是如果线程B只对这些对象操作20个,出现批量偏向,然后后面的不进行同步操作,这时,线程C过来了,继续对这些对象同步,当超过批量撤销的阈值后,就会将所有的对象转为轻量锁。
这里结果没得到证明,留下问题,之后再来研究。
public static void main(String[] args) {
TestA a = new TestA();
long start = System.currentTimeMillis();
new Thread(()->{
synchronized (a) {
System.out.println(Thread.currentThread().getName()+" running ");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
System.out.println("线程执行时间:"+(System.currentTimeMillis() - start ));
}
},"次线程").start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println(Thread.currentThread().getName()+" running ");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
可见,交替执行的线程是轻量级锁
次线程 running
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 80 f6 58 20 (10000000 11110110 01011000 00100000) (542701184)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
线程执行时间:3719
main running
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 88 f1 9b 02 (10001000 11110001 10011011 00000010) (43774344)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
如果在第一个线程修改如下,加锁后增加睡眠时间,分别设置2秒和5秒,
new Thread(()->{
synchronized (a) {
System.out.println(Thread.currentThread().getName()+" running ");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
try {
// 分别设置2秒和5秒
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程执行时间:"+(System.currentTimeMillis() - start ));
}
},"次线程").start();
结果会发现:
设置两秒的时候,最后的对象是轻量级锁,因为避开了资源争夺,设置5秒的时候,第一个线程持有,并没有释放,导致第二个线程一直在申请锁,最后锁膨胀为重量级锁。
总结:
偏向锁产生情况:
轻量级锁产生情况:
重量级锁产生情况: