互斥锁

       一个或者多个操作在CPU执行的过程中不被中断的特性,称为“原子性”。注意,原子性是面向cpu指令级别操作的,而不是面向高级语言操作。

解决原子性问题

       带来原子性问题的是线程切换,如果能够禁用线程切换,那就能够解决原子性问题。而操作系统做线程切换是依赖CPU中断的,所以禁止CPU发生中断就能够禁止线程切换。这个方案,在单核CPU时代的确是可行的,而且也有很多应用案例,但是并不适合多核CPU场景。
       在单核CPU场景下,同一时刻只有一个线程执行,禁止CPU中断,意味着操作系统不会重新调度线程,也就是禁止了线程切换,获得CPU使用权的线程就可以不间断地执行,所以两次写操作一定是:要么都被执行,要么都没有被执行,具有原子性。但是在多核场景下,同一时刻,有可能有两个线程同时在执行,一个线程执行在CPU-1上,一个线程执行在CPU-2上,此时禁止CPU中断,只能保证CPU上的线程连续执行,并不能保证同一时刻只有一个线程执行,如果这两个线程同时写long型变量高32位的话,那就有可能出现bug了。
       “同一时刻只有一个线程执行”这个条件非常重要,称之为互斥。如果能够保证对共享变量的修改是互斥的,那么无论是单核CPU还是多核CPU,就能保证原子性了。

简易锁模型

       说到互斥,一定会想到“锁”这个方案,如图:


简易锁模型

      一段需要互斥执行的代码称为临界区。线程在进入临界区之前,首先尝试加锁lock(),如果成功,则进入临界区,此时称这个线程持有锁;否则,就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁unlock()。但是,存在两个极容易忽视的问题:锁的是什么?保护的又是什么?

改进后的锁模型

       锁和锁要保护的资源是有对应关系的,上面的模型没有体现出它们之间的对应关系,故需要完善,如图:


改进后的锁模型

       首先,要把临界区要保护的资源标注出来,如图中临界区里增加了一个元素:受保护的资源R;其次,要保护资源R就得为它创建一把锁LR;最后,针对这把锁LR,需要在进出临界区时添加上加锁操作和解锁操作。另外,在锁LR和受保护资源之间的关联关系,如图中的连线,这个关联关系非常重要。

Java语言提供的锁技术:synchronized

       锁是一种通用的技术方案,Java语言提供的synchronized关键字,就是锁的一种实现。synchronized关键字可以用来修饰方法,也可以用来修饰代码块,但是他没有显示的加锁lock()和解锁unlock()操作,这两个操作是被Java默默加上的,Java编译器会在synchronized修饰的方法或者代码块前后自动加上加锁lock()和解锁unlock(),这样做的好处就是加锁lock()和解锁unlock()一定是成对出现的,毕竟忘记解锁unlock()可是一个致命的bug。
       Java中有一条隐式规则:当synchronized修饰静态方法的时候,锁定的是当前类的Class对象;当synchronized修饰非静态方式的时候,锁定的是当前实例对象this;当synchronized修饰代码块的时候,锁定的是其后括号的对象。

锁和受保护资源的关系

       一个合理的受保护资源和锁之间的关联关系是N:1。例如如下的代码:

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

       如果仔细观察,就会发现改动后的代码是用两个锁保护一个资源。这个受保护的资源就是静态变量value,两个锁分别是this和SafeCalc.class。画一幅图来描述锁和受保护资源的关系:


两把锁保护一个资源

       从图中可知,由于临界区get()和addOne()是用两个锁保护的,因此这两个临界区没有互斥关系,临界区addOne()对value的修改对临界区get()也没有可见性,这就导致并发问题了。所以在并发中,一定要注意受保护资源和锁之间的关联关系,是N:1的关系,反之,会导致并发问题。同时,在保护多个资源的时候,首先要区分这些资源是否存在关联关系。

保护没有关联关系的多个资源

       对于那些没有关联关系的多个资源,用不同的锁来保护,解决并发问题。例如:银行业务中,有针对账户余额的取款操作,也有针对账户密码的更改操作,其中余额(balance)是一种资源,账户密码(password)也是一种资源,为它们分配不同的资源即可解决并发问题。

class Accout {
  //保护余额的锁
  private final Object balanceLock = new Object;
  //账户余额
  private Integer balance;
  //保护账户密码的锁
  private final Object passwordLock = new Object;
  //账户密码
  private String password;
  //取款
  void withdraw(Integer amt) {
    synchronized(balanceLock) {
      if(this.balance > amt) {
        this.balance -= amt;
      }
    }
  }
  //查看余额
  Integer getBalance() {
    synchronized(balanceLock) {
      return balance;
    }
  }
  //更改密码
  void updatePassword(String password) {
    synchronized(passwordLock) {
      this.password = password;
    }
  }
  //查看密码
  String getPassword() {
    synchronized(passwordLock) {
      return password;
    }
  }
}

       其实,可以用一把锁来保护多个资源,例如用this这把锁来管理账户类所有的资源,但是用一把锁,性能会太差,所有的操作都是串行的。故用不同的锁对没有关联关系的资源进行精细化管理,能够提升性能,称之为细粒度锁。

保护有关联关系的多个资源

       银行的转账业务中,涉及到两个存在关联关系的账户:转出账户A和转入账户B。要解决像这样多个资源是有关联关系的并发问题,有点复杂。先把问题代码化,代码如下:

class Account {
  private int balance;
  //转账
  void transfer(Account target, int amt) {
    if(this.balance > amt) {
      this.balance -= amt;
      target.balance += amt;
    }
  }
}

       为了解决并发问题,可能会想到用synchronized关键字来修饰transfer()方法,代码如下:

class Account {
  private int balance;
  //转账
  synchronized void transfer(Account target, int amt) {
    if(this.balance > amt) {
      this,balance -= amt;
      target.balance += amt;
    }
  }
}

       从上述代码中,在临界区内有两个资源,分别是转出账户的余额this.balance和转入账户的余额target.balance,用的是this这把锁,虽然满足了多个资源可以用一把锁来保护,但是还是有问题,问题就出在this这把锁上,this这把锁可以保护自己的余额this.balance,但是保护不了别人的余额target.balance。看示意图:


用锁this保护this.balance和target.balance的示意图

       具体分析一下,假设有A、B、C三个账户,余额都是200元,用两个线程分别执行这两个转账操作:账户A转给账户B100元,账户B转给账户C100元,期望的结果是A的余额是100元,B的余额是200元,C的余额是300元。假设线程1执行账户A转账给账户B的操作,线程2执行账户B转账给账户C的操作。这个线程分别在两颗CPU上同时执行,但是它们不互斥,因为线程1锁定的是账户A的实例(A.this),而线程2锁定的是账户B的实例(B.this),所以这两个线程可以同时进入临界区transfet(),所以有可能线程1和线程2都会读到账户B的余额是为200,导致最终账户B的余额可能是300(线程1后于线程2写B.balance,线程2写的B.balance值被线程1覆盖),也可能是100(线程1先于线程2写B.balance,线程1写的B.balance值被线程2覆盖),就是不可能是200。

使用锁的正确姿势

       锁和受保护的资源的关联关系是N:1,即可以用同一把锁来保护多个资源,代码上,只要使用的锁能够覆盖所有受保护资源就可以实现。实现的方案还是挺多的:

  • 让所有的对象都持有一个唯一性的对象,但是这个方案缺乏实践的可行性,因为在创建对象的时候,必须要传入同一个锁对象,否则就会出现并发的问题。
  • 使用类的class对象,类的class对象是所有类的实例共享的,而且class对象是Java虚拟机在加载类的时候创建的,不要担心其唯一性。

总结

       对于如何保护多个资源,关键是要分析多个资源之间的关系。如果资源之间没有关系,每个资源对应一把锁即可。如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。
       原子性,其实不是不可分割,不可分割只是外在表现,其本质就是多个资源间有一致性的要求,操作的中间状态对外不可见。所以要解决原子性问题,是要保证中间状态对外不可见。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容