JAVA如何解决原子性问题-锁:资源

上篇文章我们讲解了可见性和有序性在JAVA中如何解决。
那原子性问题如何解决呢?

导致原子性的原因是线程切换,当一个线程对共享变量进行读写操作时,没有执行完,切换到另外一个线程对该共享变量进行读写。比如下面经典的count++问题。
在这里插入图片描述
那么我们只要禁止线程切换不就解决这个问题了,而操作系统的线程切换依赖CPU中断的,所以禁止CPU发生中断就可以禁止线程切换。
在单核CPU下禁止中断就可以禁止线程切换就保证原子性了,但是现在大多都是多核CPU,同一时刻,两个线程可能同时在两个CPU中执行,所以禁止中断就起不到作用了。

其实我们只要保证同一时刻只有一个线程执行就可以了,我们叫做互斥,即,锁。
在这里插入图片描述
如上图,一个线程要想读写受保护资源,必须获取锁LR加锁,这样其他线程就无法获得锁不能访问受保护资源,当线程解锁释放锁对象,其他线程获得锁才可以读写受保护资源。

既然这里说到锁和资源,那有没有考虑过锁和资源的数量关系?
锁和资源的关系时1:N(N个资源没有关系),即,一把锁可以锁多个资源,但是一个资源不能用多把锁控制,否则不同的cpu拿到不同的锁就可以同时访问资源了。这里锁其实可以理解为令牌,拿到令牌才可以访问资源。

如果出现两个有关联的资源,就需要一把范围包括两个资源的锁。
就比如转账的情况,

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

一个账户想要给其他人转账,需要获取synchronezed就是对象this的锁,才能给其他人转账,看起来没问题,但是问题出现在this,this这把锁可以访问自己的余额,但是不能访问其他的账户的余额。
比如,账户A,B,C的余额分别都是200,CPU1中的线程1执行账户A拿到自己的锁给账户B转账100(账户A的balance=200-100=100写入,账户B的blance=200+100=300写入),同时CPU2中的线程2执行账户B也可以拿到自己的锁给账户C转账100(账户B的blance=200-100=100写入,账户C的blance=200+100=300写入)。
若线程1先写入,线程2后写入,结果账户A=100,账户先B=300,后B=100覆盖,B=100,账户C=300,少了100!
若线程2先写入,线程1后写入,结果账户A=100,账户先B=100,后B=300覆盖,B=300,账户C=300,多了100!
在这里插入图片描述
那么如何正确使用锁呢,就是需要一把锁可以同时覆盖两个资源,使访问这两个资源需要拿一把更大的锁。
比如

  // 转账
  void transfer(Account target, int amt){
    // 此处检查所有对象共享的锁
    synchronized(lock) {//这里锁的是class对象,程序运行时,jvm中只有一份
      if (this.balance > amt) {
        this.balance -= amt;
        target.balance += amt;
      }
}

但是上面使用Account.class作为锁,这样就使所有的账户转账操作都是同步了,非常不好!
其实我们可以用this账户和转账目标对象两个对象作为锁,只有同时拿到两个方法才可以转账。

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

其实你也发现了,这里就会发生死锁问题,一个线程拿到this,另一个线程拿到target,然后一致等待拿到对方的锁,就会发生死锁。
解决死锁问题,

  1. 其实你可以使用ReentranLock中的tryLock尝试获得锁,若没有获得锁,并不会进入阻塞状态,而是直接返回释放锁,或者支持超时,在一定时间内获得锁,拿不到锁,返回,释放锁。这样的话,注意活锁问题。
  2. 其实也可以一次性同时获得两个锁对象,(要么获得两个锁,要么一个都不获得。)
  // 转账
  void transfer(Account target, int amt){
    // 一次性申请转出账户和转入账户,直到成功
    while(!actr.apply(this, target))try{
      // 锁定转出账户
      synchronized(this){              
        // 锁定转入账户
        synchronized(target){           
          if (this.balance > amt){
            this.balance -= amt;
            target.balance += amt;
          }

如果两个锁对象不能同时获得,就采用while死循环等待,太消耗CPU了。
其实可以使用synchronized配套的wait(),notify(),notifyall()实现唤醒等待机制。

class Allocator {
  private List<Object> als;
  // 一次性申请所有资源
  synchronized void apply(
    Object from, Object to){
    // 经典写法
    while(als.contains(from) ||
         als.contains(to)){
      try{
        wait();
      }catch(Exception e){
      }   
    } 
    als.add(from);
    als.add(to);  
  }
  // 归还资源
  synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
    notifyAll();
  }
}

尽量使用notifyall(),因为notifyall唤醒所有的然后选择一个,而notify只是随机唤醒一个。所以notify有可能的某个线程永远不会被唤醒。

注意:wait()和notify()方法必须在synchronized方法中,同时wait()方法必须必须用while循环包括。当一个线程获得锁进入方法,其他线程想要进入方法,就存储在等待队列1中,当拿到锁,进入方法中,条件不满足,wait,进入等待队列2,当notify唤醒时,会从等待队列2中唤醒一个线程,从wait()这一行往下继续执行,所以如果不加while循环重新判断条件是否满足,有可能期间条件并不满足,但是却执行了!

在这里插入图片描述

到此就结束了,如果你对上面的synchroned不太明白,或者对其感兴趣,可以看管程模型synchronized原理以及synchronized的实现

参考:极客时间
更多:邓新

发布了34 篇原创文章 · 获赞 0 · 访问量 1089

猜你喜欢

转载自blog.csdn.net/qq_42634696/article/details/104910605