2.线程的同步
对于上述3个窗口共同卖票的案例,采用方法二虽然可以实现3个窗口共享5张票,但是可能会出现同一张票被重复卖多次的情况(多运行几次的话,就可能会遇到这种不正常的情况)。尤其是当每道线程在卖票时需要一定时间延迟时,出现同一张票被重复卖多次的情况的概率会更高。
【代码12.10】
模拟现实情况时,考虑到可能卖票过程需要一段时间间隔,代码中增加了第20行和第21行。
代码12.10的运行结果:
第二窗口卖掉第5张票
第三窗口卖掉第4张票
第一窗口卖掉第3张票
第三窗口卖掉第2张票
第二窗口卖掉第1张票
第一窗口卖掉第0张票
第三窗口卖掉第-1张票
运行结果出现了不正常的情况。模拟可能的执行情形:假设当前tickets的值是1,当一道线程读取tickets的值时,tickets>0为true,然后线程执行到第20行,进入休眠状态(还未执行到tickets--);在这个时刻,CPU切换给另一道线程,该线程又读取tickets值,此时tickets的值仍旧是1,tickets>0为true,然后线程进入休眠状态,假设这段休眠时间很短,CPU并没有发生切换,接着本线程输出“卖出了第1张票”,执行到tickets--,此时tickets变为0;然后CPU切换回上一道线程,这道线程已经不会再判断tickets>0了(因为已经判断过了),而是直接输出“卖出了第0张票”,执行到tickets--,此时tickets变为-1。这样就可能造成同一张票被卖了两次的情况。我们把这种情况叫作线程不同步。
当多线程并发修改共享数据时,可能会造成数据错误,这种情况叫作线程不安全。那么怎样解决线程不安全的问题呢?解决的方法就是,针对多线程共享的数据,保证一个线程在处理共享数据的时候,其他线程都等待,直到该线程结束对共享数据的处理为止。这种方法叫作线程的同步控制。
【代码12.11】线程同步的方法一:同步代码块
在代码12.11中,第16行的关键字synchronized后面的大括号构成同步代码块,在同一时刻只能有一道线程可以在同步代码块内运行,只有当该线程离开同步代码块后,其他线程才能进入同步代码块内,这样就避免了上述的线程不安全的情况。同步代码块的定义语法:
synchronized后面小括号中的对象在这里就是当前Sale Tickets任务类对象t,3道线程都共享同一个任务类对象t,t对象对应着一把“锁”,所以3道线程需争夺这一把“锁”。获得“锁”的线程可以进入同步代码块,其他的线程要进入就必须等待“锁”的释放,当获得“锁”进入同步代码块的线程结束执行,并离开同步代码块时,就释放“锁”,其他等待“锁”的线程就可以获得“锁”而进入同步代码块。
【代码12.12】线程同步的方法二:同步方法
除了可以对代码块进行同步外,也可以对方法进行同步,只要在需要同步的方法定义前加上synchronized关键字即可。
同步方法的定义语法:
访问控制符synchronized返回值类型方法名称(参数)
当一个线程进入有synchronized修饰的方法时,其他线程就不能进入此方法,直到前一个线程执行完此方法为止。
线程同步是以牺牲程序效率为代价的,所以在保证线程安全的情况下,应该尽量缩小同步的范围。如果是在单线程的执行情况下,则提供线程不安全的版本来运行。