多线程编程常常容易突然出现一些“错误情况”,当多个线程共享资源时,经常会出现一些线程安全问题。以ATM为例,如果A/B两个人同时往一个账户(Account)中存钱,这个账户中本来有1000元,假如A取得了账户(Account)对象后,B也取得这个账户对象,然后A、B分别向该账户中存入1000元,查询账户余额会发现账户中只有2000元,而不是正常的3000元。我们先来看一下这段代码:
首先,我们先定义了一个不可变类Account,来封装账户编号和余额两个属性:
运行这个线程类,就会出现前面说的情况。之所以会出现这种现象,其原因就是其中的run方法不具有同步安全性:程序中有两条线程在修改Account对象,而恰好在执行setBalance方法语句之前切换给另外一条线程。所以,就发生了问题。
为了解决这个问题, java中引入了同步监视器(synchronized)。当用关键字sychronized修饰某个对象时,这个对象就是同步监视器。对于同步代码块来说,同步监视器就是指定的对象(synchronized(obj)中obj。为了体现同步监视器的作用,我们通常使用可能被并发共享的资源充当监视器。对于上面的代码,我们把Account对象当做同步监视器。然后,我们只要把run方法的执行体变成同步代码块就可以了。修改后的代码如下:
上面的代码通过使用synchronized将run方法的执行体变成了同步代码块,这样符合“加锁---执行修改---解锁”的逻辑,任何线程想修改资源之前,首先要对该对象进行加锁,在解锁之前,其他线程是不能调用该资源的,当该线程修改完成,该线程释放对所用资源的锁定。
synchronzied还支持对方法进行同步。当一个方法用synchronized来修饰时,它就是同步方法了。对于同步方法而言,无需显式的定义同步监视器,它的同步监视器是this对象,也就是类本身。
使用同步方法可以非常方便的使某个可变类(线程不安全的类)变成线程安全类,这里就不再举例说明了。
需要注意的是,可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全带来的负面影响,程序可以采取以下策略:
1.不要对线程安全类的所有方法都进行同步,只对那些会改变会竞争资源的方法进行同步。
2.如果可变类有两种运行环境:单线程和多线程。那么就应该为该可变类提供两种版本:线程不安全版本和线程安全版本。对于单线程,就使用线程不安全版本。
任何线程进入同步代码块或同步方法,必须先获得对同步监视器的锁定。程序无法显式释放对同步监视器的锁定,线程会在如下几种情况时释放对同步监视器的锁定:
1.当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
2.当线程在同步代码块、同步方法中遇到break/return终止了该代码块,该方法的继续执行,当前线程将会释放同步监视器。
3.当线程在同步代码块、同步方法中出现了未处理的异常或错误,导致了该代码块、该方法异常结束时释放同步监视器。
4.当线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait方法,则当前线程暂停,并释放同步监视器。
在下面情况下,线程不会释放同步监视器:
1.线程执行同步方法或同步代码块时,程序调用了Thread.sleep()、Thread.yield()方法来暂停当前线程,当前线程不会释放同步监视器。
2.线程执行同步代码块时,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器。
从JDK1.5之后,java提供了另外一种线程同步机制:它通过显式定义同步锁对象来实现同步,在这种情况下,同步锁应该由lock对象充当。使用lock对象来进行同步是,锁定和释放锁出现在不同范围中时,通常建议使用finnally块来确保必要时释放锁。lock锁比同步方法和同步不代码块更加灵活。lock提供了同步方法和同步代码块没有的其他功能,包括用于非块结构的trylock方法,以及试图获取可中断锁lockInterruptibly()方法,还有获取超时失效的tryLock(long,TimeUnit)方法。
我们常用的lock锁ReentrantLock,它具有可重入性,也就是说线程可以对它已经加锁的ReentrantL锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock方法的嵌套调用,线程在每次调用lock()加锁后,必须显式的用unlock()释放锁,并且,释放多个锁时,必须要以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。
我们也可以用lock锁来是实现存钱的线程,我们先对竞争资源A从count类进行同步:
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private final ReentrantLock lock=new ReentrantLock();
//封装银行账号、余额两个属性
private String accountNo;
private double balance;
public Account(){}
public Account(String accountNo,double balance){
this.accountNo=accountNo;
this.balance=balance;
}
public int hashCode(){
return accountNo.hashCode();
}
public boolean equals(Object obj){
if(obj!=null&&obj.getClass()==Account.class){
Account target=(Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
public void save(double saveAccount){
lock.lock();
try{
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
finally{
lock.unlock();
}
balance=balance+saveAccount;
System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:"+balance);
}
/**
* @return the accountNo
*/
public String getAccountNo() {
return accountNo;
}
/**
* @param accountNo the accountNo to set
*/
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
/**
* @return the balance
*/
public double getBalance() {
return balance;
}
/**
* @param balance the balance to set
*/
public void setBalance(double balance) {
this.balance = balance;
}
}
然后,在线程类Thread类的执行体中直接调用Account类的save方法就行。
public class SaveThread extends Thread {
private Account account;
private double saveAccount;
public SaveThread(String name,Account account,double saveAccount){
super(name);
this.account=account;
this.saveAccount=saveAccount;
}
public void run(){
account.save(saveAccount);
}
public static void main(String args[]){
Account acc=new Account("1111",1000);
new SaveThread("线程1",acc,1000).start();
new SaveThread("线程2",acc,1000).start();
}
}
当两个线程相互等待对方释放同步监视器时就会发生死锁。这是因为在调用同步监视器时要先对对象进行加锁,如果一个同步线程对该对象加锁以后 ,cpu切换到另一个同步线程,这个同步线程也会对该对象加锁,但是由于前一个同步线程没有释放同步监视器,所以只能等待,而前一个线程又要等待这个线程释放同步监视器。所以就出现了相互僵持的局面,整个程序不会发生任何异常,也不会给出任何提示。