上面两篇讲了怎么创建线程和两个种常见的创建线程的方式。那么我们来试一试怎么日常生活中的多线程的场景
1.经典的卖票应用
package ThreadTest; /** * 两个窗口同时卖票 * @author lingfengz * */ public class SalesThread { public static void main(String[] args) { Thread thread1 = new Thread(new salesTest("卖票窗口1")); Thread thread2 = new Thread(new salesTest("卖票窗口2")); thread1.start(); thread2.start(); } } class salesTest implements Runnable{ private String salesName; //票据总量为100 private int salescount = 100; public salesTest(String salesName){ this.salesName = salesName; } /** * 无参构造方法,我建议每个要实例化的model都写一个无参构造方法 */ public salesTest(){ } /** * 实现run方法 */ @Override public void run() { // TODO Auto-generated method stub try{ //当还有余票的时候,我们就开始销售 while(salescount>0){ sales(); } }catch(Exception e){ e.getStackTrace(); } } /** * 记录销售的方法 */ public void sales(){ System.out.println(salesName+"---卖出第"+(100-salescount+1)+"票"); salescount--; } }
表面上我们看这段代码好像并没有什么问题,那么我们下面获得的结果却是?
卖票窗口1---卖出第1票 卖票窗口2---卖出第1票 卖票窗口2---卖出第2票 卖票窗口2---卖出第3票
怎么会窗口1和2同时卖出第一张票呢?这是为什么呢?
原因就是每次多线程启动的时候其实修改的是jvm为线程分配的内存副本中的数据,而不是真实的内存中的数据。我们可以把这个操作想象成我们平时用的git或者svn。我们从这些版本控制器中获取代码,但是我们修改的时候修改的并不是版本控制器中的代码,而是我们的本地副本。如果我们想要更新版本控制器中的代码,那么我们就要进行提交操作。
所以我们上面会出现这样的结果。因为没有保证数据的原子性。原子性是什么呢?原子性就是一件事情不能被干扰。比如 ++这个操作,其实分三步。首先创建一个数据副本,然后对数据副本做加操作,最后再把操作后的值给原来的数据。如果我们创建了数据副本后,该数据被其他的线程修改了,那么我们数据副本中的值其实是过期的。这个就是破坏了数据的原子性。
那么上面这个我们又可以牵扯到数据副本和内存的映射。因为我们多线程操作数据其实都是操作的内存副本中的数据。所以如果线程修改了某个共享的变量,那么其他线程可以从主存中获取到实际的值。那么这就是数据的可见性。
我们这一章就讲这些,下一章我们来看看有什么方式来解决这个问题