多线程下模拟多窗口售票

还是挺有趣的,哈哈~~~

public class Test {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
		//模拟三个售票窗口
        new Thread(ticket, "A").start();
        new Thread(ticket, "B").start();
        new Thread(ticket, "C").start();
    }
}
//资源类
class Ticket implements Runnable{
    private int num = 30;
    @Override
    public void run() {
        while (true){         
                if (num > 0){
                    try {
                        TimeUnit.MILLISECONDS.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在卖第"+num+"张票");
                    num--;
                }            
        }
    }
 }

我们发现可能会出现重复的票,不存在的票,漏票,为什么呢?

多线程下模拟多窗口售票_代码块
CPU根据自己的喜好,在三个线程中来回的选择执行,所以是可能会出现上面的情况
如何避免呢?

方式一:使用同步代码块,锁的对象是this,也可以是任意对象。当某个线程执行到要操作资源类的步骤时,判断是否有锁,有,获取锁,没有等待其他线程释放锁

  public void run() {
        while (true){
            synchronized (this){
                if (num > 0){
                    try {
                        TimeUnit.MILLISECONDS.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在卖第"+num+"张票");
                    num--;
                }
            }
        }
    }

方式二:同步方法,把操作资源类的步骤提取出来,成为一个方法,锁对象是this

public void run() {
        while (true){
            sale();
      }
}
  public synchronized void sale(){
        if (num > 0){
            try {
                TimeUnit.MILLISECONDS.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"正在卖第"+num+"张票");
            num--;
        }
    }

方式三:使用静态代码块

public void run() {
        while (true){
            sale();
      }
}
public static void sale(){
        synchronized(Test.class){
            if (num > 0){
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在卖第"+num+"张票");
                num--;
            }
        }
    }

方式四:使用Lock锁
在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。

class Ticket implements Runnable{
    private static int num = 30;
    Lock l = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            l.lock();
            if (num > 0){
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                    System.out.println(Thread.currentThread().getName()+"正在卖第"+num+"张票");
                    num--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    l.unlock();
                }

            }
        }
    }
 }

注意
说明一:如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。
说明二:如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。
说明三:在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。