实现火车票预售,必然会出现线程安全问题,开始用原子类AtomicInteger,后面觉得Lock也可以实现,用Lock 也是实现了下,在用Synchronized实现的时候,出现了下面的问题;
1.首先大家抢票,那我定义一个Integer类型的count 用来存放火车票数(从这里开始就已经为后面的代码埋下了雷啊!)
2.创建抢票线程类,在这个类的run方法中对count加锁,进行票减少操作(这里就是最大的隐患)
3.启动4个窗口,对火车票进行抢售
代码如下:
public class TikcetTestSync {
private volatile Integer count = 200;
@Test
public void test() throws InterruptedException {
TikcetRunnable tr = new TikcetRunnable();
Thread t1 = new Thread(tr, "窗口A");
Thread t2 = new Thread(tr, "窗口B");
Thread t3 = new Thread(tr, "窗口C");
Thread t4 = new Thread(tr, "窗口D");
t1.start();
t2.start();
t3.start();
t4.start();
Thread.currentThread().join();
}
public class TikcetRunnable implements Runnable {
@Override
public void run() {
while (count > 0) {
synchronized (count) {//count加锁和this加锁区别?
// System.out.println(System.identityHashCode(count));
if (count > 0) {
System.out.println(Thread.currentThread().getName() + " 销售了第 " + (count--) + " 张票");
try {
Thread.sleep(200);
// System.out.println(Thread.currentThread().getName()+"开始继续卖票");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
备注:代码里,我觉得每个线程直接买完票,就释放锁,太不贴近现实,所以休眠200毫秒,然后再释放锁,因为sleep是不会释放锁的
但是运行结果,事与愿违:
可以看到16这张票,重复卖了。。。。。。。这是什么原因呢?锁没加成功?还是其他什么原因?
代码进行修改,把synchronized(count)修改为synchronized(this),运行结果就正确了,看来是count的问题 并不是加不了锁 因此在代码中添加以下代码
public class TikcetRunnable implements Runnable {
@Override
public void run() {
while (count > 0) {
synchronized (count) {
System.out.println(System.identityHashCode(count));
if (count > 0) {
System.out.println(Thread.currentThread().getName() + " 销售了第 " + (count--) + " 张票");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
运行结果是:
这里添加了一句System.out.println(System.identityHashCode(this));目的是打印每一个获得锁的对象的系统hashcode的值,发现每次获得的都不一样,引起这个主要原因是:count--操作的时候等价于 count = new Integer(count-1) ,这里可以看到,count是重新创建的对象,虽然很多地方说是integer值小于127会从常量池中获得,但是整个的逻辑应该还是:加入A窗口先获得了count=200的锁,将存放200的内存锁住,然后count -- 后 count=199,A窗口锁定的还是200那块内存,而这个时候如果B线程获得了使用权限,那B线程看到count=199并没有加锁,那就可以加锁访问,因此B对存放199的这块内存加锁,一此类推,就会出现A窗口加锁还没释放锁,B窗口已经开始加锁,这是因为锁对象一直在变换 但是还是不可能出现一张票多买的问题,那我们继续找原因;
while (count > 0) {
synchronized (count) {
其实问题原因出在这里,线程先读取了count的值,然后才加锁,有可能只读到值,还未加锁,时间片到期,但是下次是顺序执行,下一次过来的时候count的值是上次获得的值,因此会出现一次票多次销售!问题到这里基本都解决了,解决问题的代码如下:
public class TikcetTestSync {
private volatile Integer count = 200;
@Test
public void test() throws InterruptedException {
TikcetRunnable tr = new TikcetRunnable();
Thread t1 = new Thread(tr, "窗口A");
Thread t2 = new Thread(tr, "窗口B");
Thread t3 = new Thread(tr, "窗口C");
Thread t4 = new Thread(tr, "窗口D");
t1.start();
t2.start();
t3.start();
t4.start();
Thread.currentThread().join();
}
public class TikcetRunnable implements Runnable {
@Override
public void run() {
synchronized (count) {
while (count > 0) {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + " 销售了第 " + (count--) + " 张票");
try {
Thread.sleep(200);
System.out.println(Thread.currentThread().getName()+" 本次售票操作结束,等待下一次售票");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
到此,给count加锁的问题解决了,总结一下,当我先循环判断条件,再加锁,并且在加锁过程中调用sleep方法,甚至在sleep方法后打印此窗口本次售票结束,会出现两个让人费解的问题:
1.大家都知道获得锁之后,sleep方法是不会放弃锁的,但是如果运行代码,会发现,A窗口未完成本次售票,B窗口就开始销售票,主要原因是Integer类型的变量,在进行count--操作的时候,会新建一个对象,因此就出现AB线程加锁对象对应的内存地址不一样,因而不用A释放锁,B才能加锁
2.出现一张票多次销售,主要原因是加锁和读值得先后顺序有问题。
希望大家以此为戒!当然方便的话,直接对this,当前的线程对象加锁,对象加锁了,里面成员变量也是被锁住的,通过提高加锁的粒度,简化程序的复杂性!