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

时间:2021-05-26 21:06:24

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

一、引言        

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

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

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和判断equals
38 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 }
JAVA 多线程--线程同步安全

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

JAVA 多线程--线程同步安全
 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 }
JAVA 多线程--线程同步安全

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

JAVA 多线程--线程同步安全
 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 }
JAVA 多线程--线程同步安全

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

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

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

二、同步代码块

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

JAVA 多线程--线程同步安全
package thread.threadInBank;

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

// 模拟用户账户
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()
+ "取钱失败,余额不足");
}
}
//同步代码块结束,该线程释放同步锁
}
}
JAVA 多线程--线程同步安全

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

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

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

三、同步方法

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

JAVA 多线程--线程同步安全
 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 }
JAVA 多线程--线程同步安全

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

四、同步锁:Lock

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

JAVA 多线程--线程同步安全
 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 }

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

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

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