Java 多线程实例详解(三)
本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题。
一.一个典型的Java线程安全例子
publicclassThreadTest{ publicstaticvoidmain(String[]args){ Accountaccount=newAccount("123456",1000); DrawMoneyRunnabledrawMoneyRunnable=newDrawMoneyRunnable(account,700); ThreadmyThread1=newThread(drawMoneyRunnable); ThreadmyThread2=newThread(drawMoneyRunnable); myThread1.start(); myThread2.start(); } } classDrawMoneyRunnableimplementsRunnable{ privateAccountaccount; privatedoubledrawAmount; publicDrawMoneyRunnable(Accountaccount,doubledrawAmount){ super(); this.account=account; this.drawAmount=drawAmount; } publicvoidrun(){ if(account.getBalance()>=drawAmount){//1 System.out.println("取钱成功,取出钱数为:"+drawAmount); doublebalance=account.getBalance()-drawAmount; account.setBalance(balance); System.out.println("余额为:"+balance); } } } classAccount{ privateStringaccountNo; privatedoublebalance; publicAccount(){ } publicAccount(StringaccountNo,doublebalance){ this.accountNo=accountNo; this.balance=balance; } publicStringgetAccountNo(){ returnaccountNo; } publicvoidsetAccountNo(StringaccountNo){ this.accountNo=accountNo; } publicdoublegetBalance(){ returnbalance; } publicvoidsetBalance(doublebalance){ this.balance=balance; } }
上面例子很容易理解,有一张银行卡,里面有1000的余额,程序模拟你和你老婆同时在取款机进行取钱操作的场景。多次运行此程序,可能具有多个不同组合的输出结果。其中一种可能的输出为:
1取钱成功,取出钱数为:700.0
2余额为:300.0
3取钱成功,取出钱数为:700.0
4余额为:-400.0
也就是说,对于一张只有1000余额的银行卡,你们一共可以取出1400,这显然是有问题的。
经过分析,问题在于Java多线程环境下的执行的不确定性。CPU可能随机的在多个处于就绪状态中的线程中进行切换,因此,很有可能出现如下情况:当thread1执行到//1处代码时,判断条件为true,此时CPU切换到thread2,执行//1处代码,发现依然为真,然后执行完thread2,接着切换到thread1,接着执行完毕。此时,就会出现上述结果。
因此,讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。
二.同步方法
对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推...
在上例中,共享资源为account对象,当使用同步方法时,可以解决线程安全问题。只需在run()方法前加上synshronized关键字即可。
publicsynchronizedvoidrun(){ //.... }
三.同步代码块
正如上面所分析的那样,解决线程安全问题其实只需限制对共享资源访问的不确定性即可。使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的情况,于是,针对需要同步的代码可以直接另一种同步方式——同步代码块来解决。
同步代码块的格式为:
synchronized(obj){ //... }
其中,obj为锁对象,因此,选择哪一个对象作为锁是至关重要的。一般情况下,都是选择此共享资源对象作为锁对象。
如上例中,最好选用account对象作为锁对象。(当然,选用this也是可以的,那是因为创建线程使用了runnable方式,如果是直接继承Thread方式创建的线程,使用this对象作为同步锁会其实没有起到任何作用,因为是不同的对象了。因此,选择同步锁时需要格外小心...)
四.Lock对象同步锁
上面我们可以看出,正因为对同步锁对象的选择需要如此小心,有没有什么简单点的解决方案呢?以方便同步锁对象与共享资源解耦,同时又能很好的解决线程安全问题。
使用Lock对象同步锁可以方便的解决此问题,唯一需要注意的一点是Lock对象需要与资源对象同样具有一对一的关系。Lock对象同步锁一般格式为:
classX{ //显示定义Lock同步锁对象,此对象与共享资源具有一对一关系 privatefinalLocklock=newReentrantLock(); publicvoidm(){ //加锁 lock.lock(); //...需要进行线程安全同步的代码 //释放Lock锁 lock.unlock(); } }
五.wait()/notify()/notifyAll()线程通信
在博文《Java总结篇系列:java.lang.Object》中有提及到这三个方法,虽然这三个方法主要都是用于多线程中,但实际上都是Object类中的本地方法。因此,理论上,任何Object对象都可以作为这三个方法的主调,在实际的多线程编程中,只有同步锁对象调这三个方法,才能完成对多线程间的线程通信。
wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。
notify():唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
packagecom.qqyumidi; publicclassThreadTest{ publicstaticvoidmain(String[]args){ Accountaccount=newAccount("123456",0); ThreaddrawMoneyThread=newDrawMoneyThread("取钱线程",account,700); ThreaddepositeMoneyThread=newDepositeMoneyThread("存钱线程",account,700); drawMoneyThread.start(); depositeMoneyThread.start(); } } classDrawMoneyThreadextendsThread{ privateAccountaccount; privatedoubleamount; publicDrawMoneyThread(StringthreadName,Accountaccount,doubleamount){ super(threadName); this.account=account; this.amount=amount; } publicvoidrun(){ for(inti=0;i<100;i++){ account.draw(amount,i); } } } classDepositeMoneyThreadextendsThread{ privateAccountaccount; privatedoubleamount; publicDepositeMoneyThread(StringthreadName,Accountaccount,doubleamount){ super(threadName); this.account=account; this.amount=amount; } publicvoidrun(){ for(inti=0;i<100;i++){ account.deposite(amount,i); } } } classAccount{ privateStringaccountNo; privatedoublebalance; //标识账户中是否已有存款 privatebooleanflag=false; publicAccount(){ } publicAccount(StringaccountNo,doublebalance){ this.accountNo=accountNo; this.balance=balance; } publicStringgetAccountNo(){ returnaccountNo; } publicvoidsetAccountNo(StringaccountNo){ this.accountNo=accountNo; } publicdoublegetBalance(){ returnbalance; } publicvoidsetBalance(doublebalance){ this.balance=balance; } /** *存钱 * *@paramdepositeAmount */ publicsynchronizedvoiddeposite(doubledepositeAmount,inti){ if(flag){ //账户中已有人存钱进去,此时当前线程需要等待阻塞 try{ System.out.println(Thread.currentThread().getName()+"开始要执行wait操作"+"--i="+i); wait(); //1 System.out.println(Thread.currentThread().getName()+"执行了wait操作"+"--i="+i); }catch(InterruptedExceptione){ e.printStackTrace(); } }else{ //开始存钱 System.out.println(Thread.currentThread().getName()+"存款:"+depositeAmount+"--i="+i); setBalance(balance+depositeAmount); flag=true; //唤醒其他线程 notifyAll(); //2 try{ Thread.sleep(3000); }catch(InterruptedExceptione){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"--存钱--执行完毕"+"--i="+i); } } /** *取钱 * *@paramdrawAmount */ publicsynchronizedvoiddraw(doubledrawAmount,inti){ if(!flag){ //账户中还没人存钱进去,此时当前线程需要等待阻塞 try{ System.out.println(Thread.currentThread().getName()+"开始要执行wait操作"+"执行了wait操作"+"--i="+i); wait(); System.out.println(Thread.currentThread().getName()+"执行了wait操作"+"执行了wait操作"+"--i="+i); }catch(InterruptedExceptione){ e.printStackTrace(); } }else{ //开始取钱 System.out.println(Thread.currentThread().getName()+"取钱:"+drawAmount+"--i="+i); setBalance(getBalance()-drawAmount); flag=false; //唤醒其他线程 notifyAll(); System.out.println(Thread.currentThread().getName()+"--取钱--执行完毕"+"--i="+i);//3 } } }
上面的例子演示了wait()/notify()/notifyAll()的用法。部分输出结果为:
取钱线程开始要执行wait操作执行了wait操作--i=0
存钱线程存款:700.0--i=0
存钱线程--存钱--执行完毕--i=0
存钱线程开始要执行wait操作--i=1
取钱线程执行了wait操作执行了wait操作--i=0
取钱线程取钱:700.0--i=1
取钱线程--取钱--执行完毕--i=1
取钱线程开始要执行wait操作执行了wait操作--i=2
存钱线程执行了wait操作--i=1
存钱线程存款:700.0--i=2
存钱线程--存钱--执行完毕--i=2
取钱线程执行了wait操作执行了wait操作--i=2
取钱线程取钱:700.0--i=3
取钱线程--取钱--执行完毕--i=3
取钱线程开始要执行wait操作执行了wait操作--i=4
存钱线程存款:700.0--i=3
存钱线程--存钱--执行完毕--i=3
存钱线程开始要执行wait操作--i=4
取钱线程执行了wait操作执行了wait操作--i=4
取钱线程取钱:700.0--i=5
取钱线程--取钱--执行完毕--i=5
取钱线程开始要执行wait操作执行了wait操作--i=6
存钱线程执行了wait操作--i=4
存钱线程存款:700.0--i=5
存钱线程--存钱--执行完毕--i=5
存钱线程开始要执行wait操作--i=6
取钱线程执行了wait操作执行了wait操作--i=6
取钱线程取钱:700.0--i=7
取钱线程--取钱--执行完毕--i=7
取钱线程开始要执行wait操作执行了wait操作--i=8
存钱线程执行了wait操作--i=6
存钱线程存款:700.0--i=7
由此,我们需要注意如下几点:
1.wait()方法执行后,当前线程立即进入到等待阻塞状态,其后面的代码不会执行;
2.notify()/notifyAll()方法执行后,将唤醒此同步锁对象上的(任意一个-notify()/所有-notifyAll())线程对象,但是,此时还并没有释放同步锁对象,也就是说,如果notify()/notifyAll()后面还有代码,还会继续进行,知道当前线程执行完毕才会释放同步锁对象;
3.notify()/notifyAll()执行后,如果右面有sleep()方法,则会使当前线程进入到阻塞状态,但是同步对象锁没有释放,依然自己保留,那么一定时候后还是会继续执行此线程,接下来同2;
4.wait()/notify()/nitifyAll()完成线程间的通信或协作都是基于不同对象锁的,因此,如果是不同的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系;
5.当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。
当然,上面的例子相对来说比较简单,只是为了简单示例wait()/notify()/noitifyAll()方法的用法,但其本质上说,已经是一个简单的生产者-消费者模式了。
系列文章:
java多线程实例讲解(一)
Java多线程实例详解(二)
Java多线程实例详解(三)