ITKeyword,专注技术干货聚合推荐

注册 | 登录

JAVA 多线程--线程同步安全

标签: 多线程 Java

相关推荐:程序员的视角:java 线程

在我们开始谈线程之前,不得不提下进程。无论进程还是线程都是很抽象的概念,有一个关于进程和线程很形象的比喻能帮我们更好的理解。进程就像个房子,房子是

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

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

首先定义一个account.java

个人账户的实体类: 1 /** 2

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

*

4

* @author root 5

*

6

*/ 7 public class Account { 8

9

private String accountNo;10

private double balance;11 12

public Account() {13

}14 15

// 构造器16

public Account(String accountNo, double balance) {17

this.accountNo = accountNo;18

this.balance = balance;19

}20 21

public String getAccountNo() {22

return accountNo;23

}24 25

public void setAccountNo(String accountNo) {26

this.accountNo = accountNo;27

}28 29

public double getBalance() {30

return balance;31

}32 33

public void setBalance(double balance) {34

this.balance = balance;35

}36 37

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

public int hashCode() {39

return accountNo.hashCode();40

}41 42

public boolean equals(Object obj) {43

if (obj != null && obj.getClass() == Account.class) {44

Account target = (Account) obj;45

return target.getAccountNo().equals(accountNo);46

}47

return false;48

}49 50

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

public synchronized void draw(double drawAmount) {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 package thread.threadInBank; 2

3 /** 4

* 取钱的线程类,该线程类根据执行账户,取钱的数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统吐出 5

* 钞票,余额对应的减少 6

* @author root 7

* 8

*/ 9 public class DrawThread extends Thread{10 11

//模拟用户账户12

private Account account;13

14

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

private double drawAmount;16

17

public DrawThread(String name,Account account,double drawAmount){18

super(name);19

this.account = account;20

this.drawAmount= drawAmount;21

}22

23

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

public void run(){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 package thread.threadInBank; 2

3 public class testDraw { 4

public static void main(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 }

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

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

我们很容易发现上述程序出现问题是因为:当前run方法的方法体不具备同步安全性,而程序中的两个并发线程(甲乙两次取钱)都在修改该账户,所以为了解决这个问题,我们需要使用java多线程引入的同步监视器来解决,也就是将当前取钱的那段代码使用synchronized 关键字修饰,也就是使之成为同步代码块:package thread.threadInBank;/** * 取钱的线程类,该线程类根据执行账户,取钱的数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统吐出 钞票,余额对应的减少 *

* @author root *

*/public class DrawThread extends Thread

相关推荐:多进程与多线程区别

在Unix上编程采用多线程还是多进程的争执由来已久,这种争执最常见到在C/S通讯中服务端并发技术 的选型上,比如WEB服务器技术中,Apache是采用多进程的(per

{

// 模拟用户账户

private Account account;

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

private double drawAmount;

public DrawThread(String name, Account account, double drawAmount) {

super(name);

this.account = account;

this.drawAmount = drawAmount;

}

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

public void run() {

// 使用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,:

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

其实同步方法和同步代码块类似,都是使用sychronized关键字。只是这次是修饰整个方法,而这个被修饰的方法就被称为同步方法: 1 // 使用同步方法提供一个线程安全的draw方法来完成取钱的操作 2 public synchronized void draw(double drawAmount) { 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 package thread.threadInBank; 2

3 import java.util.concurrent.locks.ReentrantLock; 4

5 /** 6

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

*

8

* @author root 9

* 10

*/11 public class Account_lock {12 13

//定义锁对象14

private final ReentrantLock lock= new ReentrantLock();15

private String accountNo;16

private double balance;17 18

public Account_lock() {19

}20 21

// 构造器22

public Account_lock(String accountNo, double balance) {23

this.accountNo = accountNo;24

this.balance = balance;25

}26 27

省略了属性的get和set方法28

省略了equals和hashCode两个方法29 30

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

public

void draw(double drawAmount) {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 package thread.threadInBank; 2

3 /** 4

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

*

6

* @author root 7

*

8

*/ 9 public class DrawThread extends Thread {10

private Account_lock account_lock;11 12

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

private double drawAmount;14 15

public DrawThread(String name, Account_lock account_lock, double drawAmount) {16

super(name);17

this.account_lock = account_lock;18

this.drawAmount = drawAmount;19

}20 21

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

public void run() {23

//直接调用使用同步锁的方法24

account_lock.draw(drawAmount);25

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

}27 }

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

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

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

相关推荐:Java多线程

在并发编程中,有两个基本的执行单元:进程和线程。即使在只有单一的 CPU的计算机系统中,也有许多活动的进程和线程。因此,在任何给定的时刻,同一进程

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

相关阅读排行


用户评论

游客

相关内容推荐

最新文章

×

×

请激活账号

为了能正常使用评论、编辑功能及以后陆续为用户提供的其他产品,请激活账号。

您的注册邮箱: 修改

重新发送激活邮件 进入我的邮箱

如果您没有收到激活邮件,请注意检查垃圾箱。