缓存会导致了可见性问题,编译优化会导致有序性问题。也就是说解决可见性和有序性问题的最直接的办法就是禁用缓存和编译优化。但是,如果只是简单的禁用了缓存和编译优化,那我们写的所谓的高并发程序的性能也就高不到哪去,合理的方案应该是按照需要禁用缓存和编译优化。
java互斥锁解决原子性问题
解决原子性最常用的方案就是锁,在java中,java提供的 synchronized 关键字就是锁的一种实现。锁模型如下:
synchronized可以用来修饰方法,也可以用来修饰代码块。
当修饰静态方法的时候,锁定的是当前的Class对象。当修饰非静态方法的时候,锁定的是当前实例this对象。
受保护资源和锁直接的合理关系应该是N:1的关系,也即可以用一把锁来保护多个资源。
保护没有关联关系的多个资源
以账户的余额(balance)和密码(password)为例,修改账户余额不会影响到密码,修改密码也不会影响到账户余额,这样就可以对余额(balance)和密码(password)分别设置不同的锁来提高程序的并发性。
保护有关联关系的多个资源
以银行转账为例,账户A减少100元,账户B增加100元,这两个账户就是有关联关系的。我们声明了个账户类:Account,该类有一个成员变量余额:balance,还有一个用于转账的方法:transfer(),然后怎么保证转账操作 transfer() 没有并发问题呢? 先以下面代码为例:
class Account {
private int balance;
// 转账
synchronized void transfer(Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
在这段代码中,synchronized包含的临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance,并且用的是一把锁 this,符合我们前面提到的,多个资源可以用一把锁来保护,这看上去完全正确呀。真的是这样吗?可惜,这个方案仅仅是看似正确,为什么呢?
问题就出在 this 这把锁上,this 这把锁可以保护自己的余额 this.balance,却保护不了别人的余额 target.balance,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。
解决方法:使用 Account.class 作为共享的锁。
class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
synchronized(Account.class) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
上面的这个解决方法虽然不存在并发问题,但所有的账户转账操作都是串行的。对于转入和转出操作因为涉及到两个资源,我们可以用两把锁,转出账户一把锁,转入账户一把锁,在transfer方法内,我们首先尝试锁定转出账户this,然后尝试锁定转入账户target,只有两者同时都成功时,才执行转账操作。
代码如下:
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;
}
}
}
}
}
相对于用Account.class作为互斥锁,锁定的范围太大,上面的锁定两个账户范围小多了,这样的锁叫细粒度锁。使用细粒度锁可以提高并行度,是性能优化的一个重要手段。但是,使用细粒度锁是有代价的,这个代价就是可能会导致死锁。
java管程解决互斥问题
Java1.5之前仅仅提供wait()、notify()、notifyAll(),synchronized和wait()、notify()、notifyAll()三个方法都是管程的实现。管程解决互斥问题的思路很简单,就是将共享变量及其对共享变量的操作统一封装起来。在下图中,管程 X 将共享变量 queue 这个队列和相关的操作入队 enq()、出队 deq() 都封装起来了;线程 A 和线程 B 如果想访问共享变量 queue,只能通过调用管程提供的 enq()、deq() 方法来实现;enq()、deq() 保证互斥性,只允许一个线程进入管程。
在管程模型里,共享变量和对共享变量的操作是被封装起来的,图中最外层的框就代表封装的意思。框的上面只有一个入口,并且在入口旁边还有一个入口等待队列。当多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待。
管程里还引入了条件变量的概念,而且每个条件变量都对应有一个等待队列,如下图,条件变量 A 和条件变量 B 分别都有自己的等待队列。
那条件变量和等待队列的作用是什么呢?其实就是解决线程同步问题。你也可以结合上面提到的入队出队例子加深一下理解。
假设有个线程 T1 执行出队操作,不过需要注意的是执行出队操作,有个前提条件,就是队列不能是空的,而队列不空这个前提条件就是管程里的条件变量。 如果线程 T1 进入管程后恰好发现队列是空的,那怎么办呢?等待啊,去哪里等呢?就去条件变量对应的等待队列里面等。此时线程 T1 就去“队列不空”这个条件变量的等待队列中等待。
再假设之后另外一个线程 T2 执行入队操作,入队操作执行成功之后,“队列不空”这个条件对于线程 T1 来说已经满足了,此时线程 T2 要通知 T1,告诉它需要的条件已经满足了。当线程 T1 得到通知后,会从等待队列里面出来,但是出来之后不是马上执行,而是重新进入到入口等待队列里面。