0、不同步的问题
并发的线程不安全问题:
多个线程同时操作同一个对象,如果控制不好,就会产生问题,叫做线程不安全。
我们来看三个比较经典的案例来说明线程不安全的问题。
0.1 订票问题
例如前面说过的黄牛订票问题,可能出现负数或相同。
0.2 银行取钱
再来看一个取钱的例子:
/*
模拟一个账户
*/
class Account{
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
/*
模拟取款机,方便设置名字,继承Thread而不是实现Runnable
*/
class Drawing extends Thread{
Account account;
int outMoney;//取出去了多少钱
int outTotal;//总共取到了多少钱
public Drawing(Account account, int outMoney,String name) {
super(name);
this.account = account;
this.outMoney = outMoney;
}
@Override
public void run() {
account.money -= outMoney;
outTotal += outMoney;
System.out.println(this.getName() + "---账户余额为:" + account.money);
System.out.println(this.getName() + "---总共取到了:" + outTotal);
}
}
然后我们写个客户端调用一下,假设两个人同时取钱,操作同一个账户
public class Checkout {
public static void main(String[] args) {
Account account = new Account(200000,"礼金");
Drawing you = new Drawing(account,8000,"你");
Drawing wife = new Drawing(account,300000,"你老婆");
you.start();
wife.start();
}
}
运行起来,问题就会出现。
每次的结果都不一样,而且,这样肯定会把钱取成负数,显然这是非法的(嘻嘻),首先逻辑上需要修改,当钱少于 0 了就应该退出,并且不能继续取钱的动作了。按照这个思路,加上一个判断呢?
if (account.money < outMoney){
System.out.println("余额不足");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
可是即便是这样,发现还是会出现结果为负的情况,无法保证线程安全。
0.3 数字递增
还有一个经典的例子,那就是对于直接计算迭代过慢,而转为多线程。
一个数字 num ,开辟一万个线程对他做 ++ 操作,看结果会是多少。
public class AddSum {
private static int num = 0;
public static void main(String[] args) {
for (int i=0; i<=10000; i++){
new Thread(()->{
num++;
}).start();
}
System.out.println(num);
}
}
每次运算的结果都不一样,一样的是,结果永远 < 10000 。
或者用给 list 里添加数字来测试:
List<String> list = new ArrayList<>();
for (int i=0; i<10000; i++){
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
System.out.println(list.size());
一样的结果。
线程不安全的问题如何解决呢?
一、同步(synchronized)
1.1 问题出现的原因
从前面的介绍里,我们总结出会出现同步问题的情况,也就是并发三要素:多个线程、同时操作、操作同一个对象。另外,操作的特点是:操作类型为修改。这个时候会产生并发的问题,线程安全问题。
1.2 解决方案
- 确保线程安全,第一就是排队。只要排队,那么不管多少线程,始终一个时间点只会有一个线程在执行,就保证了安全。
不过排队会有一个问题:怎么直到轮到我了呢,也就是怎么知道排在前面的线程执行完了呢? - 现实生活中,可能会用类似房卡的形式,前一个人把卡交还了,才会有后面的人有机会入住。这就是锁。
利用 队列 + 锁 的方式保证线程安全的方式叫线程同步,就是一种等待机制,多个同时访问此对象的线程进入这个对象的等待池 形成队列,前面的线程使用完毕后,下一个线程再使用。
锁机制最开始在 java 里就是一个关键字 synchronized(同步),属于排他锁,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可。
按照这种思路,可以想象到这种保证安全方式的弊端,也就是早期的 synchronized 存在的问题:
- 一个线程持有锁会导致其他所有需要这个锁的线程挂起;
- 多线程竞争下,加锁、释放锁导致耗时严重,性能问题;
- 一个优先级高的线程等待一个优先级低的线程的锁释放,会使得本应该的优先级倒置&#