原创

并发编程进阶三:深入理解“锁“机制

作者 | 浩说编程
来源 | 公众号:浩说编程
[  用"内容"服务读者 | 让"代码"服务大众  ]

通过前两篇的内容我们了解了并发的潜在问题,以及解决部分潜在问题的方法。

本篇我们继续探寻"如何解决并发的原子性问题?"

    通过之前的文章我们了解到,并发的原子性问题是由线程切换引起的。
    那么我们如果能够做到在需要的时候禁止线程切换,原子性问题就能有效解决。
    "锁"的概念由此产生,它的作用是保证我们的业务代码在同一时刻只能被一个线程所执行

图片

图片

    需要注意的是,"锁"和"受保护的资源"存在对应关系

    也就是说资源A的锁LOCK-A只能保护资源A,资源B的锁LOCK-B只能保护资源B,LOCK-A无法保护B,这点很重要。

 

02 | Java提供的"锁"技术:synchronized

    Java通过关键字synchronized来隐式的实现上边提到的"锁"机制,它用来修饰方法、代码块,其中的内容被叫做"临界区"。

    在使用synchronized之后,Java会隐式的在对应方法、代码块前后分别加入"加锁lock()"、"解锁unlock()"操作。

    这种隐式操作的好处是能够保证"加锁"和"解锁"一定是成对出现的,避免忘记某个操作而造成事故级别的线程等待BUG。

    修饰静态方法:

class Test {
synchronized static void method() {
// 临界区
}
}

    还记得上面提到的"锁"和"受保护的资源"的一一对应关系吗?当synchronized修饰静态方法时,受保护的资源是当前类的Class对象

    相当于:

class Test {
synchronized(Test.class) static void method() {
// 临界区
}
}

    修饰非静态方法时,受保护的资源是当前类的实例对象this

class Test {
synchronized void method() {
// 临界区
}
}

    相当于:

class Test {
synchronized(this) void method() {
// 临界区
}
}

    修饰代码块时,受保护的资源是传递的参数,这里是obj:

class Test {
// 修饰代码块
Object obj = new Object();
synchronized(obj){
// 临界区
}
}

    在了解了上面的内容之后,我们就可以使用synchronized来尝试解决第一篇中count+=1可能引发的原子性问题:

class Test {
int value = 0;
synchronized void addOne() {
value += 1;
}
}

    补充的Happens-Before原则

    还记得上一篇文章中提到的Happens-Before原则吗?
    写到这里,需要补充一条关于"锁"的Happens-Before原则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
    这个含义可以理解成前一个线程的解锁操作对后一个线程的加锁操作可见。
    也就是说前一个线程在临界区修改的共享变量(该操作在解锁之前),对后续进入临界区(该操作在加锁之后)的线程是可见的
    所以对于上面这个例子,在线程1执行完addOne方法之后,线程2在进入addOne方法时看到的value一定是1,可见性问题得以保证。

 

03 | 互斥锁

    我们为上面的代码例子增加一个方法,用来获取value:

class Test {
int value = 0;
synchronized void addOne() {
value += 1;
}
//获取value
int getValue(){
return value;
}
}

    试想一下,当某个线程在执行addOne方法时,其他线程同时在执行getValue方法。这个时候两个方法中的value值是否一直呢?

    按照现在的写法,当线程执行addOne方法时,由于getValue方法没有加锁,所以即便在addOne方法中value的值变为了1,对于getValue方法来说依然不可见,所以拿到的value不一定是1。
    想要解决这个问题,我们可以利用锁的互斥性去做,互斥性是指:对于同一个锁修饰的不同方法,在同一时刻只能执行一个
    于是代码可以这样改进,在getValue方法前使用synchronized:

class Test {
int value = 0;
synchronized void addOne() {
value += 1;
}
//获取value
synchronized int getValue(){
return value;
}
}

    这样一来,两个方法的锁都是this,所以并发的时候,如果addOne方法先拿到了this锁,那么在方法执行完并释放this锁之前,getValue方法就无法获取到this锁,也就形成了互斥关系,保证了value的可见性。


    混淆的互斥锁

    对于锁的互斥性,我们可能会产生混淆的情况,还是上面的例子,我们把addOne方法修改成static静态的:

class Test {
int value = 0;
synchronized static void addOne() {
value += 1;
}
//获取value
synchronized int getValue(){
return value;
}
}

    结合本篇所学,试想一下这两个方法还存在互斥关系吗?你可以回看一下上面提到的互斥锁的关键点:同一个锁

    由于addOne方法变成了静态,所以锁变成了Test.Class,而getValue方法的锁是this,锁不同了自然也就不存在互斥性了,就会引发最初的可见性问题,这点需要特别注意。

图片

    以上就是本篇关于"锁"的知识探索,总结一下本篇的大纲:"锁"的概念、Java提供的锁技术:synchronized、"锁"和“资源”的对应关系、互斥锁。

正文到此结束
本文目录