前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >并发编程进阶三:深入理解"锁"机制

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

作者头像
浩说编程
发布2021-08-17 17:37:36
1890
发布2021-08-17 17:37:36
举报
文章被收录于专栏:Java经验之谈Java经验之谈

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

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

读者的收获

1、"锁"的概念

2、Java提供的锁技术:synchronized

3、"锁"和“资源”的对应关系

4、互斥锁

往期回顾

并发编程进阶一:从“并发引发的潜在问题”开始

并发编程进阶二:搞定可见性、有序性问题,用'它'就够了!!!

1

"锁"的概念

通过之前的文章我们了解到,并发的原子性问题是由线程切换引起的。

那么我们如果能够做到在需要的时候禁止线程切换,原子性问题就能有效解决。

"锁"的概念由此产生,它的作用是保证我们的业务代码在同一时刻只能被一个线程所执行

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

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

2

Java提供的锁技术:synchronized

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

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

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

修饰静态方法:

代码语言:javascript
复制
class Test {
  synchronized static void method() {
    // 临界区
  }
}

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

相当于:

代码语言:javascript
复制
class Test {
  synchronized(Test.class) static void method() {
    // 临界区
  }
}

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

代码语言:javascript
复制
class Test {
  synchronized void method() {
    // 临界区
  }
}

相当于:

代码语言:javascript
复制
class Test {
  synchronized(this) void method() {
    // 临界区
  }
}

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

代码语言:javascript
复制

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

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

代码语言:javascript
复制
class Test {
  int value = 0;
  synchronized void addOne() {
    value += 1;
  }
}

补充的Happens-Before原则

还记得上一篇文章中提到的Happens-Before原则吗?

写到这里,需要补充一条关于"锁"的Happens-Before原则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。

这个含义可以理解成前一个线程的解锁操作对后一个线程的加锁操作可见。

也就是说前一个线程在临界区修改的共享变量(该操作在解锁之前),对后续进入临界区(该操作在加锁之后)的线程是可见的

所以对于上面这个例子,在线程1执行完addOne方法之后,线程2在进入addOne方法时看到的value一定是1,可见性问题得以保证。

3

互斥锁

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

代码语言:javascript
复制
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:

代码语言:javascript
复制
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静态的:

代码语言:javascript
复制
class Test {
  int value = 0;
  synchronized static void addOne() {
    value += 1;
  }
  //获取value
  synchronized int getValue(){
    return value;
  }
}

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

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

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

高调分享 惊艳全场

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-07-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 浩说编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档