前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >并发编程原子性问题

并发编程原子性问题

作者头像
小土豆Yuki
发布2020-11-03 11:35:11
6390
发布2020-11-03 11:35:11
举报
文章被收录于专栏:洁癖是一只狗洁癖是一只狗

原子性问题到底如何解决呢

原子性的问题是因为线程切换,如果能够禁用线程那不就可以解决问题了吗,而操作系统做线程切换是依赖CPU中断,所以禁止CPU发生中断就可以禁止线程切换

在早期单核CPU时代,这个方案是可行的,但是在多核CPU场景就是不适应的,比如在32CPU上执行long变量的写操作说明这个问题,long类型变量是64位,在32位CPU上会被拆成两次写操作如下图

在单核CPU,同一时刻只有一个线程执行,当我们禁止CPU中断,就可以避免线程切换,这是CPU使用权的线程就可以不断的执行,做一两次写操作就一定要么执行成功,要么执行失败,保证了原子性

在多核CPU上,并不能保证同一时刻只有一个线程,比如有两个线程分别在不同的CPU上执行,禁止CPU中断,只能保证CPU上的线程连续执行,但是如何此时两个线程同时操作高32的值,就会出现bug.

同一时刻只有一个线程,称之为互斥,只要保证了对共享变量的互斥,不管在单核还是在多核CPU上都能保证原子性

简单锁模型

一般我理解的锁的样子如下图

我们把互斥执行的代码成为临时区,线程在进入临时去之前,首先尝试加锁,如果成功,则进入临时去,此时我们就有对这个线程持有锁,否则等待,知道持有锁的线程释放锁,持有锁的线程执行完临界区的代码后,执行解锁unlock看起很完美,但是我们忽略了两点

  1. 我们锁的是什么
  2. 我们保护的又是什么

改进后的锁模型

在现实生活中,你用你家的锁,锁住你家的门,我用我家的锁,锁住我家的门,在并发编程世界里,也是一样的,这个关系正如上图一样

首先,我们要把受保护资源R标注出来,如图上的受保护资源R,其次我们要保护资源R就得为它创建一把锁LR,最后针对这个锁LR,我们还需在进出临界区添加锁,和解锁操作,同时在锁LR和受保护的资源R之间有一条关联,正如上面的那条线,如果我们用自家的锁,去锁别家的资源,就可能导致bug出现。

锁技术:synchronized

synchronized关键字就是一种锁的实现,他可以修饰方法,也可以修饰代码块,如下图

代码语言:javascript
复制

class X {
// 修饰非静态方法
synchronized void foo() {
// 临界区
  }
// 修饰静态方法
synchronized static void bar() {
// 临界区
  }
// 修饰代码块
  Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
    }
  }
}

我们发现好像没有加锁和解锁的操作,其实synchronized内部实现了加解锁的操作,这样锁是为了避免我们遗忘加锁操作,否则会出现致命的bug

同时上面加锁的对象有两条默认规则如下

  • 当修饰静态方法的时候,锁定的是当前类的class对象
  • 当修饰非静态方法的时候,锁定的就是当前对象this

上面代码可以改成下面

代码语言:javascript
复制

class X {
// 修饰静态方法
synchronized(X.class) static void bar() {
// 临界区
  }
}
代码语言:javascript
复制

class X {
// 修饰非静态方法
synchronized(this) void foo() {
// 临界区
  }
}

使用synchronized解决问题

如下面代码我们有类safecalc,一个属性value,两个方法get和addOne

代码语言:javascript
复制

class SafeCalc {
long value = 0L;
long get() {
return value;
  }
synchronized void addOne() {
value += 1;
  }
}

其中addOne是被syncronized修饰,而get没有修饰,都是对value变量的有操作,那么有没有问题呢

先拿addOne方法解释,首先使用了syncronized修饰后,就可以保证无论在单核还是多核,都可以保证原子操作,且保证了线程的可见性

代码语言:javascript
复制
管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁

按照上面规则如果多个线程使用addOne方法,可见性可以保证,也就说有1000个线程执行addOne方法,最终的结果就是1000,

看上去还是很完美,但是我们忘记了get方法,因为管程中锁的规则是只能保证后续操作对这个锁的加锁的可见性,而get并没有加锁操作,因此并不能保证可见性,这个问题也很简单,只要加上synchronized,就可以解决

代码语言:javascript
复制

class SafeCalc {
long value = 0L;
synchronized long get() {
return value;
  }
synchronized void addOne() {
value += 1;
  }
}

上面代码get和addOne方法都需要访问value这个受保护的资源,这个资源使用this对象这把锁保护,线程进入临界区get和addone,必须获取this这把锁,因此get和addone互斥,这样就可以避免并发问题

这里就像球场的门票管理一样,一个座位只能有一个人使用,这个座位就是受保护的资源,而入场就是Java类中的方法,而门票就是保护资源的锁,java检票就由synchronized执行

锁和受保护资源的关系

受保护资源和锁的关系是N:1的关系一把锁可以锁多个资源,对应的现实中就是球赛的作为让你包场了

我们把上面的例子修改一下,看看有没有并发问题

代码语言:javascript
复制

class SafeCalc {
static long value = 0L;
synchronized long get() {
return value;
  }
synchronized static void addOne() {
value += 1;
  }
}

我们发现get和addone分别用两把不同的锁,get使用的this,而addOne使用的safecalc.Class,此时由于是不同的两把锁,而临界区没有互斥关系,因此两个方法对临界区的value就没有办法保证可见性,就会引发并发问题,如下图

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

本文分享自 洁癖是一只狗 微信公众号,前往查看

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

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

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