线程安全问题出现的原因:
1.存在多个线程同时运行
2.访问同一个共享资源
3.存在修改共享资源
案例: 银行取款
直接上代码:模拟银行取款的线程安全问题
1.先创建一个账户类(Account)
public class Account {
private String cardId;
private double money;
//小红和小明同时过来
public void drawMoney(double money){
//先判断谁来取钱
String name = Thread.currentThread().getName();
//判断余额是否充足
if (this.money >= money){
System.out.println(name + "来取钱,取钱成功!");
this.money -= money;
System.out.println(name + "来取钱后,账户余额" + this.money);
}else{
System.out.println(name + "来取钱,余额不足!");
}
}
public String getCardId() {
return cardId;
}
public double getMomey() {
return money;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public void setMomey(double momey) {
this.money = momey;
}
public Account(String cardId, double momey) {
this.cardId = cardId;
this.money = momey;
}
public Account() {
}
}
2.创建一个任务类(DrawThread)
public class DrawTread extends Thread{
private Account acc;
public DrawTread(Account acc, String name) {
super(name);
this.acc = acc;
}
@Override
public void run() {
//取钱(小明,小红)
acc.drawMoney(100000.00);
}
}
3.测试(ThreadTest)
public class ThreadTest {
//模拟线程安全问题
public static void main(String[] args) {
//银行账里有10万元,小明和小红同事来取钱(取10万)
Account acc = new Account("ZMSN-110",100000.00);
new DrawTread(acc,"小红").start();
new DrawTread(acc,"小明").start();
}
}
最后的结果:
小明来取钱,取钱成功!
小红来取钱,取钱成功!
小明来取钱后,账户余额0.0
小红来取钱后,账户余额-100000.0
从这里就可以看出出现了线程安全问题。上面的代码具体的讲解就不再过多赘述,主要是了解线程安全问题是怎么产生的。
线程同步:解决线程安全问题
线程同步的思想:让多个线程先后依次访问公共资源,从而解决问题
常见方案: 加锁:每次只允许一个线程加锁,加锁后才能访问,访问完毕后自动解锁,然后将其他线程再加锁进来
加锁的方案有三种。下面来介绍三中方案:
作用:把访问共享资源的核心代码上锁,保证线程安全
synchronized(同步锁){
访问共享资源的核心代码
}
同步锁使用的注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
实例方法建议用 this 作为锁对象 ;静态方法建议用 类名.class 作为锁对象。
以上面的案例为解决对象:
public static void Test() {
synchronized (Account.class){
//静态方法推荐使用 类名.class 来上锁
}
}
//小红和小明同时过来
public void drawMoney(double money){
//先判断谁来取钱
String name = Thread.currentThread().getName();
//判断余额是否充足
//this正好代表共享资源,这里最好不要用字符串,因为字符串在常量池中只有一份,会锁住其他的人,比如小明一家来取钱会锁住小黑一家人
//synchronized ("字符串") {}最好不要用
synchronized (this) {
if (this.money >= money){
System.out.println(name + "来取钱,取钱成功!");
this.money -= money;
System.out.println(name + "来取钱后,账户余额" + this.money);
}else{
System.out.println(name + "来取钱,余额不足!");
}
}
}
上面如果用字符串,则会锁住小黑一家人,当小明一家人正在取钱时,小黑一家人是不能取钱的,会出现问题的,建议用this
public static void main(String[] args) {
//银行账里有10万元,小明和小红同事来取钱(取10万)
Account acc = new Account("ZMSN-110",100000.00);
new DrawTread(acc,"小红").start();
new DrawTread(acc,"小明").start();
//另一家人
// Account acc1 = new Account("ZMSN-111",100000.00);
//new DrawTread(acc1,"小黑").start();
// new DrawTread(acc1,"小白").start();
}
作用:把访问共享资源的方法给上锁,保证线程安全
修饰符 synchronized 返回值类型 方法名称(参数列表){
共享资源的代码
}
底层原理:含有隐似的锁对象,只是锁的范围是整个方法的代码
同步方法和同步代码块的优缺点:
范围上:同步代码块锁的范围更小,同步方法锁的范围更大 *
可读性:同步方法更好
//小红和小明同时过来
public synchronized void drawMoney(double money){
//加一个synchronized 即可
}
Lock是接口,所以要找他的实现类ReentrantLock来创建对象
private finall Lock l = new ReentrantLock();(这里建议用finally修饰)
解锁的代码放在try-catch-finally的finally{}中。
public class Account {
private String cardId;
private double money;
private finall Lock l = new ReentrantLock();
//小红和小明同时过来
public void drawMoney(double money){
//先判断谁来取钱
String name = Thread.currentThread().getName();
l.lock();加锁
//判断余额是否充足
if (this.money >= money){
System.out.println(name + "来取钱,取钱成功!");
this.money -= money;
System.out.println(name + "来取钱后,账户余额" + this.money);
}else{
System.out.println(name + "来取钱,余额不足!");
}
l.unlock();解锁
}
三种方式的运行结果:
小红来取钱,取钱成功!
小红来取钱后,账户余额0.0
小明来取钱,余额不足!
成功解决问题!