线程同步的三种方法:
1.同步代码块
2.同步方法
3.同步锁
public class Account {
private String accountNo;
private double balance;
public Account(){}
public Account(String accountNo,double balance){
this.accountNo = accountNo;
this.balance = balance;
}
//省略get和set方法...
}
//1.同步代码块
public class DrawThread extends Thread{
private Account account;
private double drawAmount;
DrawThread(String name,Account account,double drawAmount){
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
public void run(){
//使用account作为同步监视器,任何线程进入下面同步代码块之前
//必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
//这种做法符合:“加锁-->修改-->释放锁”的逻辑
synchronized (account) {
if (account.getBalance() >= drawAmount) {
System.out.println(getName()+
"取钱成功!吐出钞票:"+drawAmount);
// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为: "+account.getBalance());
}
else {
System.out.println(getName()+"取钱失败!余额不足!");
}
}//同步代码块结束,该线程释放同步锁
}
}
上面是用同步代码块来实现多线程安全,下面是另一种方法:同步方法。同步方法的同步监视器是this,也就是对象本身
。具体就是:只要将Account类对balance的访问设置成线程安全的,那么只要把balance的方法修改成同步方法即可
//2.同步方法
/*public class Account{
//省略Filed和其他方法
//.....
public synchronized void draw(double drawAmount){
if (balance >= drawAmount) {
//吐钞操作
}
else {
//取钱失败
}
}
}*/
上面程序为Account类增加了一个代表取钱的draw()方法,并使用了synchronized关键字修饰该方法,使其变为同步方法。同步方法的同步监视器是this,因此对于同一个Account账户而言,任意时刻只能有一个线程获得对Account对象的锁定,然后进入draw()方法执行取钱操作。这样就保证了多个线程的并发取钱的线程安全。
public class DrawTest {
public static void main(String[] args) {
Account acct = new Account("1234567",1000);
new DrawThread("甲方", acct, 800).start();
new DrawThread("乙方", acct, 800).start();
}
}
多次运行上面的程序都可能看到:
/*甲方取钱成功!吐出钞票:800.0
乙方取钱成功!吐出钞票:800.0
余额为: 200.0
余额为: -600.0
*/
//这样的错误结果,但是也有可能看到正确结果,这正是多线程编程的“偶然错误”,因为线程调度的不确定性,假设系统调度器在被注释掉的地方暂停,让另一个线程执行那么就会出现上面的错误(为了强制暂停,只要取消注释,将总能看到错误结果)
之所以出现这样的结果,是因为run() 方法的方法体不具有同步安全性————程序中有两个并发线程在修改Account对象;而且系统恰好在注释掉的代码处执行线程切换,切换给另一个修改Account对象的线程,所以就出现了问题。(主要就是取决于线程的切换时机)
3.同步锁
public class X {
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//定义需要保证线程安全的方法
public void m(){
//加锁
lock.lock();
try {
//需要保证线程安全的代码
//...method body
} catch (Exception e) {
}finally{
lock.unlock();
}
}
}
使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。
使用Lock与使用同步方法有点类似,只是使用Lock时显示使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,同样都符合“加锁-->修改-->释放锁”的操作模式
,而且使用Lock对象时每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象同一时刻只能有一个线程能进入临界区