java 同步 安全_JAVA多线程--线程的同步安全

每当我们在项目中使用多线程的时候,我们就不得不考虑线程的安全问题,而与线程安全直接挂钩的就是线程的同步问题。而在java的多线程中,用来保证多线程的同步安全性的主要有三种方法:同步代码块,同步方法和同步锁。下面就一起来看:

一、引言

最经典的线程问题:去银行存钱和取钱的问题,现在又甲乙两个人去同一个账户中取款,每人取出800,但是账户中一共有1000元,从逻辑上来讲,如果甲取走800,那么乙一定取不出来800:

首先定义一个account.java  个人账户的实体类:

1 /**

2 * 模仿银行取钱的经典问题:在当前的账户类中封装账户编号和余额两个属性3 *4 *@authorroot5 *6 */

7 public classAccount {8

9 privateString accountNo;10 private doublebalance;11

12 publicAccount() {13 }14

15 //构造器

16 public Account(String accountNo, doublebalance) {17 this.accountNo =accountNo;18 this.balance =balance;19 }20

21 publicString getAccountNo() {22 returnaccountNo;23 }24

25 public voidsetAccountNo(String accountNo) {26 this.accountNo =accountNo;27 }28

29 public doublegetBalance() {30 returnbalance;31 }32

33 public void setBalance(doublebalance) {34 this.balance =balance;35 }36

37 //下面两个方法根据accountNo来计算account的hashcode和判断equals

38 public inthashCode() {39 returnaccountNo.hashCode();40 }41

42 public booleanequals(Object obj) {43 if (obj != null && obj.getClass() == Account.class) {44 Account target =(Account) obj;45 returntarget.getAccountNo().equals(accountNo);46 }47 return false;48 }49

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

51 public synchronized void draw(doubledrawAmount) {52 //账户余额大于取钱的数目

53 if (balance >=drawAmount) {54 //吐出钞票

55 System.out.println(Thread.currentThread().getName() + "取钱成功,吐出钞票!" +drawAmount);56

57 try{58 Thread.sleep(1);59 } catch(InterruptedException e) {60 e.printStackTrace();61 }62 //修改余额

63 balance -=drawAmount;64 System.out.println("\t 余额为: " +balance);65 } else{66 System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足");67 }68 }69

70 }

之后写一个模仿取钱的线程类:

1 packagethread.threadInBank;2

3 /**

4 * 取钱的线程类,该线程类根据执行账户,取钱的数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统吐出5 * 钞票,余额对应的减少6 *@authorroot7 *8 */

9 public class DrawThread extendsThread{10

11 //模拟用户账户

12 privateAccount account;13

14 //当前取钱线程所希望取出的的钱数

15 private doubledrawAmount;16

17 public DrawThread(String name,Account account,doubledrawAmount){18 super(name);19 this.account =account;20 this.drawAmount=drawAmount;21 }22

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

24 public voidrun(){25 //账户余额大于取钱的数目

26 if (account.getBalance() >=drawAmount) {27 //吐出钞票

28 System.out.println(getName()+ "取钱成功,吐出钞票!" +drawAmount);29

30 //try {31 //Thread.sleep(1);32 //} catch (InterruptedException e) {33 //e.printStackTrace();34 //}35 //修改余额

36 account.setBalance(account.getBalance() -drawAmount);37 System.out.println("\t 余额为: " +account.getBalance());38 }else{39 System.out.println(getName() + "取钱失败,余额不足");40 }41 }42 }

写一个测试方法,来测试当前的取钱操作:

1 packagethread.threadInBank;2

3 public classtestDraw {4 public static voidmain(String[] args) {5 //创建一个用户

6 Account acct = new Account("1234567",1000);7 //模拟两个线程对同一个账户取钱

8 new DrawThread("甲", acct, 800).start();9 new DrawThread("乙", acct, 800).start();10

11 }12 }

乍一看,上面的程序好像也没有什么问题,但是多次运行之后会出现下面两种结果:

9721363e58dc201c1340b9612d402a83.png         

73c91f3da097eb1441096758ca7efe0a.png

所以这样的程序肯定存在问题,那么我们应该如何对上述程序进行更改,使得当前的账户不能取第二次800;这就必须提到java提供的线程同步的第一种方法:同步代码块

二、同步代码块

我们很容易发现上述程序出现问题是因为:当前run方法的方法体不具备同步安全性,而程序中的两个并发线程(甲乙两次取钱)都在修改该账户,所以为了解决这个问题,我们需要使用java多线程引入的同步监视器来解决,也就是将当前取钱的那段代码使用synchronized 关键字修饰,也就是使之成为同步代码块:

packagethread.threadInBank;/*** 取钱的线程类,该线程类根据执行账户,取钱的数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统吐出 钞票,余额对应的减少

*

*@authorroot

**/

public class DrawThread extendsThread {//模拟用户账户

privateAccount account;//当前取钱线程所希望取出的的钱数

private doubledrawAmount;public DrawThread(String name, Account account, doubledrawAmount) {super(name);this.account =account;this.drawAmount =drawAmount;

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

public voidrun() {//使用account作为同步监视器,任何线程进入下面的同步代码之前//必须Ian获得对account账户的锁定--其他线程无法获得锁,也就无法修改它//这种做法符合:加锁--》 修改完成 -- 》释放锁 的逻辑

synchronized(account) {//账户余额大于取钱的数目

if (account.getBalance() >=drawAmount) {//吐出钞票

System.out.println(getName() + "取钱成功,吐出钞票!" +drawAmount);try{

Thread.sleep(1);

}catch(InterruptedException e) {

e.printStackTrace();

}//修改余额

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

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

}else{

System.out.println(getName()+ "取钱失败,余额不足");

}

}//同步代码块结束,该线程释放同步锁

}

}

这样无论何时都会保证当前账户中无法由乙提取出第二个800,:

d5d69a4a2f3a18ccd29ca487c01012c7.png

所以这种方法保证了当前只有一个线程可以处于临界区(修改共享资源的代码区),从而保证了线程的安全性;

三、同步方法

其实同步方法和同步代码块类似,都是使用sychronized关键字。只是这次是修饰整个方法,而这个被修饰的方法就被称为同步方法:

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

2 public synchronized void draw(doubledrawAmount) {3 //账户余额大于取钱的数目

4 if (balance >=drawAmount) {5 //吐出钞票

6 System.out.println(Thread.currentThread().getName() + "取钱成功,吐出钞票!" +drawAmount);7

8 try{9 Thread.sleep(1);10 } catch(InterruptedException e) {11 e.printStackTrace();12 }13 //修改余额

14 balance -=drawAmount;15 System.out.println("\t 余额为: " +balance);16 } else{17 System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足");18 }19 }

这样我们直接在测试类中调用上面的同步方法就可以了,这样可以保证多条线程并发调用draw方法且不会出现问题;

四、同步锁:Lock

其实同步锁就是显式地对当前程序加锁,而且每次只能有一个线程对lock对象加锁。所以我们使用同步锁的方法改善银行取钱问题:

1 packagethread.threadInBank;2

3 importjava.util.concurrent.locks.ReentrantLock;4

5 /**

6 * 模仿银行取钱的经典问题:在当前的账户类中封装账户编号和余额两个属性7 *8 *@authorroot9 *10 */

11 public classAccount_lock {12

13 //定义锁对象

14 private final ReentrantLock lock= newReentrantLock();15 privateString accountNo;16 private doublebalance;17

18 publicAccount_lock() {19 }20

21 //构造器

22 public Account_lock(String accountNo, doublebalance) {23 this.accountNo =accountNo;24 this.balance =balance;25 }26

27 省略了属性的get和set方法28 省略了equals和hashCode两个方法29

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

31 public void draw(doubledrawAmount) {32 //对同步锁进行加锁

33 lock.lock();34 try{35 //账户余额大于取钱的数目

36 if (balance >=drawAmount) {37 //吐出钞票

38 System.out.println(Thread.currentThread().getName() + "取钱成功,吐出钞票!" +drawAmount);39

40 try{41 Thread.sleep(1);42 } catch(InterruptedException e) {43 e.printStackTrace();44 }45 //修改余额

46 balance -=drawAmount;47 System.out.println("\t 余额为: " +balance);48 } else{49 System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足");50 }51 //使用finally块来确保释放锁

52 } finally{53 lock.unlock();54 }55

56 }57 }

添加一个测试类,测试当前的取钱操作:

1 packagethread.threadInBank;2

3 /**

4 * 取钱的线程类,该线程类根据执行账户,取钱的数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统吐出 钞票,余额对应的减少5 *6 *@authorroot7 *8 */

9 public class DrawThread extendsThread {10 privateAccount_lock account_lock;11

12 //当前取钱线程所希望取出的的钱数

13 private doubledrawAmount;14

15 public DrawThread(String name, Account_lock account_lock, doubledrawAmount) {16 super(name);17 this.account_lock =account_lock;18 this.drawAmount =drawAmount;19 }20

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

22 public voidrun() {23 //直接调用使用同步锁的方法

24 account_lock.draw(drawAmount);25 //同步代码块结束,该线程释放同步锁

26 }27 }

可以发现,当前的方法依旧可以实现取钱成功的操作;

但是可能会有人担心,线程的同步会不会影响程序的性能?

所以我们在使用多线程的时候,要尽可能的只对会改变竞争资源的方法进行同步,并且在多线程环境中使用线程安全的版本,这样尽可能地减少多线程安全给我们带来的负面影响;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值