互斥锁的目的:解决原子性问题,即“资源在同一时刻只能被一个线程占有”;根本方法就是禁止线程切换(单核场景)或者同一时刻线程互斥(多核场景)
临界区: 需要互斥执行的代码称为“临界区”,进入/离开临界区,需要加锁/解锁操作。
锁的对象: 即需要被放入临界区的对象,主要逻辑如下:
注意:
锁住的对象必须明确范围: 如果范围太大可能导致锁不生效(直接被JVM逃逸分析优化掉了),或者“强行串行”导致性能下降 如果范围太小也可能导致锁不生效,或死锁 锁住的对象不正确,可能导致锁住了其他对象
二、synchronized关键字class X {
// 修饰非静态方法,锁对象是this
//相当于synchronized(this) static void bar() {}
synchronized void foo() {
// 临界区
}
// 修饰静态方法,锁对象是X.class(因为static方法属于类)
//相当于synchronized(X.class) static void bar() {}
synchronized static void bar() {
// 临界区
}
// 修饰代码块,锁对象是obj(obj也可以是this,也可以是X.class)
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
}
优势:使用简单,加锁/解锁有JVM成对提供,可以防止忘记解锁(可能会导致其他线程持续等待)
劣势:
class SafeCalc {
static long value = 0L;
synchronized long get() { //锁住的是this
return value;
}
synchronized void addOne() { //锁住的是this
value += 1;
}
}
以上示例中的get() 方法和 addOne() 方法都需要访问 value 这个受保护的资源,这个资源用 this 这把锁来保护。线程要进入临界区 get() 和 addOne(),必须先获得 this 这把锁,这样 get() 和 addOne() 也是互斥的。线程模型如下图所示:
思考以下实现,是否能够保证可见性和原子性
class SafeCalc {
long value = 0L;
long get() {
synchronized (new Object()) {
return value;
}
}
void addOne() {
synchronized (new Object()) {
value += 1;
}
}
}
点评:
1、以上实现方式,get和addOne操作均会new出一个obj对象作为锁,但是两个obj并不是同一个
加锁进入的不是同一个对象的临界区,无法起到原子性的作用。
2、加锁的本质:在对象头中加入当前线程的ID信息,标记被那个线程持有
解锁操作:当线程释放一个锁时会强制性的将工作内存中之前所有的写操作都刷新到主内存中去,而获取一个锁则会强制性的加载可访问到的值到线程工作内存中来。
虽然锁操作只对同步方法和同步代码块这一块起到作用,但是影响的却是线程执行操作所使用的所有字段。
3、Java的对象头和对象组成详解
4、JVM逃逸分析会把这个synchronized去除(锁消除)
锁消除:如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
有一个老哥的解释很生动:“多把锁保护同一个资源,就像一个厕所坑位,有N多门可以进去,没有丝毫保护效果,管理员一看,还不如把门都撤了,弄成开放式(编译器代码优化)”
5、JVM 锁消除/锁粗化/锁升级/锁降级
受保护资源和锁之间的关联关系非常重要,一个合理的关系是:受保护资源和锁之间的关联关系是 N:1 的关系,首先见改动的如下的代码:
class SafeCalc {
static long value = 0L;
synchronized long get() { //锁住的是this
return value;
}
synchronized static void addOne() { //锁住的是SafeCalc.class
value += 1;
}
}
改动后的代码是用两个锁保护一个资源。这个受保护的资源就是静态变量 value,两个锁分别是 this 和 SafeCalc.class。我们可以用下面这幅图来形象描述这个关系。由于临界区 get() 和 addOne() 是用两个锁保护的,因此这两个临界区没有互斥关系,临界区 addOne() 对 value 的修改对临界区 get() 也没有可见性保证,这就导致并发问题了。
3.1 保护没有关联的资源
银行业务中有针对账户余额(余额是一种资源)的取款操作,也有针对账户密码(密码也是一种资源)的更改操作,我们可以为账户余额和账户密码分配不同的锁来解决并发问题。
到受保护资源和锁之间合理的关联关系应该是 N:1 的关系,即用一把锁来保护多个资源
实际场景中,这多个不同的资源之间有时候是没有关系的,处理场景有以下几种:
1、当然为了符合 “N:1” 的原则,可以通过持有this这一把锁来实现(所有的非static方法上用synchronized修饰)
但是因为查看密码和查看余额 这两个操作之间没有必然关系(实际还得结合具体业务场景),这样的做法实际上将这两类操作统统锁死了,强行变成串行化,实际运行过程中可能会影响性能
2、所以采用多个锁来保护各自的关心自资源:com.slek.app.springbootartifact.cocurrence.pojo.Account
用不同的锁对受保护资源进行精细化管理,能够提升性能。这种锁还有个名字,叫"细粒度锁"。示例中模板代码如下:
class Account {
private final Object balLock = new Object(); // 锁1:保护账户余额
private Integer balance; // 账户余额
private final Object pwLock = new Object(); // 锁2:保护账户密码
private String password; // 账户密码
void withdraw(Integer amt) { // 取款
synchronized (this.balLock) {
if (this.balance > amt) {
this.balance -= amt;
}
}
}
Integer getBalance() { // 查看余额
synchronized (this.balLock) {
return balance;
}
}
void updatePassword(String pw) { // 更改密码
synchronized (this.pwLock) {
this.password = pw;
}
}
String getPassword() { // 查看密码
synchronized (this.pwLock) {
return password;
}
}
}
3.2 保护有关联的资源
如果受保护的各个资源之间有关系,处理场景略复杂,具体场景可见下:
例如两个不同账户AccountA和AccountB之间的转账操作:从AccountA中取出100元,再向AccountB中转入100元。
AccountA的转出和AccountB的转入是一个原子操作,两者的余额balance需要被同一把锁来保护;否则多个账户同一时刻向同一个账户B转账,读取并写入到了相同的账户B余额balance,那转账的结果就会错误。



