The Stars ...My Destination

adamxx

天下事,法无定法,然后知非法法之
世间人,尤了未了,何妨以不了了之

导航

多线程未同步可能导致的问题及其解决方案

Posted on 2007-03-31 20:38  adamxx  阅读(1003)  评论(0编辑  收藏  举报

这是一个来自java的例子,我觉得很典型,就放上来谈谈.
下面的示例来自 "Java核心技术 第二卷 高级特性"

在下面的测试程序中,我们模拟一个拥有一定数量账户的银行.我们随机的产生把钱在不同账号之间转移的交易.每个账号都有一个线程,在每笔交易中,都会从线程所服务的账户中随机取出一定数额的金钱到另一个随机账户中.

我们有一个Bank类,它有一个transfer方法,这个方法将一定数额的钱从一个账户转移到另外一个账户.如果源账户没有足够的金额,该方法将直接返回.

public void transfer(int from,int to,double amount){
        
if(accounts[from] < amount) return;
        System.
out.print(Thread.currentThread());
        accounts[from] 
-= amount;
        System.
out.printf("%10.2f from %d to %d",amount,from,to);
        accounts[to] 
+= amount;
        System.
out.printf("Total Balance: %10.2f%n", getTotalBalance());
    }

下面是TransferRunnable类的代码.他的run方法不断的从一个固定账户中取出钱.在每次迭代中,run方法随机挑选一个目标账户和一个随机账户,调用Bank对象的transder方法,然后thread.sleep();
 public void run(){
        
try{
            
while(true){
                
int toAccount = (int)(bank.size() * Math.random());
                
double amount = maxAmount * Math.random();
                bank.transfer(fromAccount,toAccount,amount);
                Thread.sleep((
int)(DELAY * Math.random()));
            }

        }

        
catch(InterruptedException e){}
    }


在这个模拟程序运行时,我们不知道在某个时间某个银行账户里有多少钱.但我们知道所有账户中的金额总量保持不变,因为我们所做的只是把钱在账户之间转移.

下面是典型的输出:
...
Thread[Thread-29,5,main]    356.69 from 29 to 39Total Balance:  98847.71
Thread[Thread-12,5,main]    833.65 from 12 to 89Total Balance:  99690.69
Thread[Thread-0,5,main]    809.40 from 0 to 60Total Balance:  99774.88
Thread[Thread-80,5,main]    436.67 from 80 to 57Total Balance:  99206.67
...

就想你看到的那样,出现了错误.金额总量发生了细微的变化.

这个问题是在多个线程试图同时更新账户时出现的.假设两个线程同时执行这条指令:
accounts[to] += amount;
问题在于他不是原子操作.指令可能会以下面这种方式执行:
1)将account[to]载入寄存器.
2)增加amount.
3)将结果写回accounts[to].
        现在,假设第一个线程执行到了第一步和第二步,然后被中断了.而此时第二个线程被唤醒并更新了account数组中的同一项.接着第一个线程被唤醒并完成了第三步.
        这样,第二个线程所做的更新就被抹去了.结果导致总金额不再正确.

那么这个问题应该怎么解决?

从jdk 5.0开始,有两种机制来保护代码块不受并行访问的干扰.
1)使用ReentraltLock类,
         用ReentraltLock保护代码块的基本结构如下
myLock.lock();
try{
   critical section
}
finally{
   myLock.unlock();
}
这种结构保证在任何时刻只能有一个线程能够进入临界区.一旦一个线程锁住了锁对象,其他任何线程都无法通过lock语句.当其他线程调用lock时,他们会被被阻塞,直到第一个线程释放锁对象.
         让我们使用锁对象来保护Bank类的transfer方法.
修改Bank类

 private Lock bankLock = new ReentrantLock();
    
public void transfer(int from,int to,double amount){
        bankLock.
lock();
        
try{
        
if(accounts[from] < amount) return;
            System.
out.print(Thread.currentThread());
            accounts[from] 
-= amount;
            System.
out.printf("%10.2f from %d to %d",amount,from,to);
            accounts[to] 
+= amount;
            System.
out.printf("Total Balance: %10.2f%n", getTotalBalance());
        }

        
finally{
            bankLock.unlock();
        }

    }

再次运行程序,现在可以不会出现问题了.
另外还需要注意的是:
必须小心处理,以防临界区中的代码因为抛出了一个异常而掉出临界区.如果一个异常在临界区代码结束前抛出,那么finally子句就会释放锁,但这会使对象处在某种受损状态.

2)使用Synchronized关键字
         从JDK 1.0开始java中每个对象都有一个隐式的锁.如果一个方法又synchronized关键字声明,那么对象的锁将保护整个方法,也就是说要调用这个方法,线程必须先获得对象的锁.
public synchronized void  method(){
         method body
}
这和刚才使用lock的情况等价.

如果还需要更多了解,请参见 Java核心技术-第二卷高级特性-多线程一章

关于.net上的解决方案请转到 .NET中多线程的同步资源访问