您的位置:首页 > 编程语言 > Java开发

java多线程——线程同步问题

2015-09-14 20:58 411 查看
试想一个场景:同一个银行账户有1000RMB,两个对象同时对该账户取钱,两人各取800,流程如下:

输入帐号、密码,验证成功

输入取钱金额,系统比较账户余额和取钱金额

验证成功,允许取钱操作,然后,两个人一起开开心心的拿着1.6k回家了。剩下一个余额为-600的账户默默哭泣。

这就是我们常说的:多线程并发操作线程安全问题。

就取钱例子模拟代码实现:

1、定义一个账户类,封装了账户ID和余额两个属性

2、提供取钱线程,执行根据账户信息、取钱数量进行取钱操作

public class Account

{

private String accountNo;

private String balance;

//省略两个属性的get set方法

//构造器

public Account(String accountNo,Stringbalance)

{

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;

returntarget.getAccountNo().equals(accountNo);

}

return false;

}

}

public class DrawThread extends Thread

{

//模拟用户帐户

private Account account;

private double drawAmount;

public DrawThread(String name,Accountaccount,double drawAmount)

{

super(name);

this.account=account;

this.drawAmount=drawAmount;

}

//当多条线程同时修改一个共享数据时,将涉及数据安全问题

public void run()

{

//帐户余额>取钱数目

if(account.getBalance()>=drawAmount)

{

//取钞成功

System.out.println(getName()+"取钱成功,取钱金额为"+drawAmount);

try{

Thread.sleep(1);

}catch(InterruptedException ex)



ex.printStackTrace();



//修改账户余额

account.setBalance(account.getBalance()-drawAmount);

System.out.println("余额为"+account.getBalance());

}

else

{

System.out.println(getName()+"余额不足");

}

}

}

public class TestDraw

{

public static void main(String[] args)

{

//创建一个账户

Account account=newAccount("123",1000);

//模拟两个线程同时对一个账户取钱

newDrawThread("Max",account,800).start();

newDrawThread("Jason",account,800).start();

}

}      多次运行以上代码,会出现1000的账户余额,由于两个线程同时取800的操作,会出现一种情况:两个都取钱成功,账户余额-600.这种错误便是由于多线程并发修改同一个资源造成的线程安全问题。

       由于run方法不具有同步安全性,程序两条并发线程修改Acount对象时,就出现了问题。为了解决该问题,java的多线程提供了三种解决方式。

1、同步代码块

      synchronized(obj)

      {

             ....

             //此处的代码就是同步代码块

      }

       参数obj叫做同步监视器,线程在执行同步代码之前,必须获得同步监视器的锁定。而任何时刻,只能有一条线程获得同步监视器,当同步代码块执行完毕后,该线程才释放对该同步监视器的锁定。

       原理:阻止两条线程对同一个资源并发访问,加锁——》修改完成——》释放锁的流程。

       所以取钱实例可将账户account作为同步监视器,代码修改如下:

public class DrawThread extends Thread

{

//模拟用户帐户

private Account account;

private double drawAmount;

public DrawThread(String name,Accountaccount,double drawAmount)

{

super(name);

this.account=account;

this.drawAmount=drawAmount;

}

public void run()

{

synchronized(account)

{

//帐户余额>取钱数目

if(account.getBalance()>=drawAmount)

{

//取钞成功

System.out.println(getName()+"取钱成功,取钱金额为"+drawAmount);

try{

Thread.sleep(1);

}catch(InterruptedExceptionex)



ex.printStackTrace();



//修改账户余额

account.setBalance(account.getBalance()-drawAmount);

System.out.println("余额为"+account.getBalance());

}

else

{

System.out.println(getName()+"余额不足");

}

}

//同步代码块结束,该线程释放同步监视器

}

}


2、同步方法

       同步方法与同步代码块原理类似,使用synchronized关键字修饰某个方法,这种方法便成为同步方法。使用同步方法的特点是无需显示指定同步监视器,它的同步监视器就是this,也就是该对象本身。

public class Account

{

private String accountNo;

private String balance;

//省略两个属性的get set方法

//构造器

public Account(String accountNo,Stringbalance)

{

this.accountNo = accountNo;

this.balance = balance;

}

//提供一个线程安全的draw方法来完成取钱操作

public synchronized voiddraw(double drawAmount)

{

if(account.getBalance()>=drawAmount)

{

//取钞成功

System.out.println(getName()+"取钱成功,取钱金额为"+drawAmount);

try{

Thread.sleep(1);

}catch(InterruptedExceptionex)



ex.printStackTrace();



//修改账户余额

account.setBalance(account.getBalance()-drawAmount);

System.out.println("余额为"+account.getBalance());

}

else

{

System.out.println(getName()+"余额不足");

}

}

//此处省略了hashCode和equals两个重写方法。

.......

}     增加了取钱的draw方法,并使用了synchronized关键字修饰,变成同步方法,同步监视器为Account类对象本身,所以对同一个账户而言,任何时刻只有一个线程会获得Account对象的锁定,执行取钱操作,

       这样也避免了并发线程同时对该账户取钱的线程安全问题。

       所以在取钱线程类中,无需自己实现取钱操作,直接调用account.draw()即可。这样一来,也更符合面向对象思想,保证了Account类的完整性。

public class DrawThread extends Thread

{

//模拟用户帐户

private Account account;

private double drawAmount;

public DrawThread(String name,Accountaccount,double drawAmount)

{

super(name);

this.account=account;

this.drawAmount=drawAmount;

}

public void run()

{

//直接调用account对象的取钱方法

account.draw(drawAmount);

}

}
注意:

       1、synchronized关键字对方法、代码块均可以修饰,但属性和构造器不行。

       2、在使用synchronized关键字构造同步方法时,注意同步范围,不需要同步的,如上例中的帐号就不需要同步,只对draw方法同步控制即可。

3、同步锁

       JDK1.5以后,java提供了另一宗线程同步机制:通过显示定义同步锁Lock对象来实现同步。锁提供了对共享资源的独占访问,线程运行前先获得Lock对象,而且每次只有一个线程对Lock对象加锁。这里的锁

       等同于对象监视器的作用,但它提供了比以上两种方式更广泛的锁定操作,可以具有锁的一系列属性,所以也显得更灵活。例如使用Lock对象可显示加锁、释放锁、读写锁、可重入锁等方法属性。

       通常使用lock对象的代码格式如下:     

class X
{
//定义锁对象

private final ReentrantLocklock=new ReentrantLock();

//定义需要保证线程安全的方法
public void m()
{
//加锁
lock.lock();
try
{
//方法体
}finally
{
//释放锁
lock.unlock();
}
}
}
      使用Lock锁对Account类改造如下:

public class Account
{
//定义锁对象
private final ReentrantLock lock=new ReentrantLock();

private String accountNo;

private String balance;

//省略两个属性的get set方法

//构造器
public Account(String accountNo,Stringbalance)
{
this.accountNo = accountNo;

this.balance = balance;
}

//提供一个线程安全的draw方法来完成取钱操作

public synchronized voiddraw(double drawAmount)
{
//加锁—

lock.lock();
try
{
if(account.getBalance()>=drawAmount)
{
//取钞成功
System.out.println(getName()+"取钱成功,取钱金额为"+drawAmount);

try{

Thread.sleep(1);

}catch(InterruptedExceptionex)



ex.printStackTrace();



//修改账户余额

account.setBalance(account.getBalance()-drawAmount);

System.out.println("余额为"+account.getBalance());

}

else

{

System.out.println(getName()+"余额不足");

}
       死锁

       当两个线程互相等待对方释放同步监视器时,就会发生死锁,一旦发生死锁,整个程序所有线程出于阻塞状态,无任何异常、也不会给出任何提示。而且,在系统中出现多个同步监视器时,死锁是很容易发生的。      

      
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: