【03】Java并发编程学习笔记之——互斥锁(原子性问题解决方案)(下)

    在上一篇文章中,我们提到受保护资源和锁之间合理的关联关系应该是 N:1 的关系,也就是说可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源,并且结合文中示例,我们也重点强调了“不能用多把锁来保护一个资源”这个问题。而至于如何保护多个资源,我们今天就来聊聊。

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

    在现实世界里,球场的座位和电影院的座位就是没有关联关系的,这种场景非常容易解决,那就是球赛有球赛的门票,电影院有电影院的门票,各自管理各自的。
    同样这对应到编程领域,也很容易解决。例如,银行业务中有针对账户余额(余额是一种资源)的取款操作,也有针对账户密码(密码也是一种资源)的更改操作,我们可以为账户余额和账户密码分配不同的锁来解决并发问题,这个还是很简单的。
    账户类 Account 有两个成员变量,分别是账户余额 balance 和账户密码 password。取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance,我们创建一个 final 对象 balLock 作为锁(类比球赛门票);而更改密码 updatePassword() 和查看密码 getPassword() 操作会修改账户密码 password,我们创建一个 final 对象 pwLock 作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的,很简单。

class Account{

// 锁:保护张宇余额
private final Object baLock = new Object();
//账户余额
private  Integer balance;
//锁:保护账户密码
private final Object pwLock = new  Object();
//账户密码
private String password;

	vlid withdraw(Integer amt){
		synchronized(balLock){
		 if(this.balance > amt){
		 	this.balance  -= amt;
		 }
		}
	}
	Integer getBalance() {
		sychronized(balLock){
		return balance;
		}
	}
    void UpdatePassword(String pw){
       synchronized(pwLock){
       		this.password = pw;
       }
    }
    String getPassword(){
		sychronized(pwLock){
			this.password = pw;
		}
	}
}

当然,我们也可以用一把互斥锁,来保护多个资源,列入我们可以用this这一把锁来管理账户类里所有的资源:账户余额和用户密码。具体实现很简单,实力程序中所有的方法都增加同步关键字 sychronized 。

但是一把锁,有一个问题,就是性能太差,会导致,取款,查看余额,修改密码,查看密码,这四个操作都是串行的。而我们用两把锁,取款和修改密码都是可以并行的。用不同的锁对受保护资源进行竟喜欢管理,能够提升性能。这种锁,还有个名字,叫细粒度锁。

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

如果多个资源是相互关联关系的,那么这个问题,就有点复杂了 。例如银行里的转账操作,账户A 减少了100元,账户B增加了100元。这两个账户就是有关联关系的,那对于想转账这种有关联关系的操作,我们应该怎么去解决呢?先把这个问题,用代码来表示。我们声明了类账户Account 。

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

我们应该想到的是用 sychronized 来修饰代码

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

在这段代码中,临界区内有两个资源,分别是转出账户的余额, this.balance 和转入账户的 月 target.balance ,并且用的是一把锁 this。但是这个方案是存在问题的。因为 this这把锁,可以保护,自己的余额。但是不能保护别人的锁。
【03】Java并发编程学习笔记之——互斥锁(原子性问题解决方案)(下)_第1张图片下面我们具体分析一下,假设有A、B、C三个账户,余额都是200元,我们用两个线程,分别执行两个转账操作。A转给B100元。B转给C 100元。最后我们得到的结果,是;A :100元,B 200元,C:300 元。
我们假设,线程1 执行账户A转账户B的操作,线程2 执行B转账户C的操作。这两个线程,分别在两个Cpu上同事执行。那他们是互斥的吗?两个线程的都是获取的 this。但是两个线程的this,确实有不同的含义。第一个线程中,执行的是A,而第二个线程,this指的是B。所以这两个线程,可以同事进入临界区 transfer(). 同时进入临界区的结果,是什么呢?B账户里的钱,可能是 100 ,或者,200 ,或者 300。

【03】Java并发编程学习笔记之——互斥锁(原子性问题解决方案)(下)_第2张图片

使用锁的正确的姿势

在上一篇文章中,我们提到用通一把锁,来保护多个资源,也就是现实世界的“包场”。那在编程领域,应该怎么包场呢?很简单,只要我们的 锁能够覆盖,所有受保护的资源,就可以了。在上面的例子中,this是对象级别的锁,所以A对象和B对象都有自己的锁。如何让A对象和B对象共享一把锁?

稍微开动脑筋,你就会发现,其实方案还挺多的,比如可以让所有的对象都持有一个唯一性的对象。Acount时传入。防寒有了,完成代码,就简单了。示例代码如下,把 Account
默认的构造函数,变为 private,同事增加一个带 Object lock参数的构造函数,创建Account对象时候,传入相同的 lock。这样所有的Account对象,就会共享这个lock了。

class Account{
	private Object lock;
	private int balance;
	private Account();
	public Account(Object object){
		this.lock = object;
	}
	void transfer(Account target,int amt){
			sychronized(lock){
				if(this.balance > amt){
					this.balance -= amt;
					target.balance  +=amt;
				}
			}
	}
}

这个办法确实能解决问题,但是有点小瑕疵,它要求在创建 Account 对象的时候必须传入同一个对象,如果创建 Account 对象时,传入的 lock 不是同一个对象,那可就惨了,会出现锁自家门来保护他家资产的荒唐事。在真实的项目场景中,创建 Account 对象的代码很可能分散在多个工程中,传入共享的 lock 真的很难。
所以,上面的方案缺乏实践的可行性,我们需要更好的方案。还真有,就是用 **Account.class 作为共享的锁。Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。**使用 Account.class 作为共享的锁,我们就无需在创建 Account 对象时传入了,代码更简单。

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

下面这幅图很直观地展示了我们是如何使用共享的锁 Account.class 来保护不同对象的临界区的。【03】Java并发编程学习笔记之——互斥锁(原子性问题解决方案)(下)_第3张图片

关联关系如果用更具体、更专业的语言来描述的话,其实是一种“原子性”特征,在前面的文章中,我们提到的原子性,主要是面向 CPU 指令的,转账操作的原子性则是属于是面向高级语言的,不过它们本质上是一样的。

“原子性”的本质是什么?其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。例如,在 32 位的机器上写 long 型变量有中间状态(只写了 64 位中的 32 位),在银行转账的操作中也有中间状态(账户 A 减少了 100,账户 B 还没来得及发生变化)。所以解决原子性问题,是要保证中间状态对外不可见。

你可能感兴趣的:(java,OKR)