Java多线程(三) 多线程不安全的典型例子
多线程给程序带来了高效率,但是同时也会带来不安全的问题,例如,当多线程操作共享资源时,如果不加以保护和限制,就有可能带来问题,下面三个例子就说明了多线程操作共享资源时的问题。
1、买票问题
现实中大家都有买演唱会门票、火车飞机票的时候,如果票的数量只有100张,但是10000人都要强的话,肯定是要使用多线程的方法进行处理。在这个例子中,假设有20张票,而有三个人想要买这20张票,写一个简单例子就会发现问题。
class Ticket implements Runnable{
private int alltickets = 20;
private boolean flag = true;
@Override
public void run() {
while(alltickets>0) {
try {
Thread.sleep(100);//扩大时长,模拟实际情况
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void buy() throws InterruptedException {
if(this.alltickets<=0)
{
System.out.println(Thread.currentThread().getName()+"没票可买了");
this.flag = false;
return;
}
else
{
System.out.println(Thread.currentThread().getName()+"买了第"+this.alltickets--+"张票 ");
}
}
}
public class ThreadProblem {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Ticket t = new Ticket();
new Thread(t, "小明同学").start();
new Thread(t, "小华同学").start();
new Thread(t, "黄牛").start();
}
}
从结果可以看出来,出现了两个问题
- 买的顺序很乱,比如第18张买的比第19张票早
- 出现了重复票,小华和小明都买到了第5张票
这个问题就出现在了没有保护好共享资源 alltickets,比如在小华这个线程访问第五张这个代码块时,进行了休眠(实际情况可能是时间片结束),小明这个线程也进入了这个代码块,使得两个线程都买到了第五张票。
2、取钱问题
这个场景在银行中,余额就是共享资源,肯定是要进行保护的,否则同一时间多个人对这个账户进行取钱,大家都能取出来,结果余额变成负数了。
在这个例子中,有一个Account类型是账户类型,里面记录了余额,之后定义了一个Person类型,他的成员变量有名字还有每次取出的金额,最后是一个Drawing类,他需要一个Person的对象和一个Account的对象,实现的功能就是取钱(余额 - 取出的金额),在这个例子,是想让小明同学和小华同学取钱,那么给他们相同的Account对象,使得这两个对象对这个共享资源进行操作。
class Account{
public int money;
public Account(int money)
{
this.money = money;
}
}
class Person{
public String name;
public int needmoney;
public Person(String name, int needmoney) {
this.name = name;
this.needmoney = needmoney;
}
}
class Drawing implements Runnable{
Account account;
Person p;
static ReentrantLock lock = new ReentrantLock();
public Drawing(Account account, Person p) {
this.account = account;
this.p = p;
}
@Override
public void run() {
while(true) {
if (this.account.money - p.needmoney <= 0) {
System.out.println(p.name + "没钱取了已经");
break;
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.account.money -= p.needmoney;
System.out.println(p.name + "取了" + p.needmoney + "钱" + ",还有" + this.account.money + "钱");
}
}
}
}
public class ThreadProblem {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Account account = new Account(100);
Person i = new Person("小明同学",20);
Person you = new Person("小华同学",15);
Drawing d1 = new Drawing(account,i);
Drawing d2 = new Drawing(account,you);
new Thread(d1).start();
new Thread(d2).start();
}
}
从这个结果来看出现了以下问题:
- 第三行,小明从余额65中取出20,但是显示余额是30
- 第四行,小华又取了15,但是余额未变化
- 最后一次取完钱余额出现了负数
出现这些的原因还是源于多个线程对共享资源进行了未加保护的操作,举一个例子:小明同学这个线程在余额还有65的情况下进入代码块,但是休眠了(还未出去钱),这时小华同学这个线程进入代码块(因为余额还未修改,因此还是65),并且立即取出金额15(余额为50),这时休眠了(未打印信息),这时小明同学这个线程苏醒,进行取钱(这时余额变成了50),并进行打印(第三行信息),所以显示“还有30钱”,此时小明同学再次休眠,小华同学苏醒进行打印操作,输出第四行。
3、ArrayList
ArrayList是线程不安全的,表现在多线程操作同一个ArrayList对象时的不安全。可以看下面一个例子,有两个线程对同一个ArrayList对象进行20次add()操作。
public class ThreadProblem {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ArrayList<Integer> list = new ArrayList<>();
MyOperator m1 = new MyOperator(list);
MyOperator m2 = new MyOperator(list);
new Thread(m1).start();
new Thread(m2).start();
}
}
class MyOperator implements Runnable
{
ArrayList<Integer> list;
public MyOperator(ArrayList<Integer> list)
{
this.list = list;
}
@Override
public void run() {
for (int i=0;i<20;i++)
{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(i);
System.out.println(Thread.currentThread().getName()+"在第"+list.size()+"的位置增加了一项,现在容量为"+list.size());
}
}
}
可以看到这存在两个问题:
- 有时两个线程在同一个地方进行add操作
- 因为出现了问题1,所以最终ArrayList的总数不到40
这两个问题依旧出现在多线程操作了同一个共享变量,当线程1准备在位置1操作时被休眠,线程2在位置1添加了一个数,之后线程1醒来依旧会在位置1添加,这就出现了覆盖现象。
为了解决这些问题,Java推出了synchronized机制和锁机制,可以阅读这两篇文章继续了解和学习《Java多线程(四) 解决多线程安全——synchronized》以及《Java多线程(六) 解决多线程安全——ReentrantLock及源码解析》