面试题

模拟银行账户,对业务写方法加锁,对业务读方法不加锁,这样行不行?

不行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
容易产生脏读问题(dirtyRead)
public class Account {
String name;
double balance;

public synchronized void set(String name, double balance) {
this.name = name;

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}


this.balance = balance;
}

public /*synchronized*/ double getBalance(String name) {
return this.balance;
}


public static void main(String[] args) {
Account a = new Account();
new Thread(()->a.set("zhangsan", 100.0)).start();

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(a.getBalance("zhangsan"));

try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(a.getBalance("zhangsan"));
}
}

synchronized注意事项

synchronized可重入,不然可能会发生死锁(同一个线程)

异常跟锁:程序之中如果出现异常,默认情况下,锁会释放

synchronized锁的是对象,不是代码。synchronized(Object)

synchronized不能锁String常量,Integer等基础类型

在竞争不是很激烈的情况下,尽量让锁的粒度细化一些,使性能更快。如果竞争很激烈,可以锁的粒度粗化,也可提升性能。

锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变,应该避免将锁定对象的引用变成另外的对象。(给引用加final)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class SyncSameObject {

/*final*/ Object o = new Object();

void m() {
synchronized(o) {
while(true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());


}
}
}

public static void main(String[] args) {
SyncSameObject t = new SyncSameObject();
//启动第一个线程
new Thread(t::m, "t1").start();

try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建第二个线程
Thread t2 = new Thread(t::m, "t2");

t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会

t2.start();

}
}

对象的内存布局

在虚拟机堆中new出对象,对象中首先是8个字节的markword,4个字节的类型指针,4字节成员变量(long类型8个字节),Hotspot要求对象8字节对齐(字节数是8的整数倍),不足对齐。

在Hotspot中,所谓的上锁就是修改markword。

suo

如上图,普通对象synchronized修饰加锁,升级为偏向锁,有线程与其竞争时偏向锁升级为轻量级锁(自旋锁,无锁 ),再竞争升级为重量级锁

上锁的本质

锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率.

偏向锁

偏向锁,顾名思义,它会偏向于第一个访问锁的线程

  • 如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。线程第二次到达同步代码块时,会判断此时持有锁的线程是否就是自己,如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
  • 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。偏向锁通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致STW(stop the word)操作;

锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

轻量级锁(自旋锁)

自旋锁:自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁。

重量级锁

当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起,等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

重量级锁的特点:其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。

锁的优缺点对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行速度较长。

摘自(32条消息) 深入理解Java锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁_carroll18的博客-CSDN博客_无锁,偏向锁

上锁的本质是修改线程的markword,从markword可以看出对象的锁状态,如下,先看后两位:

Hotspot下的锁状态

偏向锁,轻量级锁:用户空间锁(用户空间锁:不需要向操作系统申请)

偏向锁:把markword线程ID改为自己线程的ID

StringBuffer加synchronized上的就是偏向锁,前提是没竞争条件下。

如果有线程竞争,偏向锁撤销,升级为自旋锁:竞争条件下的线程在线程栈内部,生成LR(LockRecord 锁记录)。自旋的方式相互竞争锁,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁,其它锁会CAS的一直监控此锁是否释放(自旋等待),自旋超过10次后,会自动升级为重量级锁

重量级锁:需要向操作系统申请锁

看操作系统的interpreterRuntime.cpp文件

锁重入

synchronized 是可重入锁

重入次数必须记录,因为要解锁几次必须对应

偏向锁重入次数记录在线程栈的里LR中,每重入一次+1,

什么时候自旋锁升级为重量级锁?

竞争加剧:有线程超过10次自旋,-XX:PreBlockSpin,或者自旋线程数超过CPU核数的一半,1.6之后,加入自适应自旋Adapative Self Spinning,JVM自己控制

为什么有自旋锁还需要有重量级锁?

自旋消耗资源,如果锁的时间长,数量多,CPU会大量消耗。

重量级锁:有等待队列

在自旋锁升级为重量级锁时,自旋的线程都会放到队列(waitSet),不需要消耗资源。

偏向锁未启动与已启动

偏向锁是否一定比自旋锁效率高?

不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁。

JVM启动时会有多线程竞争,这是默认关闭偏向锁,过一段时间再打开

偏向锁启动延时

对象new出来时可能偏向锁就已经启动,此情况叫匿名偏向,偏向锁没有偏向任何一个线程。

这个图已经在文件夹里了,不贴浪费了

yinzi