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的多线程提供了三种解决方式。
{
....
//此处的代码就是同步代码块
}
参数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()+"余额不足");
}
}
//同步代码块结束,该线程释放同步监视器
}
}
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方法同步控制即可。
等同于对象监视器的作用,但它提供了比以上两种方式更广泛的锁定操作,可以具有锁的一系列属性,所以也显得更灵活。例如使用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()+"余额不足");
}
死锁
当两个线程互相等待对方释放同步监视器时,就会发生死锁,一旦发生死锁,整个程序所有线程出于阻塞状态,无任何异常、也不会给出任何提示。而且,在系统中出现多个同步监视器时,死锁是很容易发生的。
输入帐号、密码,验证成功
输入取钱金额,系统比较账户余额和取钱金额
验证成功,允许取钱操作,然后,两个人一起开开心心的拿着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()+"余额不足");
}
死锁
当两个线程互相等待对方释放同步监视器时,就会发生死锁,一旦发生死锁,整个程序所有线程出于阻塞状态,无任何异常、也不会给出任何提示。而且,在系统中出现多个同步监视器时,死锁是很容易发生的。
相关文章推荐
- java-多线程1
- 1.spring-bean-1工程源码浅析(来源郝佳的书)
- Java Web 中文乱码的问题
- Spring AOP
- Java线程中的wait, notify and notifyAll
- spring事物注解不起作用的解决方式
- 如何判断二叉树是否是结构性对称的?
- java_单列集合复习
- java中this关键字的用法
- Java实现中文算数验证码(算数运算+-*/)
- java基础 iterator
- java学习之正则表达式
- MyEclipse不能编译的解决方案
- Java Unit Testing - JUnit & TestNG
- Java基础知识记录
- java io流的一部分解析
- JAVA String 不可变对象
- java 模拟多个客户端与服务器建立UDP连接
- Java习题4—IO流与异常
- Java生成双击可执行的jar包