JAVA线程安全的几种实现方式
场景描述
当2个用户 user_0、user_1 使用同一账户(账户余额为1000元), 在银行(bank)同时执行2次取钱(withdraw)操作,每次取300元,若不采取线程安全的措施,极易造成结果异常(如:取出1200元,以及余额显示异常等问题)。
错误代码与错误结果展示
1、Bank类
package threadSafety;
public class Bank {
private int money;
public Bank(int money) {
this.money = money;
}
//取钱操作
public void withdraw() {
if( (money-300) < 0) {
System.out.println(Thread.currentThread().getName() + "取款时余额不足");
}
else {
money-=300;
System.out.println(Thread.currentThread().getName()+ "取款"+ 300 + " 余额为:" + money);
}
}
}
2、User类
package threadSafety;
public class User extends Thread{
private Bank bank;
public User(Bank bank){
this.bank = bank;
}
public void run() {
int count = 2; //设置取款次数为2
while(count>0) {
bank.withdraw();
try {
sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} //假设取钱操作需要1s
count--;
}
}
}
3、Main类
package threadSafety;
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
Bank bank = new Bank(1000);
User use_0 = new User(bank);
User use_1 = new User(bank);
use_0.start();
use_1.start();
}
}
运行后,出现了以下错误:(两个用户都取了600)
正确的运行结果应当如下所示:(必有一个用户只能取300)
一、使用synchronized关键字保证同步
同步函数使用
将Bank类中的withdraw()函数,设置成同步函数
1、Bank类
package threadSafety;
public class Bank {
private int money;
public Bank(int money) {
this.money = money;
}
//取钱操作
public synchronized void withdraw() {
if( (money-300) < 0) {
System.out.println(Thread.currentThread().getName() + "取款时余额不足");
}
else {
money-=300;
System.out.println(Thread.currentThread().getName()+ "取款"+ 300 + " 余额为:" + money);
}
}
}
使用同步代码块
将User类中调用withdraw()函数的部分,用synchronized块包裹
使用方法
将可能会发生线程安全问题的代码,给包裹起来,如:
synchronized(对象){
// 这个对象可能会发生线程冲突的问题
}
注意事项:
在使用时避免包裹大量代码,应当在必要时使用
同步代码块和同步函数之间的关系
同步函数,相当于将同步代码块中的对象设置为this(同步代码块this锁)
2、User类
package threadSafety;
public class User extends Thread{
private Bank bank;
public User(Bank bank){
this.bank = bank;
}
public void run() {
int count = 2; //设置取款次数为2
while(count>0) {
//这里bank对象可能会发送线程冲突
synchronized (bank) {
bank.withdraw();
}
try {
sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} //假设取钱操作需要1s
count--;
}
}
}
二、使用Lock
将User类中调用withdraw()方法的部分加锁
释放锁,通常该语句都放在finally块中,避免因程序异常结束而造成死锁
2、User类
package threadSafety;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class User extends Thread{
private Bank bank;
//定义一个同步锁
private static Lock lock = new ReentrantLock();
public User(Bank bank){
this.bank = bank;
}
public void run() {
int count = 2; //本实例中假设取款次数为2次
while(count>0) {
try {
//开始锁定
lock.lock();
bank.withdraw();
}finally {
//释放锁,通常该语句都放在finally块中,避免因程序异常结束而造成死锁
lock.unlock();
}
try {
//假设用户取钱需要1s
sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} //假设取钱操作需要1s
count--;
}
}
}
参考文献出处
强烈安利niceyz的该篇博客