1.多线程下有问题的例子,见UnsynchDemo.java
/** * 多线程中,2个以上线程对同一个共享的数据进行存取时,如果不采取同步机制,会导致这个共享数据出错,原因如下 * * * 导致共享数据出错的原因在于这不是原子操作。如下面例子中线程1/线程2修改用户1余额可能被处理如下(java内存模型中有一块叫主内存的区域存放共享数据的值) * 1)将账户1余额从主内存取出复制一份到寄存器 * 2)增加amount * 3)将修改后的结果写会到主内存 * 现在,假设线程1执行了步骤1和2,然后被剥夺了运行权。假设线程2被唤醒并修改了用户1的余额更新到了主内存中。然后,线程1被唤醒并完成了其步骤3. * 这样,这一动作擦去了线程2所做的更新,导致主内存中的数据不再正确。同理用户2的余额也被这样改错了。导致最终两个用户的总金额不再是最初的2000 * * java中使用同步机制(重入锁java.util.concurrent.locks.ReentrantLock类+java.util.concurrent.locks.Condition类(或简称Lock+Condition类);和synchronized关键字),解决这一问题,保证了数据的准确性。 * 同步的例子: * Lock+Condition类实现同步的例子见SynchDemo1.java; * synchronized关键字实现同步见SynchDemo2.java * * * 另外,本类也模拟了死锁的场景,使用注释的那行DEFAULT_TRANSFER的值即可查看效果 * 当双方都想给对方转钱,但是余额都不足时,导致所有线程阻塞,这样的状态称为死锁。java的同步机制也不能解决死锁问题,所以必须仔细设计程序,以确保不会出现死锁 **/ public class UnsynchDemo{ private static final int DELAY = 20; private static final int INIT_BALANCE = 1000; private static final int DEFAULT_TRANSFER = INIT_BALANCE/100;// 用户1每次转10给用户2,用户2每次转30给用户1 //private static final int DEFAULT_TRANSFER = INIT_BALANCE/100 * 66;// 用户1每次转660给用户2,用户2每次转660*3给用户1,待双方余额都不足转账时发生死锁 public static void main(String[] args){ Bank _bank = new Bank(INIT_BALANCE); new Thread(new Runnable(){ public void run(){ try{ while(true){ _bank.transfer(_bank.getPeopleByNo(1), _bank.getPeopleByNo(2), DEFAULT_TRANSFER); Thread.sleep(DELAY); } }catch(InterruptedException e){ Thread.currentThread().interrupt(); } } }).start(); new Thread(new Runnable(){ public void run(){ try{ while(true){ _bank.transfer(_bank.getPeopleByNo(2), _bank.getPeopleByNo(1), DEFAULT_TRANSFER * 3); Thread.sleep(DELAY); } }catch(InterruptedException e){ Thread.currentThread().interrupt(); } } }).start(); } } class Bank{ People[] mPeoples = new People[2]; public Bank(int initBalance){ mPeoples[0] = new People(initBalance); mPeoples[1] = new People(initBalance); } public People getPeopleByNo(int num){ switch(num){ case 2: return mPeoples[num-1]; default: return mPeoples[0]; } } public void transfer(People from, People to, int amount){ if(from.getBalance() < amount){ return; } from.addBalance(-amount); to.addBalance(amount); System.out.println(toString()); } @Override public String toString(){ return "totalBalance:".concat(String.valueOf(mPeoples[0].getBalance()+mPeoples[1].getBalance())); } } class People{ private int balance; People(int defaultBalance){ this.balance = defaultBalance; } public void addBalance(int amount){ this.balance+= amount; } public int getBalance(){ return this.balance; } }
运行结果如下,一会就发现总额出问题了不再是2000
2.使用重入锁解决问题,见SynchDemo1.java,运行后总额一致是2000
import java.util.*; import java.util.concurrent.locks.*; /** * 多线程中,2个以上线程对同一个共享的数据进行存取时,还可使用重入锁java.util.concurrent.locks.ReentrantLock类+java.util.concurrent.locks.Condition类(或简称Lock+Condition类)实现同步机制,保证共享数据正确性 * * * 代码中,应使用Lock+Condition类还是使用synchronized进行同步呢? * 最好哪个也不使用,许多情况下可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁。如使用其中的阻塞队列来实现同步 * 还可以使用并行流 * 除非synchronized非常适合你的需求,就用它;如果特别需要Lock+Condition的独有特性才使用它 * * * 另外,本类也模拟了死锁的场景,使用注释的那行DEFAULT_TRANSFER的值即可查看效果 * 当双方都想给对方转钱,但是余额都不足时,导致所有线程阻塞,这样的状态称为死锁。java的同步机制也不能解决死锁问题,所以必须仔细设计程序,以确保不会出现死锁 **/ public class SynchDemo1{ private static final int DELAY = 20; private static final int INIT_BALANCE = 1000; private static final int DEFAULT_TRANSFER = INIT_BALANCE/100;// 用户1每次转10给用户2,用户2每次转30给用户1 //private static final int DEFAULT_TRANSFER = INIT_BALANCE/100 * 66;// 用户1每次转660给用户2,用户2每次转660*3给用户1,待双方余额都不足转账时发生死锁 public static void main(String[] args){ Bank _bank = new Bank(INIT_BALANCE); new Thread(new Runnable(){ public void run(){ try{ while(true){ _bank.transfer(_bank.getPeopleByNo(1), _bank.getPeopleByNo(2), DEFAULT_TRANSFER); Thread.sleep(DELAY); } }catch(InterruptedException e){ Thread.currentThread().interrupt(); } } }).start(); new Thread(new Runnable(){ public void run(){ try{ while(true){ _bank.transfer(_bank.getPeopleByNo(2), _bank.getPeopleByNo(1), DEFAULT_TRANSFER * 3); Thread.sleep(DELAY); } }catch(InterruptedException e){ Thread.currentThread().interrupt(); } } }).start(); } } class Bank{ People[] mPeoples = new People[2]; private Lock mBankLock;// 锁 private Condition mSufficientFunds;// 条件 public Bank(int initBalance){ mPeoples[0] = new People(initBalance); mPeoples[1] = new People(initBalance); mBankLock = new ReentrantLock();// 构建一个可以被用来保护临界区的可重入锁 mSufficientFunds = mBankLock.newCondition();// 构造一个条件对象。通常,线程进入临界区,却发现在某一条件满足之后才能执行,如下边from.getBalance() < amount时不满足继续执行的条件。要使用一个条件对象来管理哪些已经持有锁但不能做有用工作的线程 } public People getPeopleByNo(int num){ switch(num){ case 2: return mPeoples[num-1]; default: return mPeoples[0]; } } /** * 使用java.util.concurrent.locks.ReentrantLock类保证数据准确 * mBankLock对象具有持有计数功能,如果transfer()此时嵌套调用了1个别的含有mBankLock.lock();的方法,则mBankLock对象的持有计数变为2,那个方法执行完重新变1,如果transfer执行完变0线程释放锁 **/ public void transfer(People from, People to, int amount){ mBankLock.lock(); try{ while(from.getBalance() < amount){ // 此时发现余额不足,当前线程被阻塞并放弃锁,此时它进入该条件的等待集。希望另一个线程被调度 // 等待获得锁的线程和调用await()的被阻塞后的线程本质上是不同的,一旦一个线程调用await(),它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞,它仍然处于阻塞状态,直到另一线程调用同一条集上的signalAll()方法时为止。 // 其实此时signalAll()会将因为这一条件而等待的所有线程从条件集中移除,并将其状态变为可运行的,可被调度器重新调度。但不会激活他们,他们需要再次竞争,一旦这些线程中某个竞争成功再次获取到锁会从被阻塞前的地方继续执行。 /* // await()的调用应该在如下形式循环体中 while(!(ok to proceed)){ condition.await(); } */ mSufficientFunds.await(); } from.addBalance(-amount); to.addBalance(amount); System.out.println(toString()); // signalAll()让所有同条件的等待集中线程重新进入可运行状态 // signalAll()方法的调用时机,经验上讲,在对象的状态有利于条件集中等待的线程的方向改变时调用 // 比如这里完成转账后,当一个账户余额发生变化时,等待的线程会应该有机会检查余额 mSufficientFunds.signalAll(); }catch(InterruptedException e){ //Thread.currentThread().interrupt();// 不要因为异常的抛出而跳出临界区代码,临界区代码结束前抛出了异常finally子句会释放锁导致对象受损 }finally{ mBankLock.unlock();// 一定要在finally中释放锁 } } @Override public String toString(){ return "totalBalance:".concat(String.valueOf(mPeoples[0].getBalance()+mPeoples[1].getBalance())); } } class People{ private int balance; People(int defaultBalance){ this.balance = defaultBalance; } public void addBalance(int amount){ this.balance+= amount; } public int getBalance(){ return this.balance; } }
3.使用synchronized关键字解决问题,见SynchDemo2.java,运行后总额一致是2000
/** * 多线程中,2个以上线程对同一个共享的数据进行存取时,可以使用synchronized关键字同步机制,保证共享数据正确性 * * 要理解synchronized关键字,必须了解每一个对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程 * 内部所和条件存在一些局限: * 1)不能中断一个正在试图获得锁的线程 * 2)试图获得锁时不能设定超时 * 3)每个锁仅有单一的条件,有的场景这可能是不够的 * * * 使用synchronized获取锁的方法: * 1).获取类对象锁: * static synchronized type XXXMethod(){...} 或 type XXXMethod(){synchronized(XXX.class)} * 2).获取实例对象的锁: * synchronized type XXXMethod(){...} 或 type XXXMethod(){synchronized(XXX.this)} * 3)."同步阻塞"方式获取实例对象的锁: * priavate Object lock = new Object(); * type XXXMethod(){synchronized(lock){...}} * * * 代码中,应使用Lock+Condition类还是使用synchronized进行同步呢? * 最好哪个也不使用,许多情况下可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁。如使用其中的阻塞队列来实现同步 * 还可以使用并行流 * 除非synchronized非常适合你的需求,就用它;如果特别需要Lock+Condition类的独有特性才使用它 * * * 另外,本类也模拟了死锁的场景,使用注释的那行DEFAULT_TRANSFER的值即可查看效果 * 当双方都想给对方转钱,但是余额都不足时,导致所有线程阻塞,这样的状态称为死锁。java的同步机制也不能解决死锁问题,所以必须仔细设计程序,以确保不会出现死锁 **/ public class SynchDemo2{ private static final int DELAY = 20; private static final int INIT_BALANCE = 1000; private static final int DEFAULT_TRANSFER = INIT_BALANCE/100;// 用户1每次转10给用户2,用户2每次转30给用户1 //private static final int DEFAULT_TRANSFER = INIT_BALANCE/100 * 66;// 用户1每次转660给用户2,用户2每次转660*3给用户1,待双方余额都不足转账时发生死锁 public static void main(String[] args){ Bank _bank = new Bank(INIT_BALANCE); new Thread(new Runnable(){ public void run(){ try{ while(true){ _bank.transfer(_bank.getPeopleByNo(1), _bank.getPeopleByNo(2), DEFAULT_TRANSFER); Thread.sleep(DELAY); } }catch(InterruptedException e){ Thread.currentThread().interrupt(); } } }).start(); new Thread(new Runnable(){ public void run(){ try{ while(true){ _bank.transfer(_bank.getPeopleByNo(2), _bank.getPeopleByNo(1), DEFAULT_TRANSFER * 3); Thread.sleep(DELAY); } }catch(InterruptedException e){ Thread.currentThread().interrupt(); } } }).start(); } } class Bank{ People[] mPeoples = new People[2]; public Bank(int initBalance){ mPeoples[0] = new People(initBalance); mPeoples[1] = new People(initBalance); } public People getPeopleByNo(int num){ switch(num){ case 2: return mPeoples[num-1]; default: return mPeoples[0]; } } /** * 使用synchronized关键字保证数据准确,synchronized具有原子性 **/ public synchronized void transfer(People from, People to, int amount){ try{ while(from.getBalance() < amount){ // 此时发现余额不足,当前线程被阻塞并放弃锁,此时它进入该条件的等待集。希望另一个线程被调度 // wait()和notifyAll()等价于mSufficientFunds.await()和mSufficientFunds.signalAll() wait(); } from.addBalance(-amount); to.addBalance(amount); System.out.println(toString()); // notifyAll()让所有同条件的等待集中线程重新进入可运行状态 notifyAll(); }catch(InterruptedException e){ //Thread.currentThread().interrupt();// 不要因为异常的抛出而跳出临界区代码,临界区代码结束前抛出了异常finally子句会释放锁导致对象受损 } } @Override public String toString(){ return "totalBalance:".concat(String.valueOf(mPeoples[0].getBalance()+mPeoples[1].getBalance())); } } class People{ private int balance; People(int defaultBalance){ this.balance = defaultBalance; } public void addBalance(int amount){ this.balance+= amount; } public int getBalance(){ return this.balance; } }
4.死锁的例子DeadLockDemo.java
/** * 本类模拟了死锁的场景 * * 当双方都想给对方转钱,但是余额都不足时,导致所有线程阻塞,这样的状态称为死锁。 * 注意:java的同步机制也不能解决死锁问题,所以必须仔细设计程序,以确保不会出现死锁 **/ public class DeadLockDemo { private static final int DELAY = 20; private static final int INIT_BALANCE = 1000; //private static final int DEFAULT_TRANSFER = INIT_BALANCE/100;// 用户1每次转10给用户2,用户2每次转30给用户1 private static final int DEFAULT_TRANSFER = INIT_BALANCE/100 * 66;// 用户1每次转660给用户2,用户2每次转660*3给用户1,待双方余额都不足转账时发生死锁 public static void main(String[] args){ Bank _bank = new Bank(INIT_BALANCE); new Thread(new Runnable(){ public void run(){ try{ while(true){ _bank.transfer(_bank.getPeopleByNo(1), _bank.getPeopleByNo(2), DEFAULT_TRANSFER); Thread.sleep(DELAY); } }catch(InterruptedException e){ Thread.currentThread().interrupt(); } } }).start(); new Thread(new Runnable(){ public void run(){ try{ while(true){ _bank.transfer(_bank.getPeopleByNo(2), _bank.getPeopleByNo(1), DEFAULT_TRANSFER * 3); Thread.sleep(DELAY); } }catch(InterruptedException e){ Thread.currentThread().interrupt(); } } }).start(); } } class Bank{ People[] mPeoples = new People[2]; public Bank(int initBalance){ mPeoples[0] = new People(initBalance); mPeoples[1] = new People(initBalance); } public People getPeopleByNo(int num){ switch(num){ case 2: return mPeoples[num-1]; default: return mPeoples[0]; } } /** * 使用synchronized关键字保证数据准确,synchronized具有原子性 **/ public synchronized void transfer(People from, People to, int amount){ try{ while(from.getBalance() < amount){ // 此时发现余额不足,当前线程被阻塞并放弃锁,此时它进入该条件的等待集。希望另一个线程被调度 // wait()和notifyAll()等价于mSufficientFunds.await()和mSufficientFunds.signalAll() wait(); } from.addBalance(-amount); to.addBalance(amount); System.out.println(toString()); // notifyAll()让所有同条件的等待集中线程重新进入可运行状态 notifyAll(); }catch(InterruptedException e){ //Thread.currentThread().interrupt();// 不要因为异常的抛出而跳出临界区代码,临界区代码结束前抛出了异常finally子句会释放锁导致对象受损 } } @Override public String toString(){ return "totalBalance:".concat(String.valueOf(mPeoples[0].getBalance()+mPeoples[1].getBalance())); } } class People{ private int balance; People(int defaultBalance){ this.balance = defaultBalance; } public void addBalance(int amount){ this.balance+= amount; } public int getBalance(){ return this.balance; } }
运行结果如下,可见用户1第2次转账660元时因为余额不足而阻塞、用户2同样余额不足转账而发生阻塞、最终导致死锁