目录
上一篇中,主要是对加解锁,还有锁的分类有了一个了解:
1. 锁在对象中的表示(无锁:001 ,偏向锁:101 ,轻量级锁: 000,重量级锁: 010)
2. 锁分:偏向锁、轻量级锁、重量级锁
3. jvm在启动时会有大概4秒的延迟偏向,所以,一开始程序运行是无锁状态,可以通过run时加入参数修改这个值
4. 偏向锁偏向锁在计算hashCode后不能在偏向
5. 调用wait方法后,直接升级为重量级锁
6. 长时间获取不到锁会升级重量级锁
1.偏向锁发生情况?
上一篇中,我们发现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过来了,继续对这些对象同步,当超过批量撤销的阈值后,就会将所有的对象转为轻量锁。
这里结果没得到证明,留下问题,之后再来研究。
2.轻量级锁什么时候发生的?
-
单个线程
-
多个线程交替执行
-
多个线程互斥执行
当一个线程去拿一个资源的时候,发现得不到资源,然后会先自旋一段时间,然后再去拿,如果再拿不到,那么就会膨胀,具体的自旋时间需要看jvm源码。
可以这样理解:一个线程去拿一个不属于自己线程的资源时,就会膨胀(不是很准确)
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秒的时候,第一个线程持有,并没有释放,导致第二个线程一直在申请锁,最后锁膨胀为重量级锁。
总结
偏向锁产生情况:
- 启动一段时间后;
- 单线程持有;
- 重偏向;
- 批量重偏向;
轻量级锁产生情况:
- 一开始启动那会是轻量级锁
- 互斥执行:线程A持有,线程B也想持有,但A持有中,B先自旋一段时间(这个时间jvm内部的,具体我不知道),拿到锁后,因为锁原本偏向A线程,这时被B拿走,就膨胀为轻量级锁,长时间拿不到就膨胀为重量级锁;
- 交替执行:线程A持有,线程B也想持有,但在A持有过程中,B没有去申请锁,在A释放后,B才去申请锁,这里存在重偏向问题,也不是真正的重偏向,及线程B会复用A的线程,在A B间再有一个线程可以避免复用;
重量级锁产生情况:
- 两个及两个以上竞争锁
- 调用wait方法后