关于线程同步,网上也有很多资料,不过不同的人理解也不大一样,最近在研究这个问题的时候回想起大学课本上的一个经典模型,即银行存取款模型,通过这个模型,我个人感觉解释起来还是比较清楚的。本文结合自己的思考对该模型进行一个简单的模拟,阐述一下我对线程同步的理解。
接下来使用java对该问题进行模拟。在研究这个问题时会忽略掉现实系统中的很多其他属性,通过一个最简单的余额问题来看线程同步,这里首先创建三个类。
1.卡类,同时卡类提供三个方法,获取余额、存款以及取款。
public class Card { /*余额初始化*/ private double balance; public Card(double balance){ this.balance = balance; } /*获取余额方法*/ public double Get_balance(){ return this.balance; } /*存款方法*/ public void deposit(double count) throws InterruptedException{ System.out.println("存钱线程:存入金额=" + count); double now = balance + count; balance = now; System.out.println("存钱线程:当前金额=" + balance); } /*取款方法*/ public void withdraw(double count) throws InterruptedException{ System.out.println("取钱线程:取出金额=" + count); double now = balance - count; balance = now; System.out.println("取钱线程:当前金额=" + balance); } }然后是两个线程类,用于模拟并发操作所引入的余额问题。
2.存款线程类,存入金额100。
public class DepositThread extends Thread{ private Card card; public DepositThread(Card card){ this.card = card; } @Override public void run(){ try { card.deposit(100); } catch(Exception e){System.out.println(e.toString());} } }3.取款线程类,取出金额50。
public class WithdrawThread extends Thread{ private Card card; public WithdrawThread(Card card){ this.card = card; } @Override public void run(){ try { card.withdraw(50); } catch(Exception e){ System.out.println(e.toString()); } } }现在先进行一个测试,让存款线程先进行存钱操作,然后取款线程进行取款,最后验证余额与逻辑是否符合。
测试代码如下:
public class CardTest{ public static void main(String[] args) throws InterruptedException{ Card card = new Card(100); System.out.println("操作前余额:" + card.Get_balance()); DepositThread depositThread = new DepositThread(card); WithdrawThread withdrawThread = new WithdrawThread(card); depositThread.start(); withdrawThread.start(); Thread.sleep(2000); System.out.println("最终余额:" + card.Get_balance()); } }运行后输出如下结果:
现在大致的看一下,初始余额为100,然后存款线程存入100,接下来取款线程取走50,那么最后余额为150。这么看来,貌似没问题?
事实上,存取款过程是需要消耗时间的,只要一个线程在操作余额期间受到其他线程的干扰,就可能出现数据不一致问题。这里我们修改存取款方法的代码如下。
存款方法:
public void deposit(double count) throws InterruptedException{ System.out.println("存钱线程:存入金额=" + count); double now = balance + count; Thread.sleep(100); //存钱的操作用时0.1s balance = now; System.out.println("存钱线程:当前金额=" + balance); }取款方法:
public void withdraw(double count) throws InterruptedException{ System.out.println("取钱线程:取出金额=" + count); double now = balance - count; Thread.sleep(200); //取钱的操作用时0.2s balance = now; System.out.println("取钱线程:当前金额=" + balance); } }然后再运行一遍测试程序:
现在,我们发现最终余额变成了50,这很显然是个完全不符合预期的错误结果。那么,如何来解释这个现象呢? 从上图可以看到,出现数据不一致的原因在于多个线程并发访问了同一个对象,破坏了不可分割的操作,这里的这个共同访问对象就是余额。其实我们所谓预期的‘正确’结果,就是希望先进行存款,然后再进行取款,或者反之。
上面提到‘不可分割的操作’,这种操作就是原子操作。是因为实际上多线程编程的情境下,很多敏感数据不允许被同时访问,因此对于这种针对敏感数据的操作,需要进行线程访问的协调与控制,这就是所谓的线程同步(协同步调)访问技术。线程同步控制的结果,就是把每次对敏感数据的操作变成原子操作,从而让执行顺序按照我们预期的过程进行。 上述情境下,存款与取款应当是两个原子操作,我们必须保证先进行且完成存款操作再进行取款操作,才能保证最终数据的一致性,才能得到我们认为是‘正确’的结果。
下面我们通过锁来实现线程同步访问控制,修改Card类的代码如下。
public class Card { private double balance; private Object lock = new Object(); //锁 ...省略其它代码 /*存款*/ public void deposit(double count) throws InterruptedException{ System.out.println("存钱线程:存入金额=" + count); synchronized (lock) { double now = balance + count; Thread.sleep(100);//存钱的操作用时0.1s balance = now; } System.out.println("存钱线程:当前金额=" + balance); } /*取款*/ public void withdraw(double count) throws InterruptedException{ System.out.println("取钱线程:取出金额=" + count); synchronized (lock) { double now = balance - count; Thread.sleep(200);//取钱的操作用时0.2s balance = now; } System.out.println("取钱线程:当前金额=" + balance); } }运行结果如下:
这段代码中,通过synchronized 关键字保证lock对象只能同时被一个线程访问,要想操作余额,那么必须先获取lock对象的访问许可,因此就保证了余额不会被多个线程同时修改,而最终的结果也完全符合我们的预期。这个lock对象就可以形象的理解成锁,整个执行过程大致如下图所示,
相关资源:JAVA上百实例源码以及开源项目源代码